Compare commits

...
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
1abab4cb1b Version Packages (#6294)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-25 02:33:10 -08:00
Joaquim VergesandGitHub 1851a8c1da [thirdweb] feat: Add activeWallet prop to PayEmbed (#6335) 2025-02-25 02:22:49 -08:00
Joaquim VergesandGitHub 6345634e2c [thirdweb] fix: Remove icon property check in currency tests (#6334) 2025-02-25 01:35:00 -08:00
a19c0c96ff [SDK] Feature: Add SiweOptions for useConnectModal (#6317)
Co-authored-by: Joaquim Verges <[email protected]>
2025-02-25 01:34:44 -08:00
gregandGitHub cf202c03dd [Dashboard] Fix: Use Secret Key to Fetch Routes on Server (#6333) 2025-02-24 23:21:12 -07:00
Joaquim VergesandGitHub 28df6f7e40 [SDK] fix: Force pre-EIP1559 transactions on Homeverse chains (#6332) 2025-02-24 21:37:38 -08:00
gregandGitHub 150a90c387 Feature: Routes page (#6318) 2025-02-24 22:16:02 -07:00
Giselle Chacon NessiandGitHub 454fc6a5b9 Update billing management text (#6258) 2025-02-24 19:33:36 -08:00
Giselle Chacon NessiandGitHub 72c3499366 removed discord cta (#6259) 2025-02-24 19:31:58 -08:00
Jonas DanielsandGitHub 5612542e1a migrate analytics params from clientId/accountId to teamId/projectId (#6331) 2025-02-24 19:30:33 -08:00
Phillip HoandGitHub 1fa36c1d56 chore: allow passing teamId to service-utils auth (#6329) 2025-02-25 07:36:14 +08:00
Jonas DanielsandGitHub fa99acf609 migrate user op analytics to server components (#6328) 2025-02-24 15:34:14 -08:00
Joaquim VergesandGitHub 15adec4fb7 [SDK] feat: Enhance fiat onramp with multi-step flow and currency support (#6323) 2025-02-24 15:19:35 -08:00
Jonas DanielsandGitHub 1405289ad0 migrate analytics endpoints from v1 to v2 (#6326) 2025-02-24 12:47:29 -08:00
kilavvyandGitHub 95cc74e807 fix: typos in documentation files (#6321)
Signed-off-by: kilavvy <[email protected]>
2025-02-24 12:21:13 -08:00
gregandGitHub 2cb89cb3a1 [SDK] Fix: Upload storage timeout (#6300) 2025-02-23 21:26:05 -08:00
saminaandGitHub f693ab8869 [Portal] Minor docs updates for Nebula (#6320) 2025-02-23 20:18:38 +13:00
MananTank 208b052a1b [TOOL-3476] Dashboard: Add Transfer Project in Project settings page (#6316)
<!--

## 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 project management features by introducing team member retrieval, project transfer capabilities, and UI improvements in the `ProjectGeneralSettingsPage`. It also updates the handling of team roles and member information.

### Detailed summary
- Updated `AccountTeamsUI` to handle empty image string for avatars.
- Added `getMemberById` function to fetch team member details.
- Replaced `getMembers` with `getMemberById` in `Page` component.
- Enhanced `DangerSettingCard` to include `isDisabled` prop.
- Refactored `ProjectGeneralSettingsPage` to manage team roles and project transfers.
- Introduced `TransferProject` component for transferring projects between teams.
- Updated storybook stories to include owner and member account scenarios.

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

<!-- end pr-codex -->
2025-02-22 18:23:44 +00:00
MananTank 514c1a8480 [TOOL-3496] Dashboard: Ecosystem pages UI improvements (#6311)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/yNOf1svJ8o3zjO7zQouZ/f1d0c79f-f711-42e3-9c5e-2b57bfacc05b.png)

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/yNOf1svJ8o3zjO7zQouZ/39702f47-477c-4177-bf3b-ac41c05b5093.png)

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/yNOf1svJ8o3zjO7zQouZ/ce5cc358-a16a-4a06-90a1-a11ff47f67e4.png)

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring and enhancing the loading components and ecosystem management pages in the `dashboard` application. It introduces improvements in code structure, UI components, and error handling.

### Detailed summary
- Removed unused files from `utils`, `data`, and `ecosystem` directories.
- Updated `GenericLoadingPage` to accept a `className` prop for styling.
- Refactored loading components to use the new `GenericLoadingPage` structure.
- Enhanced `EcosystemCreatePage` layout and styling.
- Improved `EcosystemAnalyticsPage` to fetch and display wallet usage statistics.
- Replaced direct API calls with token-based authentication in ecosystem pages.
- Updated `EcosystemHeader` to utilize new UI components and improve layout.
- Refactored ecosystem creation form for better user experience and error handling.

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

<!-- end pr-codex -->
2025-02-22 01:38:35 +00:00
jnsdls a2d75df4c6 improve Turnstile captcha error messages (#6315) 2025-02-22 01:29:04 +00:00
MananTank 5d345e7abb [TOOL-3497] Dashboard: Add Linked Wallets page in account layout, minor UI tweaks in account layout (#6313)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the account management features in the dashboard by introducing linked wallets functionality, refining UI elements, and improving code organization.

### Detailed summary
- Added `/account/wallets` route with "Linked Wallets" label.
- Updated button text from "Create a Team" to "Create Team".
- Changed `<div>` to `<header>` in `AccountSettingsPage.tsx`.
- Enhanced `Devices` page structure with headers and descriptions.
- Implemented `getLinkedWallets` API function.
- Updated `Page` component to fetch linked wallets and account info.
- Refactored `LinkWallet` and `LinkWalletUI` components for better state management and UI.
- Added stubs for unlinking wallets in `LinkWalletsUI.stories.tsx`.
- Improved wallet unlinking functionality with confirmation dialogs.

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

<!-- end pr-codex -->
2025-02-22 01:21:02 +00:00
jnsdls 43638e5d96 only validate Turnstile captcha on Vercel environments (#6314) 2025-02-22 00:16:34 +00:00
jnsdls cc5cdd4f79 enable team invites for starter and growth plans (#6312)
blocked by: https://github.com/thirdweb-dev/api-server/pull/1412
2025-02-21 23:43:33 +00:00
jnsdls 8823acbeb2 add Turnstile captcha to login flow (#6310)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on integrating `Turnstile` CAPTCHA for enhanced security during the login process in the `LoginPage`. It modifies the `doLogin` function to include token validation and updates the UI to present the `Turnstile` challenge.

### Detailed summary
- Added `TURNSTILE_SITE_KEY` and `Turnstile` component to `LoginPage.tsx`.
- Introduced state management for `turnstileToken`.
- Updated `doLogin` function to accept `turnstileToken` and validate it.
- Enhanced error handling for login failures.
- Updated UI to display `Turnstile` CAPTCHA.

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

<!-- end pr-codex -->
2025-02-21 21:20:45 +00:00
MananTank 035fcf666a [TOOL-3493] Dashboard: Fix search in team members page, minor ui tweaks (#6309)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the member management functionality by adding search capabilities to filter team members and invites based on a search term. It introduces new state variables and updates components to utilize these features.

### Detailed summary
- Added `searchTerm`, `setSearchTerm`, and `searchPlaceholder` to relevant components.
- Updated the `Input` component to use `searchTerm` and `setSearchTerm`.
- Implemented filtering logic for invites and members based on `searchTerm`.
- Replaced `GradientAvatar` with `Badge` in `TeamSettingsSidebar`.

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

<!-- end pr-codex -->
2025-02-21 16:17:03 +00:00
MananTank 7775e7c14e [TOOL-3473] Add Team Member invites (#6301)
<!--

## 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 team management features in the dashboard, including invite management, member roles, and UI improvements for better user experience.

### Detailed summary
- Added `skipShowingPlans` prop in `LoginPage`.
- Updated `AnnouncementBanner` to include new layout segments.
- Enhanced `TeamMember` type with new fields.
- Introduced `DotsBackgroundPattern` component for UI.
- Implemented `service_getTeamBySlug` API function.
- Added `leaveTeam` functionality in various components.
- Improved onboarding logic to skip plans.
- Created `JoinTeamPage` with invite handling.
- Added invite management features in `ManageInvitesSection`.
- Enhanced member management with delete functionality.
- Updated UI components for better accessibility and usability.

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

<!-- end pr-codex -->
2025-02-21 07:28:35 +00:00
MananTank fa6da43a77 [TOOL-3488] Dashboard: Add disclaimer for starter plan about verification hold on pricing card (#6308) 2025-02-21 07:20:19 +00:00
MananTank 733c1e6136 [TOOL-3483] Dashboard: Fix Nebula subdomain rewrite for Vercel preview links (#6303)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates the middleware logic to handle subdomains more flexibly, accommodating both the standard `nebula` subdomain and a new format used in Vercel previews.

### Detailed summary
- Changed the condition to check for `subdomain` being either `nebula` or starting with `nebula---`.
- Updated the path rewriting logic to prepend `nebula-app` to the existing paths when the condition is met.

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

<!-- end pr-codex -->
2025-02-20 21:24:40 +00:00
Jonas DanielsandGitHub 439485bc47 [Apps] login (#6295) 2025-02-20 12:06:03 -08:00
MananTank 5c2da5fc5d [TOOL-3485] Dashboard: Improve USD currency formatting (#6304)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving the formatting of monetary values across various components by replacing manual formatting with a utility function `toUSD`. This enhances consistency and readability in displaying currency values.

### Detailed summary
- Added `toUSD` utility function for consistent currency formatting.
- Replaced manual currency formatting in `PaymentsSuccessRate.tsx`, `Payouts.tsx`, `PayCustomersTable.tsx`, `TotalVolumePieChart.tsx`, and `TotalSponsoredChartCard.tsx`.
- Modified random number generation in `storyUtils.ts` to use non-integer values.
- Updated `format-utils.ts` to use a new compact number formatter.

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

<!-- end pr-codex -->
2025-02-20 20:00:55 +00:00
Waren GonzagaandGitHub 846a5b5028 Update page.mdx (#6302)
Signed-off-by: Waren Gonzaga <[email protected]>
2025-02-20 10:48:58 -08:00
MananTank 724e7d9238 [TOOL-3419] Nebula: Add hardcoded reply for Nebula Capabilities (#6298)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the user interface and functionality of various components in the dashboard application, particularly in handling links, chat interactions, and example prompts for a feature called Nebula.

### Detailed summary
- Updated `className` for improved styling in `markdown-renderer.tsx`.
- Changed `backgroundImage` gradient in `EmptyStateChatPageContent.tsx`.
- Added `target` attribute for external links in `Sidebar.tsx`.
- Introduced `examplePrompts` with new responses in `ChatPageContent.tsx`.
- Added `interceptedReply` to `ExamplePrompt` interface in `examplePrompts.ts`.
- Provided detailed descriptions of Nebula's capabilities in `examplePrompts.ts`.

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

<!-- end pr-codex -->
2025-02-19 18:51:27 +00:00
MananTank c30b6d7c14 [TOOL-3470] Playground: Fix eslint image warnings (#6297) 2025-02-19 18:47:34 +00:00
0xFirekeeper 45ac0cb683 [Docs] Improve Unity Build Instructions (#6287)
Closes TOOL-3456

<!--

## 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 descriptions and improving the clarity of build instructions for Unity and troubleshooting OAuth redirects.

### Detailed summary
- Fixed a typo in the `description` for the Unreal Engine link.
- Changed section title from `General` to `All Target Platforms`.
- Clarified build settings for IL2CPP Code Generation.
- Added details about OAuth redirect troubleshooting, including best practices for bundle IDs.

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

<!-- end pr-codex -->
2025-02-19 07:56:18 +00:00
MananTank 8610b467b6 [TOOL-3447] Dashboard: Remove /v1/keys usage (#6293)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on refactoring the project and team management functionalities in the application, specifically updating the handling of API keys, project settings, and team responses, alongside improving redirection logic and type definitions.

### Detailed summary
- Removed `getAPIKeys.ts`.
- Updated types for `TeamResponse` and `ProjectResponse`.
- Changed redirection logic in various components to use the correct team/project slugs.
- Updated `ProjectGeneralSettingsPage` to use the new project structure.
- Refactored API key handling to support new project structure.
- Replaced instances of `LazyCreateAPIKeyDialog` with `LazyCreateProjectDialog`.
- Updated service utilities to include new project-related types.
- Improved error handling and user feedback in settings pages.

> The following files were skipped due to too many changes: `apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts`, `apps/dashboard/src/components/settings/ApiKeys/Create/index.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.tsx`

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

<!-- end pr-codex -->
2025-02-19 00:51:37 +00:00
kumaryash90 d4a49b2fbd [SDK] fix: Custom create2 gas config update (#6286)
TOOL-3458

<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new gas price configuration for a blockchain network named `Ronin` within the `create-2-factory.ts` file.

### Detailed summary
- Added a new entry for the year `"2020"` with:
  - `name`: `"Ronin"`
  - `gasPrice`: `110n * 10n ** 9n`

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

<!-- end pr-codex -->
2025-02-18 21:27:58 +00:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
35b7a0e991 Version Packages (#6290)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-18 10:43:34 -08:00
MananTank ee951926fb [TOOL-3453] Dashboard: Fix NFT Metadata issues when adding media URLs instead of uploading (#6288)
=

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the NFT metadata handling in the dashboard application, specifically renaming and refactoring properties related to custom images and animations, improving form handling, and enhancing user interface elements.

### Detailed summary
- Updated documentation link in `next-env.d.ts`.
- Changed `customImage` and `customAnimationUrl` to `image` and `animation_url` in multiple components.
- Updated titles and headers for improved clarity in forms.
- Enhanced form controls with better error handling and user feedback.
- Adjusted layout and spacing in several components for better usability.

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

<!-- end pr-codex -->
2025-02-18 18:28:32 +00:00
Joaquim VergesandGitHub 5abbae7368 [thirdweb] fix: Respect buyWithFiat setting in PayEmbed (#6289) 2025-02-18 10:28:03 -08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
b98c228f26 Version Packages (#6280)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-18 19:01:01 +13:00
Joaquim VergesandGitHub 5a08176eb0 [thirdweb] fix: Respect supportedTokens override in PayEmbed (#6283) 2025-02-18 18:04:20 +13:00
Jonas DanielsandGitHub 647c285136 add abstract chain to supported networks (#6284) 2025-02-17 20:16:06 -08:00
Joaquim VergesandGitHub 45ca0334bb [thirdweb] feat: Replace walletClient with wallet in viemAdapter (#6282) 2025-02-17 16:41:33 -08:00
Joaquim VergesandGitHub a0f355717a [SDK] fix: Text wrapping and balance display in token selector (#6281) 2025-02-17 16:41:19 -08:00
YashandGitHub b182302f59 [SDK] bytes32 salt for deterministic deployment (#6274) 2025-02-17 13:50:49 -08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
942d20a5d9 Version Packages (#6266)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-17 02:46:05 -08:00
Joaquim VergesandGitHub 739a2ae3c5 [wagmi-adapter] fix: Improve autoconnect handling and storage (#6273) 2025-02-17 01:35:00 -08:00
Joaquim VergesandGitHub 4158a796a8 [SDK] fix: Adjust swap summary border styling (#6272) 2025-02-17 00:12:52 -08:00
Joaquim VergesandGitHub a258e7eaf0 [SDK] feat: Redesign token selection flow in PayEmbed (#6268) 2025-02-16 19:10:19 -08:00
warengonzaga e386ac9476 Update page.mdx (#6270)
Update "extension" to "extensions"

<!--

## 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 `balanceOf` function from the `thirdweb/extension/erc1155` to `thirdweb/extensions/erc1155`, ensuring that the correct module is referenced for checking wallet balances.

### Detailed summary
- Changed the import statement for `balanceOf` from `thirdweb/extension/erc1155` to `thirdweb/extensions/erc1155`.
- Added a newline at the end of the file.

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

<!-- end pr-codex -->
2025-02-17 03:01:03 +00:00
Joaquim VergesandGitHub 0574eac02c [SDK] feat: Enable chain switching for EIP1193 provider (#6269) 2025-02-16 14:03:13 -08:00
gregandGitHub 52cbcd2d57 [Pay] Fix: fix fiat value to 2 decimals (#6243) 2025-02-15 12:54:12 -08:00
Joaquim VergesandGitHub 387891dde0 [SDK] feat: Redesign payment method selector UI (#6265) 2025-02-15 12:53:45 -08:00
Joaquim VergesandGitHub 562c534d37 [SDK] feat: new PayEmbed UI for selecting tokens (#6257) 2025-02-15 12:52:40 -08:00
MananTank b882e29935 [TOOL-3430] SDK: Fix ConnectEmbed showing a flash of Sign In prompt screen even if user is already signed in (#6262)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on improving the user experience in the `ConnectEmbed` component by addressing the display of a "Sign In" prompt when the login status is uncertain. Instead, it shows a loading indicator during this period.

### Detailed summary
- Updated the condition for rendering the loading screen in `SignatureScreen.tsx`.
- The loading screen is now shown if either `wallet` is not available or `siweAuth.isLoading` is true, enhancing user feedback during the login process.

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

<!-- end pr-codex -->
2025-02-15 08:43:56 +00:00
MananTank 9ad1d3cbbf [TOOL-3429] Dashboard: Delete Insight playground, Add Playground Link (#6261)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/yNOf1svJ8o3zjO7zQouZ/fb78547c-031f-44e0-bbd3-410a534d237d.png)
2025-02-15 02:10:24 +00:00
MananTank aab3da0729 [TOOL-3431] Dashboard: Add toggle theme button on logged out page header and other UI tweaks (#6263)
<!--

## 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 components related to the `TeamHeader` and `HeaderLoggedOut`, improving code organization and consistency, and updating UI elements for better styling.

### Detailed summary
- Renamed `TeamHeaderLoggedOut` to `HeaderLoggedOut` and updated its import paths.
- Consolidated the header components for logged-out states.
- Changed class names for styling consistency.
- Refactored color mode toggle component to `ToggleThemeButton`.
- Updated button variants and styles in several components.
- Adjusted layout and spacing in various UI elements for improved design.

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

<!-- end pr-codex -->
2025-02-15 01:51:40 +00:00
kumaryash90 78c9d2fe13 [Dashboard] Crosschain tab UI fixes (#6264)
TOOL-3433

<!--

## 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 user experience in the dashboard by improving messaging for contract deployment, adding asynchronous handling for network selection, and integrating a mutation for adding chains, which provides feedback during the process.

### Detailed summary
- Updated message in `no-crosschain-prompt.tsx` to clarify contract functionality.
- Modified `SingleNetworkSelector` to include `isAddingRow` prop and changed `handleChange` to be asynchronous.
- Added mutation for adding rows in `DataTable`, with feedback for processing and error states.
- Removed redundant `handleAddRow` function from `DataTable`.

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

<!-- end pr-codex -->
2025-02-14 22:11:22 +00:00
GWSzeto 557d3e3dbd [Dashboard] Feature: UI for crosschain modules (#5399)
https://linear.app/thirdweb/project/[modular-contracts]-op-interoperability-module-87740e1390a9/overview

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces new features and improvements related to cross-chain contract deployment in the `thirdweb` framework, enhancing functionality and usability for developers.

### Detailed summary
- Added `getDeployedCloneFactoryContract` export in `contract.ts`.
- Introduced `moduleInstalledEvent` export in `modules.ts`.
- Updated TypeScript documentation links.
- Enhanced sidebar links in `getContractPageSidebarLinks.ts`.
- Improved `namehash.ts` with typed return value.
- Created `NoCrossChainPrompt` component for user feedback.
- Added `nonce` and `initializeData` options in `installPublishedModule.ts`.
- Updated event handling for `ProxyDeployedV2`.
- Implemented cross-chain deployment logic in various files.
- Updated tests to cover new cross-chain functionality.
- Refactored contract deployment methods to support new parameters.

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

<!-- end pr-codex -->
2025-02-14 18:46:16 +00:00
Jonas DanielsandGitHub 06d6504545 add teamId parameter to webhook endpoints (#6260) 2025-02-14 07:48:54 -08:00
kumaryash90 e201e3d423 Handle single unnamed param (#6242)
TOOL-3401

Dashboard handles single unnamed param as "*". Handle this in sdk. Throw error for all other cases.

<!--

## 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 `normalizeFunctionParams` function to handle cases where parameter names may be missing or empty. It introduces error handling for unnamed parameters and updates the associated tests to cover these scenarios.

### Detailed summary
- Updated the condition to check for both `undefined` and empty string in parameter names.
- Added a comment for future handling of multiple unnamed parameters.
- Modified test cases to include checks for empty parameter names.
- Ensured that unnamed parameters can be accessed via the `params["*"]` syntax.

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

<!-- end pr-codex -->
2025-02-14 06:22:30 +00:00
Jonas DanielsandGitHub c8ef941d8e fetch gas sponsored chains from API instead of hardcoding (#6256) 2025-02-13 22:21:12 -08:00
gregfromstl 39a5b5c8a3 [Portal] Docs: Get Users Docs (#6255)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the user experience by adding a new feature to fetch users through an API and updating the API endpoint for wallet pregeneration.

### Detailed summary
- Added a new menu item `Fetch Users` in `sidebar.tsx`.
- Updated the API endpoint in `page.mdx` for wallet pregeneration.
- Introduced documentation for fetching users via REST API in `get-users/page.mdx`.
- Provided example curl command and response format for the new feature.

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

<!-- end pr-codex -->
2025-02-14 04:08:25 +00:00
MananTank 6f381d768c [TOOL-3378] Dashboard: Add Contract to Project UI updates (#6233)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on refactoring and improving the structure of the dashboard application by removing obsolete files, enhancing component functionality, and updating UI elements for better user experience.

### Detailed summary
- Deleted several unused files related to contract management.
- Updated `className` properties for better styling consistency.
- Added `accountAddress` prop to various components for enhanced functionality.
- Introduced new hooks and functions for managing project contracts.
- Improved error handling in contract import functionality.
- Enhanced UI components for better responsiveness and clarity.

> The following files were skipped due to too many changes: `apps/dashboard/src/components/contract-components/tables/cells.tsx`, `apps/dashboard/src/components/contract-components/contract-deploy-form/add-to-project-card.tsx`, `apps/dashboard/src/components/smart-wallets/AccountFactories/index.tsx`, `apps/dashboard/src/components/contract-components/contract-deploy-form/custom-contract.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/primary-dashboard-button.tsx`, `apps/dashboard/src/components/contract-components/tables/contract-table.tsx`

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

<!-- end pr-codex -->
2025-02-14 01:41:11 +00:00
MananTank 0c5a51c5bf [TOOL-3243] Dashboard: Add redirect for support old engine import link (#6254)
<!--

## 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 `Page` function in `page.tsx` to handle `searchParams`, allowing for an import URL to trigger a redirect when present.

### Detailed summary
- Added `searchParams` to the `Page` function's props.
- Used `Promise.all` to await both `params` and `searchParams`.
- Implemented a redirect to an import URL if `searchParams.importUrl` is provided.

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

<!-- end pr-codex -->
2025-02-13 23:45:52 +00:00
MananTank 90428b4574 [TOOL-3422] Dashboard: Remove unused assets, Add Delete script (#6253)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on the deletion of numerous unused asset files from the `apps/dashboard/public/assets` directory and introduces a script for managing the deletion of these files.

### Detailed summary
- Deleted multiple unused asset files from various directories in `apps/dashboard/public/assets`.
- Introduced `deleteUnusedAssets.ts` script to handle the deletion of specified files.
- Added functionality to log deleted files and handle errors when files are not found.

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

<!-- end pr-codex -->
2025-02-13 19:54:05 +00:00
MananTank e24547bed1 [NEB-95] Dashboard: Fix Nebula analytics crash, add missing use client directive (#6252) 2025-02-13 18:58:25 +00:00
MananTank 3ff8c3fbb2 [NEB-95] Dashboard: Nebula Analytics UI updates (#6248)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/yNOf1svJ8o3zjO7zQouZ/6a8f069c-904b-447b-8757-54dde4b13c84.png)

<!-- start pr-codex -->

## PR-Codex overview
This PR primarily focuses on enhancing the `ThirdwebBarChart` and `ThirdwebAreaChart` components by introducing a `header` prop for improved chart display, modifying chart aspect ratios, and refining random data generation for analytics. It also updates the `NebulaAnalyticsPage` structure.

### Detailed summary
- Added `header` prop to `ThirdwebBarChart` and `ThirdwebAreaChart` for titles and descriptions.
- Changed aspect ratio from `lg:aspect-[4.5]` to `lg:aspect-[3.5]` in `Analytics.tsx`.
- Updated import path for `NebulaAnalyticsPage`.
- Refined random data generation logic in `nebula-analytics-ui.stories.tsx`.
- Improved chart configurations in multiple analytics components.
- Introduced `AnalyticsChart` component for better organization of chart data and props.
- Adjusted styling and structure of various charts for consistency.

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

<!-- end pr-codex -->
2025-02-13 17:56:48 +00:00
MananTank 4bd03b06e7 [TOOL-3415] Dashboard: Fix Explore page header showing logged out state (#6250) 2025-02-13 15:12:38 +00:00
Jonas DanielsandGitHub b7158c2b54 add projectId parameter to pay analytics components (#6245) 2025-02-12 21:54:34 -08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
5f26883fbd Version Packages (#6247)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-12 19:38:31 -08:00
Joaquim VergesandGitHub 98b6198687 [SDK] fix: Prevent popup when using auth redirect mode (#6246) 2025-02-12 19:31:09 -08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
880148b793 Version Packages (#6241)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-12 14:34:11 -08:00
Joaquim VergesandGitHub ee1bc3eb07 [SDK] fix: show token + dollar values for wallet balances (#6240) 2025-02-12 14:20:16 -08:00
MananTank e9ab8cb770 [TOOL-3398] Dashboard: Fix DialogOverlay and DialogContent missing default shadcn z-index (#6239) 2025-02-12 21:08:03 +00:00
MananTank 9c33981b72 [TOOL-3396] Dashboard: Fix inputMode for OTP input, Use text instead of default numeric (#6238) 2025-02-12 11:43:10 +00:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
c645b42684 Version Packages (#6208)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-12 01:36:43 -08:00
Joaquim VergesandGitHub 7583074c06 Update page.mdx (#6235)
Signed-off-by: Joaquim Verges <[email protected]>
2025-02-12 01:30:39 -08:00
Joaquim VergesandGitHub fbafb65f5c [SDK] fix: Polish PayEmbed UI and balance display (#6237) 2025-02-12 01:30:20 -08:00
Joaquim VergesandGitHub 6b286c7e83 [SDK] fix: Disable amount editing for non-wallet funding modes (#6236) 2025-02-12 01:23:28 -08:00
joaquim-verges 159ffbfaee [SDK] feat: Add verifyClaim export and canClaim function (#6234)
Fixes TOOL-3381

<!--

## 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 exposing the `canClaim` extension function for ERC20, ERC721, and ERC1155 drops, allowing for better claim verification and improving the overall functionality of the drops system.

### Detailed summary
- Added `canClaim` function to `erc20`, `erc721`, and `erc1155` extensions.
- Updated tests to include `canClaim` functionality.
- Modified JSON ABI files to include `verifyClaim` function.
- Adjusted gas values in tests.
- Enhanced error handling in `extract-error.ts`.

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

<!-- end pr-codex -->
2025-02-12 07:56:01 +00:00
kumaryash90 0c1bb6ccae [Dashboard] Deploy config updates (#6159)
TOOL-3389

<!--

## 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 default fee configuration for contract deployments, updates the handling of platform fees, and improves the UI components related to fee management, ensuring that default values are used consistently throughout the application.

### Detailed summary
- Added `DEFAULT_FEE_RECIPIENT` and `DEFAULT_FEE_BPS` to `addresses.ts`.
- Introduced `hasDefaultFeeConfig` in `ContractSettingsPage`.
- Filtered out specific functions related to platform fees in `contract-function.tsx`.
- Integrated platform fee logic in `CustomContractForm`.
- Updated `PlatformFeeFieldset` to reflect marketplace conditions.

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

<!-- end pr-codex -->
2025-02-11 22:00:08 +00:00
Shubham ParkhiandGitHub dceb47de8b [SDK/Dashboard/Portal] Feature/Fix: fix typo in docs (#6229)
Signed-off-by: Shubham Parkhi <[email protected]>
2025-02-10 21:19:02 -08:00
Jonas DanielsandGitHub dfbdbd06e5 use supportPlan instead of billingPlan for support tickets (#6228) 2025-02-10 21:17:24 -08:00
66bbedbe3a [SDK] Feature: add treasure and treasure topaz (#6191)
Co-authored-by: Joaquim Verges <[email protected]>
2025-02-10 19:42:15 -08:00
Joaquim VergesandGitHub 2fbf105d15 [SDK] fix: Disable quote retries and show fiat values (#6210) 2025-02-10 19:41:42 -08:00
FirekeeperandGitHub afcaa91873 Docs remove legacy enc key arg (#6220) 2025-02-10 19:41:08 -08:00
Manan TankandGitHub 51f955352d [TOOL-3377] Portal: Remove v4 SDK links, setup missing redirects (#6222) 2025-02-10 19:40:48 -08:00
Joaquim VergesandGitHub 6b2a90ac89 [SDK] feat: Add session key update handling and expose utility (#6217) 2025-02-10 19:40:06 -08:00
MananTank cf53184bb1 [TOOL-3379] Dashboard: Add team selector in support form (#6227) 2025-02-10 22:21:18 +00:00
MananTank 815b9b1ce1 [NEB-92] Dashboard: Add Nebula Analytics dashboard (#6198)
<!-- start pr-codex -->

## PR-Codex overview
This PR primarily focuses on enhancing the `Nebula` analytics functionality by adding new components and improving data handling for analytics. It introduces new utilities for managing date ranges and search parameters, along with updates to the UI for better user interaction.

### Detailed summary
- Added `getNebulaAnalyticsRangeFromSearchParams` function in `utils.ts`.
- Updated `ProjectTabs` to remove `isOnNebulaWaitList` prop.
- Introduced `normalizeTime` function in `time.ts` for date normalization.
- Created `NebulaAnalyticsFilter` component for filtering analytics data.
- Implemented `NebulaAnalyticsPage` and `NebulaAnalyticDashboard` for displaying analytics.
- Enhanced `DateRangeSelector` with popover alignment options.
- Added `NebulaAnalyticsDashboardUI` for rendering analytics data and charts.
- Integrated `responsive-rsc` for responsive search parameters.
- Updated `package.json` to include `responsive-rsc` dependency.
- Removed calls to `getTeamNebulaWaitList` in `layout.tsx` and associated logic.

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

<!-- end pr-codex -->
2025-02-10 20:05:31 +00:00
Maxim EvtushandGitHub ae1baa5105 fix: typos in documentation files (#6215)
Signed-off-by: Maxim Evtush <[email protected]>
2025-02-10 10:40:32 -08:00
saminacodes 92266b7459 [Portal] Cosmetic Changes (#6218)
CORE-0000

---

## PR-Codex overview
This PR focuses on updating content and structure across various pages in the `apps/portal` application, including the addition of new features, improved descriptions, and adjustments to the sidebar navigation.

### Detailed summary
- Deleted `supported-chains.png` and `context-filters.png`.
- Added new content for `faqs`, `troubleshoot`, and `Nebula` guides.
- Updated the sidebar with new links and icons.
- Changed import paths for images.
- Enhanced descriptions in `Deploy`, `Contracts`, and `Insight` pages.
- Added a new section for "Supported Chains" in `Contracts` and `Insight` pages.

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

<!-- end pr-codex -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the documentation and content for the `thirdweb` platform, particularly for the `Insight` and `Nebula` sections, while also restructuring some sidebar links and enhancing the user interface with new images.

### Detailed summary
- Deleted `supported-chains` image and `page.mdx` files.
- Added new content for `Insight FAQs` and `Troubleshoot Guide`.
- Updated `Nebula` description and added a support link.
- Changed sidebar items for `Insight` and `Contracts`.
- Introduced new features and layout in `Insight` and `Contracts` pages.
- Added support for displaying `Supported Chains` images in relevant sections.

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

<!-- end pr-codex -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updates to the `insight` and `nebula` sections of the application, including the addition of new content, restructuring of sidebar navigation, and changes to image imports. It also enhances the user interface with improved documentation and guidance.

### Detailed summary
- Deleted `apps/portal/src/app/insight/supported-chains/page.mdx`.
- Added new content for `Insight FAQs`, `Insight Troubleshoot Guide`, and `Nebula Troubleshoot Guide`.
- Updated sidebar navigation in `apps/portal/src/app/insight/sidebar.tsx`.
- Changed the name and href for several sidebar items.
- Reorganized `Supported Chains` references and added a new image import in multiple files.
- Enhanced content descriptions in `apps/portal/src/app/contracts/deploy/overview/page.mdx`.
- Added new features and video tutorials in `apps/portal/src/app/insight/page.mdx`.
- Updated layout and structure of `Contracts` section in `apps/portal/src/app/contracts/page.mdx`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`
2025-02-10 17:19:27 +00:00
MananTank 80ad84b803 [TOOL-3376] Dashboard: Fix 404 on publish contract page (#6221) 2025-02-10 17:01:59 +00:00
saminacodes 633f0cadf1 [Portal] Nebula documentation update (#6201)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the documentation for `thirdweb Nebula`, adding detailed metadata, explanations of new features, and clarifying existing concepts. It includes updates to various pages, improving user understanding of plugins, sessions, context filters, and response handling.

### Detailed summary
- Added `metadata` exports to multiple `.mdx` files.
- Enhanced FAQ entries with links and additional context.
- Updated `sidebar` to include new key concepts.
- Added sections on `Chat & Execute endpoints`, `Execute Configuration`, and `Context Filters`.
- Introduced new content on `Response Handling` and streaming vs non-streaming responses.
- Clarified `Sessions` management and benefits.
- Replaced `Stack` with `Grid` in the `page.mdx` and updated links.
- Improved descriptions and examples for API usage and configurations.

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

<!-- end pr-codex -->
2025-02-10 16:00:45 +00:00
Phillip HoandGitHub 91b0367b23 [service-utils] fix auth cache for cf workers (#6219) 2025-02-10 23:51:58 +08:00
MananTank c86e13b522 [TOOL-3366] Dashboard: Contract Deployment UI improvements, Reduce Chakra Usage (#6211)
<!--

## 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 refining the UI components and improving the overall structure of the contract deployment forms. It introduces new styles, modifies existing components, and enhances accessibility.

### Detailed summary
- Updated `ThemeProvider` to enable color scheme.
- Changed class names for various components to improve styling.
- Simplified props for `SolidityInput` components.
- Replaced `FormControl` with `FormFieldSetup` for better structure.
- Enhanced accessibility by modifying error messages and labels.
- Streamlined imports and removed unnecessary components.
- Improved the layout and styling of various fieldsets and buttons.
- Added new utility functions for better handling of form states.

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

<!-- end pr-codex -->
2025-02-09 21:35:45 +05:30
MananTank bc42c2fdfb [TOOL-3368] Dashboard: Improve page load perf of explore page and other few others (#6214)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on updating the contract fetching logic in the `dashboard` application to improve the retrieval of the latest published contracts. It replaces older functions with new ones that specifically fetch the latest contract versions.

### Detailed summary
- Added `export const dynamic = "force-static";` to `page.tsx` files.
- Replaced `getPublishedContractsWithPublisherMapping` with `getLatestPublishedContractsWithPublisherMapping` in multiple files.
- Introduced `getLatestPublishedContractsWithPublisherMapping` function to fetch the latest contract version.
- Updated `fetchPublishedContractVersions` to use `getSortedPublishedContractVersions`.
- Created `fetchLatestPublishedContractVersion` for fetching the latest version details.
- Modified `ContractCard` to use `ClientOnly` for rendering the `ContractPublisher`.

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

<!-- end pr-codex -->
2025-02-09 15:33:14 +00:00
Phillip HoandGitHub cad9c74bef [service-utils] Omit team_id for client usageV2 events (#6213) 2025-02-09 17:59:56 +08:00
Phillip HoandGitHub 6f2f92ddea [service-utils]H elper to call client usageV2 reporting endpoint (#6212) 2025-02-09 17:26:08 +08:00
Joaquim VergesandGitHub 97c9ab7e1c [SDK] feat: Batch approvals and swaps for smart wallets (#6209) 2025-02-08 03:04:11 -08:00
d4mr b73fa4b16f [dashboard-engine] updates for circle and wallet credentials (#6202)
closes TOOL-3351

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces support for managing Circle wallet credentials in the thirdweb Engine, enhancing the wallet management features by allowing users to create, update, and configure Circle wallet credentials.

### Detailed summary
- Added `Wallet Credentials` section in the sidebar.
- Introduced `Circle` wallet option in backend wallet configurations.
- Created new components for managing Circle wallet credentials.
- Implemented credential creation and update functionalities.
- Updated UI components to support Circle wallet management.
- Enhanced documentation for Circle wallet setup and usage.

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

<!-- end pr-codex -->
2025-02-08 09:45:46 +00:00
Phillip HoandGitHub cbfe9ec459 [service-utils] Allow sending client-side usageV2 events (#6207) 2025-02-08 13:07:57 +08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
fecd3a6c16 Version Packages (#6205)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-07 20:07:21 -08:00
Phillip HoandGitHub 8aeb4d6308 chore: Export sources array (#6206) 2025-02-08 12:00:43 +08:00
Joaquim VergesandGitHub f1cd2539d1 [SDK] chore: Update coinbase/wallet-sdk (#6204) 2025-02-07 19:31:39 -08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
a322e16853 Version Packages (#6184)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-07 16:10:52 -08:00
0xFirekeeper e50d77d28f Docs up to .NET 2.17.1 / Unity 5.17.1 (#6200)
Closes TOOL-2991
<!--

## 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 enhancements to the `sidebar` structure and API documentation, adds new wallet authentication options, and refines existing content for clarity. It also includes new RPC overrides and updates to the `InAppWallet` and `EcosystemWallet` functionalities.

### Detailed summary
- Added `rpcOverrides` example to `page.mdx`.
- Updated `sidebar.tsx` to include "API Reference" and "Blockchain API" sections.
- Replaced "Blockchain API" with "Nebula AI" in `sidebar.tsx`.
- Enhanced `EcosystemWallet` and `InAppWallet` functionalities with new login options.
- Clarified documentation on wallet connections and authentication methods.
- Added examples for `SiweExternal` login process.
- Updated various sections in `page.mdx` for better clarity and detail.

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

<!-- end pr-codex -->
2025-02-07 21:06:12 +00:00
MananTank 7f3b91da37 [TOOL-3346] Portal: Add Nebula icon support in dynamic OG image generation (#6199) 2025-02-07 21:02:35 +00:00
MananTank 41d50443d9 [TOOL-3328] Dashboard: Show error message in toast on Email submission fail in onboaridng flow (#6186)
<!-- start pr-codex -->

## PR-Codex overview
This PR enhances error handling in the `AccountForm` component by improving the way errors are processed and displayed to the user.

### Detailed summary
- Replaced the `onError` parameter's type from `Error` to a more generic `error`.
- Added console logging for errors.
- Implemented specific error handling for duplicate email and invalid email address cases.
- Integrated `toast` notifications for user-friendly error messages.

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

<!-- end pr-codex -->
2025-02-07 20:40:27 +00:00
catalyst17andcatalyst17 a8ac98bd49 feat: allowedToCreateChains in TeamResponse (#6172)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on adding the `canCreatePublicChains` property to the `mocks.ts` and modifying its type in `api.ts` to enhance the API's capabilities regarding public chain creation.

### Detailed summary
- Added `canCreatePublicChains: false` to the object in `mocks.ts`.
- Updated the type of `canCreatePublicChains` in `api.ts` to `boolean | null`.

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

<!-- end pr-codex -->
2025-02-07 11:52:05 -08:00
YashandGitHub 0781da9dca [SDK] Check bytecode prefix (#6196) 2025-02-07 09:52:00 -08:00
MananTank c5dfad8dbb [TOOL-3298] Playground: Add Insight playground (#6185)
<!-- start pr-codex -->

## PR-Codex overview
This PR introduces several enhancements and new features to the `playground-web` application, including a new `Spinner` component, improved layout for the `APIHeader`, and various updates to forms, UI components, and hooks for better functionality and usability.

### Detailed summary
- Added `isProd` export in `env.ts`.
- Introduced `Spinner` component in `Spinner.tsx` with CSS animations.
- Updated `Layout` to include `APIHeader` with title and links.
- Enhanced `RenderCode` to support new scrollable container class.
- Added `Anchor` component with improved link handling.
- Modified `CodeClient` to accept new props for scrollable container.
- Implemented `useShowMore` hook for dynamic item display.
- Updated `tailwind.config.ts` for new color schemes.
- Refactored `AppSidebar` to accept dynamic links.
- Introduced `fetchAllBlueprints` function for sidebar link generation.
- Added `Alert`, `Tooltip`, and `Table` components with improved styling.
- Enhanced `SelectWithSearch` for better option handling and search functionality.
- Updated various components to use client-side rendering (`"use client"`).
- Improved error handling and loading states in image components.
- Added new utility functions for better link and chain handling.

> The following files were skipped due to too many changes: `apps/playground-web/src/components/ui/select.tsx`, `apps/playground-web/src/app/insight/[blueprint_slug]/blueprint-playground.client.tsx`, `pnpm-lock.yaml`

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

<!-- end pr-codex -->
2025-02-07 15:29:21 +00:00
Joaquim VergesandGitHub 9663079062 [SDK] feat: Add session keys to smart wallet options (#6194) 2025-02-07 04:37:28 -08:00
Joaquim VergesandGitHub 1f6bb7c329 [SDK] feat: Add fiat value display to buy token input (#6193) 2025-02-07 04:19:24 -08:00
Joaquim VergesandGitHub 30e13e6b91 [SDK] feat: Add editable token amount input to buy screen (#6192) 2025-02-07 03:54:22 -08:00
Joaquim VergesandGitHub 2dfc245d44 [SDK] feat: Add provider selection to fiat onramp (#6190) 2025-02-07 01:41:47 -08:00
jnsdls 64d7bf358f prefer explicit clientId over secretKey computation (#6188)
TOOL-0000

### TL;DR

Prioritize explicitly passed `clientId` over computed values when creating a Thirdweb client.

### What changed?

Modified the client initialization logic to prefer explicitly passed `clientId` parameters over computing them from `secretKey`. Only falls back to computing the `clientId` from `secretKey` when no explicit `clientId` is provided.

### How to test?

1. Create a Thirdweb client with both `clientId` and `secretKey` parameters
2. Verify that the provided `clientId` is used instead of computing one from the `secretKey`
3. Create a client with only `secretKey` and verify the `clientId` is computed correctly

### Why make this change?

To give developers more control over client configuration by respecting explicitly provided parameters rather than always computing values from the secret key. This allows for more flexible client setup scenarios where the computed `clientId` might not be desired.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the behavior of the `createThirdwebClient()` function to prioritize the explicitly provided `clientId` over deriving it from the `secretKey`. It also modifies the corresponding test to reflect this new behavior.

### Detailed summary
- Updated `createThirdwebClient()` to prefer `clientId` over computed value from `secretKey`.
- Changed test description to indicate `clientId` is not ignored when `secretKey` is provided.
- Updated test expectation to check that `clientId` is equal to the explicitly provided value instead of the computed one.

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

<!-- end pr-codex -->
2025-02-07 08:28:29 +00:00
jnsdls a6e9c97c6f add redirect from /connect/pay to /connect/universal-bridge (#6189)
fixes: TOOL-0000
2025-02-07 08:07:35 +00:00
Joaquim VergesandGitHub ee57ded902 [SDK] fix: Requery allowances on quote screen return (#6187) 2025-02-06 21:00:01 -08:00
MananTank 6d3947ab0c [TOOL-3314] Playground: Fix Next.js errors in Headless UI example pages (#6175)
Fixed "Can not import server (async) components in client components" error

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces new preview components for wallet, NFT, token, and account functionalities in the `playground-web` application. It enhances code modularity by separating previews into distinct files, improving readability and maintainability.

### Detailed summary
- Added `wallet-previews.tsx` with previews for `WalletIcon`, `WalletName`, and formatting options.
- Created `nft-previews.tsx` with previews for `NFTMedia`, `NFTName`, and `NFTDescription`.
- Introduced `token-previews.tsx` with previews for `TokenIcon`, `TokenName`, and `TokenSymbol`.
- Developed `account-previews.tsx` for `AccountAddress`, `AccountName`, and `AccountBalance` with various configurations.
- Updated example files (`wallet-examples.tsx`, `nft-examples.tsx`, `token-examples.tsx`, `account-examples.tsx`) to utilize new preview components, enhancing code clarity.

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

<!-- end pr-codex -->
2025-02-06 20:10:27 +00:00
MananTank 632f1d138f [TOOL-3299] Playground: Update Sidebar, Minor improvements and fixes (#6174) 2025-02-06 20:10:26 +00:00
Phillip HoandGitHub 18f0e575c4 [service-utils] Update list of usageV2 sources (#6183) 2025-02-06 17:39:33 +08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
e732b11e45 Version Packages (#6166)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-06 00:46:47 -08:00
Joaquim VergesandGitHub f77165e2d1 [SDK] fix: Skip swap approvals and use local gas prices (#6182) 2025-02-06 00:36:31 -08:00
Phillip HoandGitHub dfd64d2896 [service-utils] Expose more kafka configs, set max in-flight requests (#6181) 2025-02-06 11:55:23 +08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4e703fdb5d build(deps-dev): bump vitest from 3.0.4 to 3.0.5 in the npm_and_yarn group (#6167)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 14:07:21 -08:00
Aaron RamirezandGitHub af88935680 [Portal Documentation] Fix: Typo Fix (#6156)
Signed-off-by: Aaron Ramirez <[email protected]>
2025-02-05 14:06:09 -08:00
gregandGitHub c50c82aacd fix: remove pay framer page (#6178) 2025-02-05 14:02:23 -08:00
MananTank 0fa8354dec [TOOL-3318] Dashboard: Add Upload Account Avatar feature in account settings page (#6179) 2025-02-05 20:44:06 +00:00
gregfromstl 769a927627 [Dashboard] Feature: add universal-bridge framer page (#6176)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on modifying the routes in the `framer-rewrites.js` file for the dashboard application, specifically adding and updating certain paths.

### Detailed summary
- Added the route `"/connect/universal-bridge"`.
- Updated the existing route list by retaining `"/connect/pay"`, `"/connect/account-abstraction"`, and `"/nebula"`.
- Removed the comment indicating `// --insight`.

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

<!-- end pr-codex -->
2025-02-05 18:53:54 +00:00
saminacodes ceff873cba [Portal] Add video and update prompt guide (#6163)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the `Nebula` documentation by adding new video tutorials and reorganizing content related to supported chains. It also expands the action descriptions in the `prompt-guide` to include bridging and swapping currencies.

### Detailed summary
- Added `Grid` and `ArticleCard` components to introduce video tutorials on using `Nebula`.
- Moved the section on supported chains to follow the new video tutorials.
- Expanded action descriptions in the `prompt-guide` to include "Bridge & Swap" functionality.

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

<!-- end pr-codex -->
2025-02-05 18:44:49 +00:00
arcoraven 9a432af1cf fix: Tweak 404 page (#6173)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates the content of the `NotFoundPage` component to enhance the user experience by changing the messaging displayed when a page is not found.

### Detailed summary
- Changed the message from "Looks like web3 can't be found here." to "Looks like you're lost!".
- Updated the text from "Try our" to "Find live content on our".
- Removed "instead" and added a period after the `TrackedLinkTW` component.

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

<!-- end pr-codex -->
2025-02-05 14:35:21 +00:00
Phillip HoandGitHub ee6bd4f898 [service-utils] fix: export kafka producer (#6171) 2025-02-05 14:03:50 +08:00
Phillip HoandGitHub ccf6329810 [service-utils] Add kafka producer to emit raw messages (#6170) 2025-02-05 12:36:46 +08:00
Phillip HoandGitHub 821caa63d7 fix: Fix usageV2 imports, allow sdk source (#6164) 2025-02-05 11:31:22 +08:00
kumaryash90 09e396d856 [SDK] Update implementations (#6162)
TOOL-3283

<!--

## 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 `MarketplaceV3` address in the `zksync` implementation file, likely reflecting a new deployment or contract version.

### Detailed summary
- Added a new `MarketplaceV3` address: `0x4027561E163a420c4e5Db46E07EBd581CAa8Bb62` at line 2741.
- Removed the previous `MarketplaceV3` address: `0x9EB0830B0b10010F2a53383517A7D0B75531Bb1b`.

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

<!-- end pr-codex -->
2025-02-05 01:26:13 +00:00
jnsdls 9573b2f9d2 move account abstraction page to connect directory (#6161)
closes: TOOL-3311
2025-02-04 22:39:11 +00:00
MananTank b004a07e5d [NEB-76] Dashboard: Add Nebula service in chainlist, Update all product icons, links (#6158)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new `Nebula` product to the application, updates several links to point directly to the `thirdweb` site, and refines various UI components for better styling and functionality.

### Detailed summary
- Added `nebula` to the `products` array in `chain.ts`.
- Updated links in `products` to `thirdweb.com` for several items.
- Added `className` to `<TableHeader>` for styling.
- Refined styling and classes in several components, including `<Star>` and `<ProductIcon>`.
- Removed unused functions related to link generation.
- Updated SVG icons to improve accessibility and presentation.

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

<!-- end pr-codex -->
2025-02-04 22:08:01 +00:00
MananTank 2396b4e461 [NEB-87] Dashboard, Docs - Use updated Nebula API (#6157)
<!--

## 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 API's context handling by replacing the `context_filter` with a simplified `context` structure, changing types, and cleaning up related components to improve clarity and functionality in the codebase.

### Detailed summary
- Changed `description` type from `string` to `React.ReactNode` in `APIParameter`.
- Replaced `nebulaContextFilterPathParameter` and `nebulaExecuteConfigPathParameter` with `nebulaContextParameter`.
- Updated JSON structures to use `context` instead of `context_filter`.
- Modified related API response types and request parameters to align with the new context structure.
- Refactored components to use `NebulaContext` instead of `ContextFilters`.
- Adjusted context handling in various components and functions for consistency.

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

<!-- end pr-codex -->
2025-02-04 21:59:22 +00:00
737 changed files with 24227 additions and 12735 deletions
+7 -1
View File
@@ -10,7 +10,13 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["playground-web", "thirdweb-dashboard", "wallet-ui", "portal"],
"ignore": [
"playground-web",
"thirdweb-dashboard",
"wallet-ui",
"portal",
"thirdweb-login"
],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"updateInternalDependents": "always",
"onlyUpdatePeerDependentsWhenOutOfRange": true
+4 -3
View File
@@ -6,8 +6,6 @@ To get started, read the [How this repo works](#how-this-repo-works) section bel
From there, you can take a look at our [Good First Issues](https://github.com/thirdweb-dev/js/labels/good%20first%20issue) board and find an issue that interests you!
If you have any questions about the issue, feel free to ask on our [Discord server](https://discord.gg/thirdweb) in the `#contributors` channel; where you'll be able to get help from our team and other contributors.
<br />
## How this repo works
@@ -85,7 +83,10 @@ If your test depends on a downstream network call, you must mock the call using
import { setupServer } from "msw/node";
import { downloadMock, uploadMock } from "../../../test/src/mocks/storage.js";
const server = setupServer(uploadMock("HASH"), downloadMock({ name: "Test NFT" }));
const server = setupServer(
uploadMock("HASH"),
downloadMock({ name: "Test NFT" })
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
+3 -6
View File
@@ -1,7 +1,7 @@
<p align="center">
<br />
<a href="https://thirdweb.com">
<img src="https://github.com/thirdweb-dev/js/blob/main/legacy_packages/sdk/logo.svg?raw=true" width="200" alt=""/></a>
<img src="https://thirdweb.com/brand/thirdweb-icon.svg" width="200" alt=""/></a>
<br />
</p>
@@ -11,9 +11,6 @@
<a href="https://github.com/thirdweb-dev/js/actions/workflows/CI.yml">
<img alt="Build Status" src="https://github.com/thirdweb-dev/js/actions/workflows/CI.yml/badge.svg"/>
</a>
<a href="https://discord.gg/thirdweb">
<img alt="Join our Discord!" src="https://img.shields.io/discord/834227967404146718.svg?color=7289da&label=discord&logo=discord&style=flat"/>
</a>
</p>
<p align="center"><strong>All-in-one web3 SDK for Browser, Node and Mobile apps</strong></p>
@@ -31,12 +28,12 @@
## Library Comparison
| | thirdweb | Wagmi + Viem | Ethers@6 |
| | thirdweb | Wagmi + Viem | Ethers@6 |
| ----------------------------------------- | -------- | ------------------ | -------- |
| Type safe contract API | ✅ | ✅ | ✅ |
| Type safe wallet API | ✅ | ✅ | ✅ |
| EVM utils | ✅ | ✅ | ✅ |
| RPC for any EVM | ✅  | ⚠️ public RPC only | ❌ |
| RPC for any EVM | ✅  | ⚠️ public RPC only | ❌ |
| Automatic ABI Resolution | ✅ | ❌ | ❌ |
| IPFS Upload/Download | ✅ | ❌ | ❌ |
| Embedded wallet (email/ social login) | ✅ | ⚠️ via 3rd party | ❌ |
+6 -2
View File
@@ -6,10 +6,14 @@
# not required to build, defaults to prod
NEXT_PUBLIC_THIRDWEB_DOMAIN="localhost:3000"
# API host. For local development, please use "https://api.thirdweb-preview.com"
# API host. For local development, please use "https://api.thirdweb-dev.com"
# otherwise: "https://api.thirdweb.com"
NEXT_PUBLIC_THIRDWEB_API_HOST="https://api.thirdweb-dev.com"
# Bridge API. For local development, please use "https://bridge.thirdweb-dev.com"
# otherwise: "https://bridge.thirdweb.com"
NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST="https://bridge.thirdweb-dev.com"
# Paper API host
NEXT_PUBLIC_THIRDWEB_EWS_API_HOST="https://ews.thirdweb-dev.com"
@@ -97,4 +101,4 @@ REDIS_URL=""
ANALYTICS_SERVICE_URL=""
# Required for Nebula Chat
NEXT_PUBLIC_NEBULA_URL=""
NEXT_PUBLIC_NEBULA_URL=""
+2 -1
View File
@@ -9,7 +9,8 @@ module.exports = [
// -- connect
"/connect",
"/connect/sign-in",
"/connect/pay",
"/connect/account-abstraction",
"/connect/universal-bridge",
// -- nebula
"/nebula",
// --insight
+2 -1
View File
@@ -72,7 +72,7 @@
"next-plausible": "^3.12.4",
"next-themes": "^0.4.4",
"nextjs-toploader": "^1.6.12",
"openapi-types": "^12.1.3",
"nuqs": "^2.4.0",
"p-limit": "^6.2.0",
"papaparse": "^5.5.2",
"pluralize": "^8.0.0",
@@ -89,6 +89,7 @@
"react-table": "^7.8.0",
"recharts": "2.15.1",
"remark-gfm": "^4.0.0",
"responsive-rsc": "0.0.7",
"server-only": "^0.0.1",
"shiki": "1.27.0",
"sonner": "^1.7.4",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 956 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 709 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 906 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 733 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

+12
View File
@@ -331,6 +331,18 @@ async function redirects() {
destination: "/templates/:slug",
permanent: false,
},
// redirect /account-abstraction to /connect/account-abstraction
{
source: "/account-abstraction",
destination: "/connect/account-abstraction",
permanent: false,
},
// redirect /connect/pay to /connect/universal-bridge
{
source: "/connect/pay",
destination: "/connect/universal-bridge",
permanent: false,
},
// PREVIOUS CAMPAIGNS
{
source: "/unlimited-wallets",
@@ -0,0 +1,28 @@
import { promises as fs } from "node:fs";
import path from "node:path";
// after running logUnusedAssets.ts,
// 1. validate the output
// 2. If you think that a file should not be deleted, remove it from the output
// 3. paste the output files in `filesToDelete` and run this script to delete them
const filesToDelete: string[] = [];
async function deleteFiles() {
for (const filePath of filesToDelete) {
try {
const fullPath = path.join(process.cwd(), filePath);
await fs.unlink(fullPath);
console.log(`Deleted: ${filePath}`);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
console.warn(`File not found: ${filePath}`);
} else {
console.error(`Error deleting ${filePath}:`, error);
}
}
}
console.log("--- DONE ---");
}
deleteFiles();
@@ -26,9 +26,6 @@ const filesToIgnore = new Set([
// macOS stuff
"public/assets/product-pages/.DS_Store",
"public/assets/.DS_Store",
// pdfs
"public/Thirdweb_Terms_of_Service.pdf",
"public/thirdweb_Privacy_Policy_May_2022.pdf",
]);
function getAllFilesInFolder(folderPath: string) {
@@ -0,0 +1,53 @@
"use server";
import { getAuthToken } from "../../app/api/lib/getAuthToken";
import { API_SERVER_URL } from "../constants/env";
export async function acceptInvite(options: {
teamId: string;
inviteId: string;
}) {
const token = await getAuthToken();
if (!token) {
return {
ok: false,
errorMessage: "You are not authorized to perform this action",
};
}
const res = await fetch(
`${API_SERVER_URL}/v1/teams/${options.teamId}/invites/${options.inviteId}/accept`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({}),
},
);
if (!res.ok) {
let errorMessage = "Failed to accept invite";
try {
const result = (await res.json()) as {
error: {
code: string;
message: string;
statusCode: number;
};
};
errorMessage = result.error.message;
} catch {}
return {
ok: false,
errorMessage,
};
}
return {
ok: true,
};
}
-9
View File
@@ -71,15 +71,6 @@ async function proxy<T extends object>(
};
}
export async function analyticsServerProxy<T extends object = object>(
params: ProxyActionParams,
) {
return proxy<T>(
process.env.ANALYTICS_SERVICE_URL || "https://analytics.thirdweb.com",
params,
);
}
export async function apiServerProxy<T extends object = object>(
params: ProxyActionParams,
) {
@@ -0,0 +1,68 @@
"use server";
import { getAuthToken } from "../../app/api/lib/getAuthToken";
import { API_SERVER_URL } from "../constants/env";
export async function sendTeamInvites(options: {
teamId: string;
invites: Array<{ email: string; role: "OWNER" | "MEMBER" }>;
}): Promise<
| {
ok: true;
results: Array<"fulfilled" | "rejected">;
}
| {
ok: false;
errorMessage: string;
}
> {
const token = await getAuthToken();
if (!token) {
return {
ok: false,
errorMessage: "You are not authorized to perform this action",
};
}
const results = await Promise.allSettled(
options.invites.map((invite) => sendInvite(options.teamId, invite, token)),
);
return {
ok: true,
results: results.map((x) => x.status),
};
}
async function sendInvite(
teamId: string,
invite: { email: string; role: "OWNER" | "MEMBER" },
token: string,
) {
const res = await fetch(`${API_SERVER_URL}/v1/teams/${teamId}/invites`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
inviteEmail: invite.email,
inviteRole: invite.role,
}),
});
if (!res.ok) {
const errorMessage = await res.text();
return {
email: invite.email,
ok: false,
errorMessage,
};
}
return {
email: invite.email,
ok: true,
};
}
@@ -5,6 +5,7 @@ import { API_SERVER_URL } from "../constants/env";
export async function updateAccount(values: {
name?: string;
email?: string;
image?: string | null;
}) {
const token = await getAuthToken();
+220 -29
View File
@@ -1,6 +1,8 @@
import { fetchAnalytics } from "data/analytics/fetch-analytics";
import "server-only";
import type {
AnalyticsQueryParams,
EcosystemWalletStats,
InAppWalletStats,
RpcMethodStats,
TransactionStats,
@@ -8,15 +10,67 @@ import type {
WalletStats,
WalletUserStats,
} from "types/analytics";
import { getAuthToken } from "../../app/api/lib/getAuthToken";
import { getChains } from "./chain";
async function fetchAnalytics(
input: string | URL,
init?: RequestInit,
): Promise<Response> {
const token = await getAuthToken();
if (!token) {
throw new Error("You are not authorized to perform this action");
}
const [pathname, searchParams] = input.toString().split("?");
if (!pathname) {
throw new Error("Invalid input, no pathname provided");
}
// create a new URL object for the analytics server
const ANALYTICS_SERVICE_URL = new URL(
process.env.ANALYTICS_SERVICE_URL || "https://analytics.thirdweb.com",
);
ANALYTICS_SERVICE_URL.pathname = pathname;
for (const param of searchParams?.split("&") || []) {
const [key, value] = param.split("=");
if (!key || !value) {
throw new Error("Invalid input, no key or value provided");
}
ANALYTICS_SERVICE_URL.searchParams.append(
decodeURIComponent(key),
decodeURIComponent(value),
);
}
// client id DEBUG OVERRIDE
// ANALYTICS_SERVICE_URL.searchParams.delete("clientId");
// ANALYTICS_SERVICE_URL.searchParams.delete("accountId");
// ANALYTICS_SERVICE_URL.searchParams.append(
// "clientId",
// "...",
// );
return fetch(ANALYTICS_SERVICE_URL, {
...init,
headers: {
"content-type": "application/json",
...init?.headers,
authorization: `Bearer ${token}`,
},
});
}
function buildSearchParams(params: AnalyticsQueryParams): URLSearchParams {
const searchParams = new URLSearchParams();
if (params.clientId) {
searchParams.append("clientId", params.clientId);
}
if (params.accountId) {
searchParams.append("accountId", params.accountId);
searchParams.append("teamId", params.teamId);
if (params.projectId) {
searchParams.append("projectId", params.projectId);
}
// shared params
if (params.from) {
searchParams.append("from", params.from.toISOString());
}
@@ -33,13 +87,19 @@ export async function getWalletConnections(
params: AnalyticsQueryParams,
): Promise<WalletStats[]> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(`v1/wallets?${searchParams.toString()}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
const res = await fetchAnalytics(
`v2/sdk/wallet-connects?${searchParams.toString()}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
if (res?.status !== 200) {
console.error("Failed to fetch wallet connections");
const reason = await res?.text();
console.error(
`Failed to fetch wallet connections, ${res?.status} - ${res.statusText} - ${reason}`,
);
return [];
}
@@ -52,7 +112,7 @@ export async function getInAppWalletUsage(
): Promise<InAppWalletStats[]> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v1/wallets/in-app?${searchParams.toString()}`,
`v2/wallet/connects?${searchParams.toString()}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
@@ -60,7 +120,10 @@ export async function getInAppWalletUsage(
);
if (res?.status !== 200) {
console.error("Failed to fetch in-app wallet usage");
const reason = await res?.text();
console.error(
`Failed to fetch in-app wallet usage, ${res?.status} - ${res.statusText} - ${reason}`,
);
return [];
}
@@ -72,10 +135,13 @@ export async function getUserOpUsage(
params: AnalyticsQueryParams,
): Promise<UserOpStats[]> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(`v1/user-ops?${searchParams.toString()}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
const res = await fetchAnalytics(
`v2/bundler/usage?${searchParams.toString()}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
if (res?.status !== 200) {
const reason = await res?.text();
@@ -89,12 +155,46 @@ export async function getUserOpUsage(
return json.data as UserOpStats[];
}
export async function getAggregateUserOpUsage(
params: Omit<AnalyticsQueryParams, "period">,
): Promise<UserOpStats> {
const [userOpStats, chains] = await Promise.all([
getUserOpUsage({ ...params, period: "all" }),
getChains(),
]);
// Aggregate stats across wallet types
return userOpStats.reduce(
(acc, curr) => {
// Skip testnets from the aggregated stats
if (curr.chainId) {
const chain = chains.data.find(
(c) => c.chainId.toString() === curr.chainId,
);
if (chain?.testnet) {
return acc;
}
}
acc.successful += curr.successful;
acc.failed += curr.failed;
acc.sponsoredUsd += curr.sponsoredUsd;
return acc;
},
{
date: (params.from || new Date()).toISOString(),
successful: 0,
failed: 0,
sponsoredUsd: 0,
},
);
}
export async function getClientTransactions(
params: AnalyticsQueryParams,
): Promise<TransactionStats[]> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v1/transactions/client?${searchParams.toString()}`,
`v2/sdk/contract-transactions?${searchParams.toString()}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
@@ -118,7 +218,7 @@ export async function getRpcMethodUsage(
): Promise<RpcMethodStats[]> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v1/rpc/evm-methods?${searchParams.toString()}`,
`v2/rpc/evm-methods?${searchParams.toString()}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
@@ -139,7 +239,7 @@ export async function getWalletUsers(
): Promise<WalletUserStats[]> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v1/wallets/users?${searchParams.toString()}`,
`v2/wallet-connects/users?${searchParams.toString()}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
@@ -147,7 +247,10 @@ export async function getWalletUsers(
);
if (res?.status !== 200) {
console.error("Failed to fetch wallet user stats");
const reason = await res?.text();
console.error(
`Failed to fetch wallet user stats: ${res?.status} - ${res.statusText} - ${reason}`,
);
return [];
}
@@ -155,20 +258,108 @@ export async function getWalletUsers(
return json.data as WalletUserStats[];
}
type ActiveStatus = {
bundler: boolean;
storage: boolean;
rpc: boolean;
nebula: boolean;
sdk: boolean;
insight: boolean;
pay: boolean;
inAppWallet: boolean;
ecosystemWallet: boolean;
};
export async function isProjectActive(
params: AnalyticsQueryParams,
): Promise<boolean> {
): Promise<ActiveStatus> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(`v1/active?${searchParams.toString()}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
const res = await fetchAnalytics(
`v2/active-usage?${searchParams.toString()}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
if (res?.status !== 200) {
console.error("Failed to fetch project active status");
return false;
const reason = await res?.text();
console.error(
`Failed to fetch project active status: ${res?.status} - ${res.statusText} - ${reason}`,
);
return {
bundler: false,
storage: false,
rpc: false,
nebula: false,
sdk: false,
insight: false,
pay: false,
inAppWallet: false,
ecosystemWallet: false,
} as ActiveStatus;
}
const json = await res.json();
return json.data.isActive as boolean;
return json.data as ActiveStatus;
}
export async function getEcosystemWalletUsage(args: {
teamId: string;
ecosystemSlug: string;
ecosystemPartnerId?: string;
projectId?: string;
from?: Date;
to?: Date;
period?: "day" | "week" | "month" | "year" | "all";
}) {
const {
ecosystemSlug,
ecosystemPartnerId,
teamId,
projectId,
from,
to,
period,
} = args;
const searchParams = new URLSearchParams();
// required params
searchParams.append("ecosystemSlug", ecosystemSlug);
searchParams.append("teamId", teamId);
// optional params
if (ecosystemPartnerId) {
searchParams.append("ecosystemPartnerId", ecosystemPartnerId);
}
if (projectId) {
searchParams.append("projectId", projectId);
}
if (from) {
searchParams.append("from", from.toISOString());
}
if (to) {
searchParams.append("to", to.toISOString());
}
if (period) {
searchParams.append("period", period);
}
const res = await fetchAnalytics(
`v2/wallets/connects/${ecosystemSlug}?${searchParams.toString()}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
},
);
if (res?.status !== 200) {
console.error("Failed to fetch ecosystem wallet stats");
return null;
}
const json = await res.json();
return json.data as EcosystemWalletStats[];
}
+54
View File
@@ -0,0 +1,54 @@
import "server-only";
import type { ChainMetadata } from "thirdweb/chains";
import type { ChainService } from "../../app/(dashboard)/(chain)/types/chain";
import { API_SERVER_URL, THIRDWEB_API_SECRET } from "../constants/env";
export async function getGasSponsoredChains() {
if (!THIRDWEB_API_SECRET) {
throw new Error("API_SERVER_SECRET is not set");
}
const res = await fetch(`${API_SERVER_URL}/v1/chains/gas-sponsored`, {
headers: {
"Content-Type": "application/json",
"x-service-api-key": THIRDWEB_API_SECRET,
},
next: {
revalidate: 15 * 60, //15 minutes
},
});
if (!res.ok) {
console.error(
"Failed to fetch gas sponsored chains",
res.status,
res.statusText,
);
res.body?.cancel();
return [];
}
try {
return (await res.json()).data as number[];
} catch (e) {
console.error("Failed to parse gas sponsored chains", e);
return [];
}
}
export function getChains() {
return fetch(
`${API_SERVER_URL}/v1/chains`,
// revalidate every 60 minutes
{ next: { revalidate: 60 * 60 } },
).then((res) => res.json()) as Promise<{ data: ChainMetadata[] }>;
}
export function getChainServices() {
return fetch(
`${API_SERVER_URL}/v1/chains/services`,
// revalidate every 60 minutes
{ next: { revalidate: 60 * 60 } },
).then((res) => res.json()) as Promise<{
data: Record<number, Array<ChainService>>;
}>;
}
@@ -0,0 +1,32 @@
import { getAuthToken } from "../../app/api/lib/getAuthToken";
import { API_SERVER_URL } from "../constants/env";
export type LinkedWallet = {
createdAt: string;
id: string;
walletAddress: string;
};
export async function getLinkedWallets() {
const token = await getAuthToken();
if (!token) {
return null;
}
const res = await fetch(`${API_SERVER_URL}/v1/account/wallets`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (res.ok) {
const json = (await res.json()) as {
data: LinkedWallet[];
};
return json.data;
}
return null;
}
+2 -16
View File
@@ -1,23 +1,9 @@
import "server-only";
import { API_SERVER_URL } from "@/constants/env";
import type { ProjectResponse } from "@thirdweb-dev/service-utils";
import { getAuthToken } from "../../app/api/lib/getAuthToken";
export type Project = {
id: string;
name: string;
createdAt: Date;
updatedAt: Date;
deletedAt: Date | null;
bannedAt: Date | null;
domains: string[];
bundleIds: string[];
redirectUrls: string[];
lastAccessedAt: Date | null;
slug: string;
teamId: string;
publishableKey: string;
// image: string; // TODO
};
export type Project = ProjectResponse;
export async function getProjects(teamSlug: string) {
const token = await getAuthToken();
+46
View File
@@ -0,0 +1,46 @@
import { getAuthToken } from "../../app/api/lib/getAuthToken";
import { API_SERVER_URL } from "../constants/env";
export type TeamInvite = {
id: string;
teamId: string;
email: string;
role: "OWNER" | "MEMBER";
createdAt: string;
status: "pending" | "accepted" | "expired";
expiresAt: string;
};
export async function getTeamInvites(
teamId: string,
options: {
count: number;
start: number;
},
) {
const authToken = await getAuthToken();
if (!authToken) {
throw new Error("Unauthorized");
}
const res = await fetch(
`${API_SERVER_URL}/v1/teams/${teamId}/invites?skip=${options.start}&take=${options.count}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
},
);
if (!res.ok) {
const errorMessage = await res.text();
throw new Error(errorMessage);
}
const json = (await res.json()) as {
result: TeamInvite[];
};
return json.result;
}
+28 -4
View File
@@ -12,15 +12,16 @@ export type TeamAccountRole =
export type TeamMember = {
account: {
creatorWalletAddress: string;
name: string;
email: string | null;
image: string | null;
};
} & {
deletedAt: Date | null;
deletedAt: string | null;
accountId: string;
teamId: string;
createdAt: Date;
updatedAt: Date;
createdAt: string;
updatedAt: string;
role: TeamAccountRole;
};
@@ -46,3 +47,26 @@ export async function getMembers(teamSlug: string) {
return undefined;
}
export async function getMemberById(teamSlug: string, memberId: string) {
const token = await getAuthToken();
if (!token) {
return undefined;
}
const res = await fetch(
`${API_SERVER_URL}/v1/teams/${teamSlug}/members/${memberId}`,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
if (res.ok) {
return (await res.json())?.result as TeamMember;
}
return undefined;
}
+21 -28
View File
@@ -1,34 +1,9 @@
import "server-only";
import { API_SERVER_URL } from "@/constants/env";
import { API_SERVER_URL, THIRDWEB_API_SECRET } from "@/constants/env";
import type { TeamResponse } from "@thirdweb-dev/service-utils";
import { getAuthToken } from "../../app/api/lib/getAuthToken";
type EnabledTeamScope =
| "pay"
| "storage"
| "rpc"
| "bundler"
| "insight"
| "embeddedWallets"
| "relayer"
| "chainsaw"
| "nebula";
export type Team = {
id: string;
name: string;
slug: string;
createdAt: string;
updatedAt: string;
deletedAt?: string;
bannedAt?: string;
image?: string;
billingPlan: "pro" | "growth" | "free" | "starter";
billingStatus: "validPayment" | (string & {}) | null;
billingEmail: string | null;
growthTrialEligible: false;
enabledScopes: EnabledTeamScope[];
};
export type Team = TeamResponse;
export async function getTeamBySlug(slug: string) {
const token = await getAuthToken();
@@ -47,6 +22,24 @@ export async function getTeamBySlug(slug: string) {
return null;
}
export async function service_getTeamBySlug(slug: string) {
const teamRes = await fetch(`${API_SERVER_URL}/v1/teams/${slug}`, {
headers: {
"x-service-api-key": THIRDWEB_API_SECRET,
},
});
if (teamRes.ok) {
return (await teamRes.json())?.result as Team;
}
return null;
}
export function getTeamById(id: string) {
return getTeamBySlug(id);
}
export async function getTeams() {
const token = await getAuthToken();
if (!token) {
@@ -28,7 +28,7 @@ export const Mobile: Story = {
function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
<BadgeContainer label="Base">
<DangerSettingCard
title="This is a title"
@@ -6,7 +6,6 @@ import {
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
@@ -17,13 +16,14 @@ export function DangerSettingCard(props: {
title: string;
className?: string;
footerClassName?: string;
description: string;
description: React.ReactNode;
buttonLabel: string;
buttonOnClick: () => void;
isDisabled?: boolean;
isPending: boolean;
confirmationDialog: {
title: string;
description: string;
description: React.ReactNode;
};
children?: React.ReactNode;
}) {
@@ -55,7 +55,7 @@ export function DangerSettingCard(props: {
<Button
variant="destructive"
className="gap-2 bg-red-600 font-semibold text-white hover:bg-red-600/80"
disabled={props.isPending}
disabled={props.isDisabled || props.isPending}
>
{props.isPending && <Spinner className="size-3" />}
{props.buttonLabel}
@@ -63,20 +63,22 @@ export function DangerSettingCard(props: {
</DialogTrigger>
<DialogContent
className="z-[10001]"
className="z-[10001] overflow-hidden p-0"
dialogOverlayClassName="z-[10000]"
>
<DialogHeader className="pr-10">
<DialogTitle className="leading-snug">
{props.confirmationDialog.title}
</DialogTitle>
<div className="p-6">
<DialogHeader className="pr-10">
<DialogTitle className="leading-snug">
{props.confirmationDialog.title}
</DialogTitle>
<DialogDescription>
{props.confirmationDialog.description}
</DialogDescription>
</DialogHeader>
<DialogDescription>
{props.confirmationDialog.description}
</DialogDescription>
</DialogHeader>
</div>
<DialogFooter className="mt-4 gap-4 lg:gap-2">
<div className="flex justify-end gap-4 border-t bg-card p-6 lg:gap-2">
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
@@ -90,7 +92,7 @@ export function DangerSettingCard(props: {
{props.isPending && <Spinner className="size-3" />}
{props.buttonLabel}
</Button>
</DialogFooter>
</div>
</DialogContent>
</Dialog>
</div>
@@ -1,10 +1,11 @@
import { Label } from "@/components/ui/label";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { AsteriskIcon, InfoIcon } from "lucide-react";
import type React from "react";
export function FormFieldSetup(props: {
htmlFor?: string;
label: string;
label: React.ReactNode;
errorMessage: React.ReactNode | undefined;
children: React.ReactNode;
tooltip?: React.ReactNode;
@@ -1,7 +1,6 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ThirdwebMiniLogo } from "app/components/ThirdwebMiniLogo";
import { DiscordIcon } from "components/icons/brand-icons/DiscordIcon";
import { GithubIcon } from "components/icons/brand-icons/GithubIcon";
import { InstagramIcon } from "components/icons/brand-icons/InstagramIcon";
import { LinkedInIcon } from "components/icons/brand-icons/LinkedinIcon";
@@ -61,11 +60,6 @@ export function AppFooter(props: AppFooterProps) {
<YoutubeIcon className="size-5 text-muted-foreground" />
</Link>
</Button>
<Button size="icon" variant="ghost" asChild className="size-9">
<Link href="https://discord.gg/thirdweb" target="_blank">
<DiscordIcon className="size-5 text-muted-foreground" />
</Link>
</Button>
<Button size="icon" variant="ghost" asChild className="size-9">
<Link href="https://www.reddit.com/r/thirdweb/" target="_blank">
<RedditIcon className="size-5 text-muted-foreground" />
@@ -1,5 +1,12 @@
"use client";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
@@ -15,8 +22,14 @@ import {
EmptyChartState,
LoadingChartState,
} from "../../../../components/analytics/empty-chart-state";
import { cn } from "../../../lib/utils";
type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
header?: {
title: string;
description?: string;
titleClassName?: string;
};
// chart config
config: TConfig;
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
@@ -25,78 +38,99 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
// chart className
chartClassName?: string;
isPending: boolean;
hideLabel?: boolean;
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
};
export function ThirdwebAreaChart<TConfig extends ChartConfig>(
props: ThirdwebAreaChartProps<TConfig>,
) {
const configKeys = useMemo(() => Object.keys(props.config), [props.config]);
return (
<div className="rounded-lg border border-border px-4 pt-10 pb-4">
<ChartContainer config={props.config} className={props.chartClassName}>
{props.isPending ? (
<LoadingChartState />
) : props.data.length === 0 ? (
<EmptyChartState />
) : (
<AreaChart
accessibilityLayer
data={props.data}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tickMargin={20}
tickFormatter={(value) => formatDate(new Date(value), "MMM dd")}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
<defs>
{configKeys.map((key) => (
<linearGradient
key={key}
id={`fill_${key}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="5%"
stopColor={`var(--color-${key})`}
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor={`var(--color-${key})`}
stopOpacity={0.1}
/>
</linearGradient>
))}
</defs>
{configKeys.map((key) => (
<Area
key={key}
dataKey={key}
type="natural"
fill={`url(#fill_${key})`}
fillOpacity={0.4}
stroke={`var(--color-${key})`}
stackId="a"
/>
))}
{props.showLegend && (
<ChartLegend content={<ChartLegendContent />} className="pt-8" />
)}
</AreaChart>
)}
</ChartContainer>
</div>
return (
<Card>
{props.header && (
<CardHeader>
<CardTitle className={cn("mb-2", props.header.titleClassName)}>
{props.header.title}
</CardTitle>
{props.header.description && (
<CardDescription>{props.header.description}</CardDescription>
)}
</CardHeader>
)}
<CardContent className={cn(!props.header && "pt-6")}>
<ChartContainer config={props.config} className={props.chartClassName}>
{props.isPending ? (
<LoadingChartState />
) : props.data.length === 0 ? (
<EmptyChartState />
) : (
<AreaChart accessibilityLayer data={props.data}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tickMargin={20}
tickFormatter={(value) => formatDate(new Date(value), "MMM dd")}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
hideLabel={
props.hideLabel !== undefined ? props.hideLabel : true
}
labelFormatter={props.toolTipLabelFormatter}
/>
}
/>
<defs>
{configKeys.map((key) => (
<linearGradient
key={key}
id={`fill_${key}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="5%"
stopColor={`var(--color-${key})`}
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor={`var(--color-${key})`}
stopOpacity={0.1}
/>
</linearGradient>
))}
</defs>
{configKeys.map((key) => (
<Area
key={key}
dataKey={key}
type="natural"
fill={`url(#fill_${key})`}
fillOpacity={0.4}
stroke={`var(--color-${key})`}
stackId="a"
/>
))}
{props.showLegend && (
<ChartLegend
content={<ChartLegendContent className="pt-8" />}
/>
)}
</AreaChart>
)}
</ChartContainer>
</CardContent>
</Card>
);
}
@@ -27,8 +27,11 @@ import { cn } from "../../../lib/utils";
type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
// metadata
title: string;
description?: string;
header?: {
title: string;
description?: string;
titleClassName?: string;
};
// chart config
config: TConfig;
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
@@ -39,7 +42,6 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
isPending: boolean;
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
hideLabel?: boolean;
titleClassName?: string;
};
export function ThirdwebBarChart<TConfig extends ChartConfig>(
@@ -52,15 +54,18 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
return (
<Card>
<CardHeader>
<CardTitle className={cn("mb-2", props.titleClassName)}>
{props.title}
</CardTitle>
{props.description && (
<CardDescription>{props.description}</CardDescription>
)}
</CardHeader>
<CardContent>
{props.header && (
<CardHeader>
<CardTitle className={cn("mb-2", props.header.titleClassName)}>
{props.header.title}
</CardTitle>
{props.header.description && (
<CardDescription>{props.header.description}</CardDescription>
)}
</CardHeader>
)}
<CardContent className={cn(!props.header && "pt-6")}>
<ChartContainer config={props.config} className={props.chartClassName}>
{props.isPending ? (
<LoadingChartState />
@@ -87,7 +92,9 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
}
/>
{props.showLegend && (
<ChartLegend content={<ChartLegendContent />} />
<ChartLegend
content={<ChartLegendContent className="pt-5" />}
/>
)}
{configKeys.map((key, idx) => (
<Bar
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { cn } from "@/lib/utils";
import { CheckIcon, CircleDollarSignIcon } from "lucide-react";
import { CheckIcon, CircleAlertIcon, CircleDollarSignIcon } from "lucide-react";
import type React from "react";
import { TEAM_PLANS } from "utils/pricing";
import { remainingDays } from "../../../utils/date-utils";
@@ -105,6 +105,23 @@ export const PricingCard: React.FC<PricingCardProps> = ({
{!isCustomPrice && (
<span className="text-muted-foreground">/ month</span>
)}
{billingPlan === "starter" && (
<ToolTipLabel
contentClassName="max-w-[320px]"
label="We will place a temporary hold of $25 to verify your card, this will be immediately released back to you after verification."
>
<Button
asChild
variant="ghost"
className="h-auto w-auto p-1 text-muted-foreground hover:text-foreground"
>
<div>
<CircleAlertIcon className="size-5 shrink-0" />
</div>
</Button>
</ToolTipLabel>
)}
</div>
{remainingTrialDays > 0 && (
@@ -1,10 +1,20 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { cn } from "../../../lib/utils";
export function GenericLoadingPage() {
export function GenericLoadingPage({
className,
}: {
className?: string;
}) {
return (
<div className="flex min-h-[500px] grow items-center justify-center rounded-lg border border-border">
<div
className={cn(
"flex min-h-[500px] grow items-center justify-center rounded-lg border border-border",
className,
)}
>
<Spinner className="size-10" />
</div>
);
@@ -2,27 +2,29 @@
import { Button } from "@/components/ui/button";
import { ClientOnly } from "components/ClientOnly/ClientOnly";
import { Moon, Sun } from "lucide-react";
import { MoonIcon, SunIcon } from "lucide-react";
import { useTheme } from "next-themes";
import { Skeleton } from "./ui/skeleton";
export function ColorModeToggle() {
export function ToggleThemeButton() {
const { setTheme, theme } = useTheme();
return (
<ClientOnly ssr={<Skeleton className="size-10 border bg-accent" />}>
<ClientOnly
ssr={<Skeleton className="size-[34px] rounded-full border bg-accent" />}
>
<Button
variant="ghost"
size="icon"
variant="outline"
onClick={() => {
setTheme(theme === "dark" ? "light" : "dark");
}}
aria-label="Toggle theme"
className="h-auto w-auto rounded-full border border-border bg-background p-2"
>
{theme === "light" ? (
<Sun strokeWidth={1} className="size-5" />
<SunIcon className="size-4" />
) : (
<Moon strokeWidth={1} className="size-5" />
<MoonIcon className="size-4" />
)}
</Button>
</ClientOnly>
@@ -27,6 +27,7 @@ export function DatePickerWithRange(props: {
header?: React.ReactNode;
footer?: React.ReactNode;
labelOverride?: string;
popoverAlign?: "start" | "end" | "center";
}) {
const [screen, setScreen] = React.useState<"from" | "to">("from");
const { from, to, setFrom, setTo } = props;
@@ -65,7 +66,11 @@ export function DatePickerWithRange(props: {
</PopoverTrigger>
{/* Popover */}
<PopoverContent className="w-auto p-0" align="start">
<PopoverContent
className="w-auto p-0"
align={props.popoverAlign || "start"}
sideOffset={10}
>
<DynamicHeight>
<div>
{!isValid && (
@@ -0,0 +1,16 @@
import Link from "next/link";
import { cn } from "../../lib/utils";
type LinkProps = React.ComponentProps<typeof Link>;
export function UnderlineLink(props: LinkProps) {
return (
<Link
{...props}
className={cn(
"underline decoration-muted-foreground/50 decoration-dotted underline-offset-[5px] hover:text-foreground hover:decoration-foreground hover:decoration-solid",
props.className,
)}
/>
);
}
@@ -0,0 +1,20 @@
import { cn } from "@/lib/utils";
export function DotsBackgroundPattern(props: {
className?: string;
}) {
return (
<div
className={cn(
"pointer-events-none absolute inset-0 text-foreground/30 dark:text-foreground/10",
props.className,
)}
style={{
backgroundImage: "radial-gradient(currentColor 1px, transparent 1px)",
backgroundSize: "24px 24px",
maskImage:
"radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 50%)",
}}
/>
);
}
@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
className,
)}
{...props}
@@ -43,7 +43,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-lg",
"fixed z-50 grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-lg",
// on mobile - put the dialog at the bottom of the screen, animate - slide up and fade in
"right-0 bottom-0 left-0",
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+5
View File
@@ -4,6 +4,8 @@ export const DASHBOARD_THIRDWEB_CLIENT_ID =
export const DASHBOARD_THIRDWEB_SECRET_KEY =
process.env.DASHBOARD_SECRET_KEY || "";
export const THIRDWEB_API_SECRET = process.env.API_SERVER_SECRET || "";
export const IPFS_GATEWAY_URL =
(process.env.NEXT_PUBLIC_IPFS_GATEWAY_URL as string) ||
"https://{clientId}.ipfscdn.io/ipfs/{cid}/{path}";
@@ -21,6 +23,9 @@ export const DASHBOARD_STORAGE_URL =
export const API_SERVER_URL =
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
export const BRIDGE_URL =
process.env.NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST || "https://bridge.thirdweb.com";
/**
* Faucet stuff
*/
+1 -1
View File
@@ -117,7 +117,7 @@ function DashboardRouterTopProgressBarInner() {
const width = isLoading ? progress : 100;
return (
<span
className="fixed top-0 block h-[2px] bg-blue-500"
className="fixed top-0 block h-[3px] bg-foreground"
style={{
opacity: isLoading ? "100" : "0",
width: `${width}%`,
@@ -42,15 +42,6 @@ export const accountKeys = {
[...accountKeys.wallet(walletAddress), "billing-session"] as const,
};
export const apiKeys = {
all: ["api"] as const,
wallet: (walletAddress: string) => [...apiKeys.all, walletAddress] as const,
keys: (walletAddress: string) =>
[...apiKeys.wallet(walletAddress), "keys"] as const,
key: (id: string, walletAddress: string) =>
[...apiKeys.keys(walletAddress), id] as const,
};
export const authorizedWallets = {
all: ["authorizedWallets"] as const,
wallet: (walletAddress: string) =>
@@ -125,4 +116,6 @@ export const engineKeys = {
[...engineKeys.all, engineId, "alerts"] as const,
notificationChannels: (engineId: string) =>
[...engineKeys.all, engineId, "notificationChannels"] as const,
walletCredentials: (instance: string) =>
[...engineKeys.all, instance, "walletCredentials"] as const,
};
@@ -1,22 +1,12 @@
import { analyticsServerProxy, apiServerProxy } from "@/actions/proxies";
import { apiServerProxy } from "@/actions/proxies";
import type { Project } from "@/api/projects";
import type { Team } from "@/api/team";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useAllChainsData } from "hooks/chains/allChains";
import { useActiveAccount } from "thirdweb/react";
import type { UserOpStats } from "types/analytics";
import { accountKeys, apiKeys, authorizedWallets } from "../cache-keys";
import { accountKeys, authorizedWallets } from "../cache-keys";
// FIXME: We keep repeating types, API server should provide them
export const accountStatus = {
noCustomer: "noCustomer",
noPayment: "noPayment",
paymentVerification: "paymentVerification",
validPayment: "validPayment",
invalidPayment: "invalidPayment",
invalidPaymentMethod: "invalidPaymentMethod",
} as const;
export const accountPlan = {
free: "free",
growth: "growth",
@@ -39,6 +29,7 @@ export type Account = {
id: string;
isStaff: boolean;
creatorWalletAddress: string;
image?: string | null;
name?: string;
email?: string;
advancedEnabled: boolean;
@@ -70,109 +61,6 @@ interface ConfirmEmailInput {
confirmationToken: string;
}
type ApiKeyRecoverShareManagement = "AWS_MANAGED" | "USER_MANAGED";
type ApiKeyCustomAuthentication = {
jwksUri: string;
aud: string;
};
type ApiKeyCustomAuthEndpoint = {
authEndpoint: string;
customHeaders: { key: string; value: string }[];
};
// MAP to api-server types in PolicyService.ts
export type ApiKeyServicePolicy = {
allowedChainIds?: number[] | null;
allowedContractAddresses?: string[] | null;
allowedWallets?: string[] | null;
blockedWallets?: string[] | null;
bypassWallets?: string[] | null;
serverVerifier?: {
url: string;
headers: { key: string; value: string }[] | null;
} | null;
limits?: ApiKeyServicePolicyLimits | null;
};
export type ApiKeyServicePolicyLimits = {
global?: {
// in dollars or ETH
maxSpend: string;
maxSpendUnit: "usd" | "native";
} | null;
// ----------------------
// TODO implement perUser limits
perUserSpend?: {
// in dollars or ETH
maxSpend: string | null;
maxSpendUnit: "usd" | "native";
maxSpendPeriod: "day" | "week" | "month";
} | null;
perUserTransactions?: {
maxTransactions: number;
maxTransactionsPeriod: "day" | "week" | "month";
} | null;
};
export type ApiKeyService = {
id: string;
name: string;
targetAddresses: string[];
actions: string[];
// If updating here, need to update validation logic in `validation.ts` as well for recoveryShareManagement
// EMBEDDED WALLET
recoveryShareManagement?: ApiKeyRecoverShareManagement;
customAuthentication?: ApiKeyCustomAuthentication;
customAuthEndpoint?: ApiKeyCustomAuthEndpoint;
applicationName?: string;
applicationImageUrl?: string;
// PAY
payoutAddress?: string;
};
export type ApiKey = {
id: string;
name: string;
key: string;
secret?: string;
secretMasked: string;
accountId: string;
creatorWalletAddress: string;
walletAddresses: string[];
domains: string[];
bundleIds: string[];
redirectUrls: string[];
revokedAt: string;
lastAccessedAt: string;
createdAt: string;
updatedAt: string;
services?: ApiKeyService[];
};
interface UpdateKeyServiceInput {
name: string;
targetAddresses: string[];
actions?: string[];
}
export interface CreateKeyInput {
name?: string;
domains?: string[];
bundleIds?: string[];
walletAddresses?: string[];
services?: UpdateKeyServiceInput[];
}
export interface UpdateKeyInput {
id: string;
name: string;
domains: string[];
bundleIds: string[];
walletAddresses?: string[];
services?: UpdateKeyServiceInput[];
redirectUrls: string[];
}
interface UsageStorage {
sumFileSizeBytes: number;
}
@@ -252,132 +140,6 @@ export function useAccountCredits() {
});
}
type UserOpUsageQueryResult = (UserOpStats & { chainId?: string })[];
async function getUserOpUsage(args: {
clientId: string;
from?: Date;
to?: Date;
period?: "day" | "week" | "month" | "year" | "all";
}) {
const { clientId, from, to, period } = args;
const searchParams: Record<string, string> = {
clientId,
};
if (from) {
searchParams.from = from.toISOString();
}
if (to) {
searchParams.to = to.toISOString();
}
if (period) {
searchParams.period = period;
}
const res = await analyticsServerProxy<{ data: UserOpUsageQueryResult }>({
pathname: "/v1/user-ops",
method: "GET",
searchParams: searchParams,
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
throw new Error(res.error);
}
const json = res.data;
return json.data;
}
// TODO - remove this hook, fetch this on server
export function useUserOpUsageAggregate(args: {
clientId: string;
from?: Date;
to?: Date;
}) {
const { clientId, from, to } = args;
const address = useActiveAccount()?.address;
const chainStore = useAllChainsData();
return useQuery<UserOpStats>({
queryKey: accountKeys.userOpStats(
address || "",
clientId,
from?.toISOString() || "",
to?.toISOString() || "",
"all",
),
queryFn: async () => {
const userOpStats = await getUserOpUsage({
clientId,
from,
to,
period: "all",
});
// Aggregate stats across wallet types
return userOpStats.reduce(
(acc, curr) => {
// Skip testnets from the aggregated stats
if (curr.chainId) {
const chain = chainStore.idToChain.get(Number(curr.chainId));
if (chain?.testnet) {
return acc;
}
}
acc.successful += curr.successful;
acc.failed += curr.failed;
acc.sponsoredUsd += curr.sponsoredUsd;
return acc;
},
{
date: (from || new Date()).toISOString(),
successful: 0,
failed: 0,
sponsoredUsd: 0,
},
);
},
enabled: !!clientId && !!address,
});
}
// TODO - remove this hook, fetch this on server
export function useUserOpUsagePeriod(args: {
clientId: string;
from?: Date;
to?: Date;
period: "day" | "week" | "month" | "year";
}) {
const { clientId, from, to, period } = args;
const address = useActiveAccount()?.address;
return useQuery({
queryKey: accountKeys.userOpStats(
address || "",
clientId as string,
from?.toISOString() || "",
to?.toISOString() || "",
period,
),
queryFn: async () => {
return getUserOpUsage({
clientId,
from,
to,
period,
});
},
enabled: !!clientId && !!address,
});
}
export function useUpdateAccount() {
const queryClient = useQueryClient();
const address = useActiveAccount()?.address;
@@ -460,9 +222,6 @@ export function useUpdateNotifications() {
}
export function useConfirmEmail() {
const address = useActiveAccount()?.address;
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (input: ConfirmEmailInput) => {
type Result = {
@@ -491,20 +250,6 @@ export function useConfirmEmail() {
return json.data;
},
onSuccess: async () => {
// invalidate related cache, since could be relinking account
return Promise.all([
queryClient.invalidateQueries({
queryKey: apiKeys.keys(address || ""),
}),
queryClient.invalidateQueries({
queryKey: accountKeys.usage(address || ""),
}),
queryClient.invalidateQueries({
queryKey: accountKeys.me(address || ""),
}),
]);
},
});
}
@@ -548,205 +293,106 @@ export function useResendEmailConfirmation() {
});
}
export function useCreateApiKey() {
const address = useActiveAccount()?.address;
const queryClient = useQueryClient();
export async function createProjectClient(
teamId: string,
body: Partial<Project>,
) {
type Response = {
result: {
project: Project;
secret: string;
};
};
return useMutation({
mutationFn: async (input: CreateKeyInput) => {
type Result = {
data: ApiKey;
error?: { message: string };
};
const res = await apiServerProxy<Result>({
pathname: "/v1/keys",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(input),
});
if (!res.ok) {
throw new Error(res.error);
}
const json = res.data;
if (json.error) {
throw new Error(json.error.message);
}
return json.data;
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: apiKeys.keys(address || ""),
});
const res = await apiServerProxy<Response>({
pathname: `/v1/teams/${teamId}/projects`,
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error(res.error);
}
return res.data.result;
}
export function useUpdateApiKey() {
const address = useActiveAccount()?.address;
const queryClient = useQueryClient();
export async function updateProjectClient(
params: {
projectId: string;
teamId: string;
},
body: Partial<Project>,
) {
type Response = {
result: Project;
};
return useMutation({
mutationFn: async (input: UpdateKeyInput) => {
type Result = {
data: ApiKey;
error?: { message: string };
};
const res = await apiServerProxy<Result>({
pathname: `/v1/keys/${input.id}`,
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(input),
});
if (!res.ok) {
throw new Error(res.error);
}
const json = res.data;
if (json.error) {
throw new Error(json.error.message);
}
return json.data;
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: apiKeys.keys(address || ""),
});
const res = await apiServerProxy<Response>({
pathname: `/v1/teams/${params.teamId}/projects/${params.projectId}`,
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error(res.error);
}
return res.data.result;
}
export function useRevokeApiKey() {
const address = useActiveAccount()?.address;
const queryClient = useQueryClient();
export async function deleteProjectClient(params: {
projectId: string;
teamId: string;
}) {
type Response = {
result: true;
};
return useMutation({
mutationFn: async (id: string) => {
type Result = {
data: ApiKey;
error?: { message: string };
};
const res = await apiServerProxy<Result>({
pathname: `/v1/keys/${id}/revoke`,
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
if (!res.ok) {
throw new Error(res.error);
}
const json = res.data;
if (json.error) {
throw new Error(json.error.message);
}
return json.data;
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: apiKeys.keys(address || ""),
});
},
const res = await apiServerProxy<Response>({
pathname: `/v1/teams/${params.teamId}/projects/${params.projectId}`,
method: "DELETE",
});
if (!res.ok) {
throw new Error(res.error);
}
return res.data.result;
}
export const usePolicies = (serviceId?: string) => {
return useQuery({
queryKey: ["policies", serviceId],
queryFn: async () => {
if (!serviceId) {
throw new Error();
}
type Result = {
data: ApiKeyServicePolicy;
error?: { message: string };
};
const res = await apiServerProxy<Result>({
pathname: "/v1/policies",
method: "GET",
headers: {
"Content-Type": "application/json",
},
searchParams: {
serviceId,
},
});
if (!res.ok) {
throw new Error(res.error);
}
const json = res.data;
if (json.error) {
throw new Error(json.error.message);
}
return json.data;
},
enabled: !!serviceId,
});
export type RotateSecretKeyAPIReturnType = {
data: {
secret: string;
secretMasked: string;
secretHash: string;
};
};
export const useUpdatePolicies = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (input: {
serviceId: string;
data: ApiKeyServicePolicy;
}) => {
type Result = {
data: ApiKeyServicePolicy;
error?: { message: string };
};
const res = await apiServerProxy<Result>({
pathname: "/v1/policies",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
serviceId: input.serviceId,
data: input.data,
}),
});
if (!res.ok) {
throw new Error(res.error);
}
const json = res.data;
if (json.error) {
throw new Error(json.error.message);
}
return json.data;
},
onSuccess: (_, variables) => {
return queryClient.invalidateQueries({
queryKey: ["policies", variables.serviceId],
});
export async function rotateSecretKeyClient(projectId: string) {
const res = await apiServerProxy<RotateSecretKeyAPIReturnType>({
pathname: "/v2/keys/rotate-secret-key",
method: "POST",
body: JSON.stringify({
projectId,
}),
headers: {
"Content-Type": "application/json",
},
});
};
if (!res.ok) {
throw new Error(res.error);
}
return res.data;
}
export function useRevokeAuthorizedWallet() {
const address = useActiveAccount()?.address;
@@ -15,6 +15,7 @@ export function useDashboardContractMetadata(contract: ThirdwebContract) {
return useQuery({
queryKey: ["contract-metadata-header", contract.chain.id, contract.address],
queryFn: () => fetchDashboardContractMetadata(contract),
refetchOnWindowFocus: false,
});
}
@@ -93,7 +93,8 @@ type EngineFeature =
| "CONTRACT_SUBSCRIPTIONS"
| "IP_ALLOWLIST"
| "HETEROGENEOUS_WALLET_TYPES"
| "SMART_BACKEND_WALLETS";
| "SMART_BACKEND_WALLETS"
| "WALLET_CREDENTIALS";
interface EngineSystemHealth {
status: string;
@@ -860,6 +861,10 @@ export type SetWalletConfigInput =
gcpKmsKeyRingId: string;
gcpApplicationCredentialEmail: string;
gcpApplicationCredentialPrivateKey: string;
}
| {
type: "circle";
circleApiKey: string;
};
export function useEngineSetWalletConfig(params: {
@@ -869,8 +874,8 @@ export function useEngineSetWalletConfig(params: {
const { instanceUrl, authToken } = params;
const queryClient = useQueryClient();
return useMutation<WalletConfigResponse, void, SetWalletConfigInput>({
mutationFn: async (input) => {
return useMutation({
mutationFn: async (input: SetWalletConfigInput) => {
invariant(instanceUrl, "instance is required");
const res = await fetch(`${instanceUrl}configuration/wallets`, {
@@ -884,7 +889,7 @@ export function useEngineSetWalletConfig(params: {
throw new Error(json.error.message);
}
return json.result;
return json.result as WalletConfigResponse;
},
onSuccess: () => {
return queryClient.invalidateQueries({
@@ -894,10 +899,17 @@ export function useEngineSetWalletConfig(params: {
});
}
export type CreateBackendWalletInput = {
type: EngineBackendWalletType;
label?: string;
};
export type CreateBackendWalletInput =
| {
type: Exclude<EngineBackendWalletType, "circle" | "smart:circle">;
label?: string;
}
| {
type: "circle" | "smart:circle";
label?: string;
credentialId: string;
isTestnet: boolean;
};
export function useEngineCreateBackendWallet(params: {
instanceUrl: string;
@@ -1851,3 +1863,118 @@ export function useEngineDeleteNotificationChannel(engineId: string) {
},
});
}
export interface WalletCredential {
id: string;
type: string;
label: string;
isDefault: boolean | null;
createdAt: string;
updatedAt: string;
}
interface CreateWalletCredentialInput {
type: "circle";
label: string;
entitySecret?: string;
isDefault?: boolean;
}
export function useEngineWalletCredentials(params: {
instanceUrl: string;
authToken: string;
page?: number;
limit?: number;
}) {
const { instanceUrl, authToken, page = 1, limit = 100 } = params;
return useQuery({
queryKey: [...engineKeys.walletCredentials(instanceUrl), page, limit],
queryFn: async () => {
const res = await fetch(
`${instanceUrl}wallet-credentials?page=${page}&limit=${limit}`,
{
method: "GET",
headers: getEngineRequestHeaders(authToken),
},
);
const json = await res.json();
return (json.result as WalletCredential[]) || [];
},
enabled: !!instanceUrl,
});
}
export function useEngineCreateWalletCredential(params: {
instanceUrl: string;
authToken: string;
}) {
const { instanceUrl, authToken } = params;
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (input: CreateWalletCredentialInput) => {
invariant(instanceUrl, "instance is required");
const res = await fetch(`${instanceUrl}wallet-credentials`, {
method: "POST",
headers: getEngineRequestHeaders(authToken),
body: JSON.stringify(input),
});
const json = await res.json();
if (json.error) {
throw new Error(json.error.message);
}
return json.result as WalletCredential;
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.walletCredentials(instanceUrl),
});
},
});
}
interface UpdateWalletCredentialInput {
label?: string;
isDefault?: boolean;
entitySecret?: string;
}
export function useEngineUpdateWalletCredential(params: {
instanceUrl: string;
authToken: string;
}) {
const { instanceUrl, authToken } = params;
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
id,
...input
}: UpdateWalletCredentialInput & { id: string }) => {
invariant(instanceUrl, "instance is required");
const res = await fetch(`${instanceUrl}wallet-credentials/${id}`, {
method: "PUT",
headers: getEngineRequestHeaders(authToken),
body: JSON.stringify(input),
});
const json = await res.json();
if (json.error) {
throw new Error(json.error.message);
}
return json.result as WalletCredential;
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.walletCredentials(instanceUrl),
});
},
});
}
@@ -1,149 +0,0 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { addContractToMultiChainRegistry } from "components/contract-components/utils";
import { MULTICHAIN_REGISTRY_CONTRACT } from "constants/contracts";
import {
DASHBOARD_ENGINE_RELAYER_URL,
DASHBOARD_FORWARDER_ADDRESS,
} from "constants/misc";
import type { BasicContract } from "contract-ui/types/types";
import { getAllMultichainRegistry } from "dashboard-extensions/common/read/getAllMultichainRegistry";
import { useAllChainsData } from "hooks/chains/allChains";
import { useMemo } from "react";
import { sendAndConfirmTransaction } from "thirdweb";
import { remove } from "thirdweb/extensions/thirdweb";
import { useActiveAccount } from "thirdweb/react";
import invariant from "tiny-invariant";
export function useMultiChainRegContractList(walletAddress?: string) {
return useQuery({
queryKey: ["dashboard-registry", walletAddress, "multichain-contract-list"],
queryFn: async () => {
invariant(walletAddress, "walletAddress is required");
const contracts = await getAllMultichainRegistry({
contract: MULTICHAIN_REGISTRY_CONTRACT,
address: walletAddress,
});
return contracts;
},
enabled: !!walletAddress,
});
}
interface Options {
onlyMainnet?: boolean;
}
export const useAllContractList = (
walletAddress: string | undefined,
{ onlyMainnet }: Options = { onlyMainnet: false },
) => {
const multiChainQuery = useMultiChainRegContractList(walletAddress);
// TODO - instead of using ALL chains, fetch only the ones used here
const { idToChain } = useAllChainsData();
const contractList = useMemo(() => {
const data = multiChainQuery.data || [];
const mainnets: BasicContract[] = [];
const testnets: BasicContract[] = [];
// biome-ignore lint/complexity/noForEach: FIXME
data.forEach((net) => {
const chn = idToChain.get(net.chainId);
if (chn && chn.status !== "deprecated") {
if (chn.testnet) {
testnets.push(net);
} else {
mainnets.push(net);
}
}
});
mainnets.sort((a, b) => a.chainId - b.chainId);
if (onlyMainnet) {
return mainnets;
}
testnets.sort((a, b) => a.chainId - b.chainId);
return mainnets.concat(testnets);
}, [multiChainQuery.data, onlyMainnet, idToChain]);
return {
...multiChainQuery,
data: contractList,
};
};
type RemoveContractParams = {
contractAddress: string;
chainId: number;
};
export function useRemoveContractMutation() {
const queryClient = useQueryClient();
const account = useActiveAccount();
return useMutation({
mutationFn: async (data: RemoveContractParams) => {
invariant(data.chainId, "chainId not provided");
invariant(account, "No wallet connected");
const { contractAddress, chainId } = data;
const transaction = remove({
contract: MULTICHAIN_REGISTRY_CONTRACT,
deployer: account.address,
deployment: contractAddress,
chainId: BigInt(chainId),
});
return await sendAndConfirmTransaction({
transaction,
account,
gasless: {
experimentalChainlessSupport: true,
provider: "engine",
relayerUrl: DASHBOARD_ENGINE_RELAYER_URL,
relayerForwarderAddress: DASHBOARD_FORWARDER_ADDRESS,
},
});
},
onSettled: () => {
return queryClient.invalidateQueries({
queryKey: ["dashboard-registry"],
});
},
});
}
type AddContractParams = {
contractAddress: string;
chainId: number;
};
export function useAddContractMutation() {
const account = useActiveAccount();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: AddContractParams) => {
invariant(account, "cannot add a contract without an address");
return await addContractToMultiChainRegistry(
{
address: data.contractAddress,
chainId: data.chainId,
},
account,
300000n,
);
},
onSettled: () => {
return queryClient.invalidateQueries({
queryKey: ["dashboard-registry"],
});
},
});
}
@@ -0,0 +1,37 @@
"use client";
import { PaginationButtons } from "@/components/pagination-buttons";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { usePathname, useSearchParams } from "next/navigation";
import { useCallback } from "react";
type ChainlistPaginationProps = {
totalPages: number;
activePage: number;
};
export const ChainlistPagination: React.FC<ChainlistPaginationProps> = ({
activePage,
totalPages,
}) => {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useDashboardRouter();
const createPageURL = useCallback(
(pageNumber: number) => {
const params = new URLSearchParams(searchParams || undefined);
params.set("page", pageNumber.toString());
return `${pathname}?${params.toString()}`;
},
[pathname, searchParams],
);
return (
<PaginationButtons
activePage={activePage}
totalPages={totalPages}
onPageClick={(page) => router.push(createPageURL(page))}
/>
);
};
@@ -0,0 +1,74 @@
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { SearchIcon, XCircleIcon } from "lucide-react";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect, useRef } from "react";
import { useDebouncedCallback } from "use-debounce";
function cleanUrl(url: string) {
if (url.endsWith("?")) {
return url.slice(0, -1);
}
return url;
}
export const SearchInput: React.FC = () => {
const router = useDashboardRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const inputRef = useRef<HTMLInputElement>(null);
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
// reset the input if the query param is removed
if (inputRef.current?.value && !searchParams?.get("query")) {
inputRef.current.value = "";
}
}, [searchParams]);
const handleSearch = useDebouncedCallback((term: string) => {
const params = new URLSearchParams(searchParams ?? undefined);
if (term) {
params.set("query", term);
} else {
params.delete("query");
}
// always delete the page number when searching
params.delete("page");
const url = cleanUrl(`${pathname}?${params.toString()}`);
router.replace(url);
}, 300);
return (
<div className="group relative w-full">
<SearchIcon className="-translate-y-1/2 absolute top-[50%] left-3 size-4 text-muted-foreground" />
<Input
placeholder="Search by token address or chain ID"
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)}
ref={inputRef}
/>
{searchParams?.has("query") && (
<Button
size="icon"
className="-translate-y-1/2 absolute top-[50%] right-0 bg-background text-muted-foreground opacity-0 transition duration-300 ease-in-out group-hover:opacity-100"
variant="outline"
onClick={() => {
const params = new URLSearchParams(searchParams ?? undefined);
params.delete("query");
params.delete("page");
const url = cleanUrl(`${pathname}?${params.toString()}`);
router.replace(url);
}}
>
<XCircleIcon className="size-5" />
</Button>
)}
</div>
);
};
@@ -0,0 +1,55 @@
"use client";
import { Button } from "@/components/ui/button";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { ArrowDownLeftIcon, ArrowUpRightIcon } from "lucide-react";
import { usePathname, useSearchParams } from "next/navigation";
import { useCallback } from "react";
type QueryTypeProps = {
activeType: "origin" | "destination";
};
export const QueryType: React.FC<QueryTypeProps> = ({ activeType }) => {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useDashboardRouter();
const createPageURL = useCallback(
(type: "origin" | "destination") => {
const params = new URLSearchParams(searchParams || undefined);
params.set("type", type);
return `${pathname}?${params.toString()}`;
},
[pathname, searchParams],
);
return (
<div className="flex flex-row">
<ToolTipLabel label="Origin" contentClassName="w-full">
<Button
size="icon"
variant={activeType === "origin" ? "default" : "outline"}
onClick={() => {
router.replace(createPageURL("origin"));
}}
className="rounded-r-none"
>
<ArrowUpRightIcon strokeWidth={1} />
</Button>
</ToolTipLabel>
<ToolTipLabel label="Destination" contentClassName="w-full">
<Button
variant={activeType === "destination" ? "default" : "outline"}
size="icon"
onClick={() => {
router.replace(createPageURL("destination"));
}}
className="rounded-l-none"
>
<ArrowDownLeftIcon strokeWidth={1} />
</Button>
</ToolTipLabel>
</div>
);
};
@@ -0,0 +1,50 @@
"use client";
import { Button } from "@/components/ui/button";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { Grid2X2Icon, ListIcon } from "lucide-react";
import { usePathname, useSearchParams } from "next/navigation";
import { useCallback } from "react";
type RouteListViewProps = {
activeView: "grid" | "table";
};
export const RouteListView: React.FC<RouteListViewProps> = ({ activeView }) => {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useDashboardRouter();
const createPageURL = useCallback(
(view: "grid" | "table") => {
const params = new URLSearchParams(searchParams || undefined);
params.set("view", view);
return `${pathname}?${params.toString()}`;
},
[pathname, searchParams],
);
return (
<div className="flex flex-row">
<Button
size="icon"
variant={activeView === "table" ? "default" : "outline"}
onClick={() => {
router.replace(createPageURL("table"));
}}
className="rounded-r-none"
>
<ListIcon strokeWidth={1} />
</Button>
<Button
variant={activeView === "grid" ? "default" : "outline"}
size="icon"
onClick={() => {
router.replace(createPageURL("grid"));
}}
className="rounded-l-none"
>
<Grid2X2Icon strokeWidth={1} />
</Button>
</div>
);
};
@@ -0,0 +1,126 @@
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
import { NATIVE_TOKEN_ADDRESS, defineChain, getContract } from "thirdweb";
import { getChainMetadata } from "thirdweb/chains";
import { name } from "thirdweb/extensions/common";
type RouteListCardProps = {
originChainId: number;
originTokenAddress: string;
originTokenIconUri: string | null;
destinationChainId: number;
destinationTokenAddress: string;
destinationTokenIconUri: string | null;
};
export async function RouteListCard({
originChainId,
originTokenAddress,
originTokenIconUri,
destinationChainId,
destinationTokenAddress,
destinationTokenIconUri,
}: RouteListCardProps) {
const [
originChain,
originTokenName,
destinationChain,
destinationTokenName,
resolvedOriginTokenIconUri,
resolvedDestinationTokenIconUri,
] = await Promise.all([
// eslint-disable-next-line no-restricted-syntax
getChainMetadata(defineChain(originChainId)),
originTokenAddress.toLowerCase() === NATIVE_TOKEN_ADDRESS
? "ETH"
: name({
contract: getContract({
address: originTokenAddress,
// eslint-disable-next-line no-restricted-syntax
chain: defineChain(originChainId),
client: getThirdwebClient(),
}),
}).catch(() => undefined),
// eslint-disable-next-line no-restricted-syntax
getChainMetadata(defineChain(destinationChainId)),
destinationTokenAddress.toLowerCase() === NATIVE_TOKEN_ADDRESS
? "ETH"
: name({
contract: getContract({
address: destinationTokenAddress,
// eslint-disable-next-line no-restricted-syntax
chain: defineChain(destinationChainId),
client: getThirdwebClient(),
}),
}).catch(() => undefined),
originTokenIconUri
? resolveSchemeWithErrorHandler({
uri: originTokenIconUri,
client: getThirdwebClient(),
})
: undefined,
destinationTokenIconUri
? resolveSchemeWithErrorHandler({
uri: destinationTokenIconUri,
client: getThirdwebClient(),
})
: undefined,
]);
return (
<div className="relative h-full">
<Card className="h-full w-full transition-colors hover:border-active-border">
<CardHeader className="flex flex-row items-center justify-between p-4">
<div className="flex flex-row items-center gap-2">
{resolvedOriginTokenIconUri ? (
<img
src={resolvedOriginTokenIconUri}
alt={originTokenAddress}
className="size-8 rounded-full bg-white"
/>
) : (
<div className="size-8 rounded-full bg-white/10" />
)}
{resolvedDestinationTokenIconUri ? (
<img
src={resolvedDestinationTokenIconUri}
alt={destinationTokenAddress}
className="-translate-x-4 size-8 rounded-full bg-white ring-2 ring-card"
/>
) : (
<div className="-translate-x-4 size-8 rounded-full bg-muted-foreground ring-2 ring-card" />
)}
</div>
</CardHeader>
<CardContent className="px-4 pt-0 pb-4">
<table className="w-full">
<tbody className="text-sm [&_td>*]:min-h-[25px]">
<tr>
<th className="text-left font-normal text-base">
{originTokenName === "ETH"
? originChain.nativeCurrency.name
: originTokenName}
</th>
<td className="text-right text-base text-muted-foreground">
{originChain.name}
</td>
</tr>
<tr>
<th className="text-left font-normal text-base">
{destinationTokenName === "ETH"
? destinationChain.nativeCurrency.name
: destinationTokenName}
</th>
<td className="text-right text-base text-muted-foreground">
{destinationChain.name}
</td>
</tr>
</tbody>
</table>
</CardContent>
</Card>
</div>
);
}
@@ -0,0 +1,144 @@
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { TableCell, TableRow } from "@/components/ui/table";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
import { NATIVE_TOKEN_ADDRESS, getContract } from "thirdweb";
import { defineChain, getChainMetadata } from "thirdweb/chains";
import { symbol } from "thirdweb/extensions/common";
type RouteListRowProps = {
originChainId: number;
originTokenAddress: string;
originTokenIconUri: string | null;
destinationChainId: number;
destinationTokenAddress: string;
destinationTokenIconUri: string | null;
};
export async function RouteListRow({
originChainId,
originTokenAddress,
originTokenIconUri,
destinationChainId,
destinationTokenAddress,
destinationTokenIconUri,
}: RouteListRowProps) {
const [
originChain,
originTokenSymbol,
destinationChain,
destinationTokenSymbol,
resolvedOriginTokenIconUri,
resolvedDestinationTokenIconUri,
] = await Promise.all([
// eslint-disable-next-line no-restricted-syntax
getChainMetadata(defineChain(originChainId)),
originTokenAddress.toLowerCase() === NATIVE_TOKEN_ADDRESS
? "ETH"
: symbol({
contract: getContract({
address: originTokenAddress,
// eslint-disable-next-line no-restricted-syntax
chain: defineChain(originChainId),
client: getThirdwebClient(),
}),
}).catch(() => undefined),
// eslint-disable-next-line no-restricted-syntax
getChainMetadata(defineChain(destinationChainId)),
destinationTokenAddress.toLowerCase() === NATIVE_TOKEN_ADDRESS
? "ETH"
: symbol({
contract: getContract({
address: destinationTokenAddress,
// eslint-disable-next-line no-restricted-syntax
chain: defineChain(destinationChainId),
client: getThirdwebClient(),
}),
}).catch(() => undefined),
originTokenIconUri
? resolveSchemeWithErrorHandler({
uri: originTokenIconUri,
client: getThirdwebClient(),
})
: undefined,
destinationTokenIconUri
? resolveSchemeWithErrorHandler({
uri: destinationTokenIconUri,
client: getThirdwebClient(),
})
: undefined,
]);
return (
<TableRow linkBox className="hover:bg-accent/50">
<TableCell>
<div className="flex flex-row items-center gap-4">
<div className="flex items-center gap-1">
{resolvedOriginTokenIconUri ? (
// For now we're using a normal img tag because the domain for these images is unknown
<img
src={resolvedOriginTokenIconUri}
alt={originTokenAddress}
className="size-6 rounded-full bg-muted-foreground"
/>
) : (
<div className="size-6 rounded-full bg-muted-foreground" />
)}
{originTokenSymbol && (
<CopyTextButton
textToCopy={originTokenAddress}
textToShow={
originTokenSymbol === "ETH"
? originChain.nativeCurrency.symbol
: originTokenSymbol
}
tooltip="Copy Token Address"
className="relative z-10 text-base"
variant="ghost"
copyIconPosition="right"
/>
)}
</div>
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{originChain.name}
</TableCell>
<TableCell>
<div className="flex flex-row items-center gap-4">
<div className="flex items-center gap-1">
{resolvedDestinationTokenIconUri ? (
<img
src={resolvedDestinationTokenIconUri}
alt={destinationTokenAddress}
className="size-6 rounded-full bg-muted-foreground"
/>
) : (
<div className="size-6 rounded-full bg-muted-foreground" />
)}
{destinationTokenSymbol && (
<CopyTextButton
textToCopy={destinationTokenAddress}
textToShow={
destinationTokenSymbol === "ETH"
? destinationChain.nativeCurrency.symbol
: destinationTokenSymbol
}
tooltip="Copy Token Address"
className="relative z-10 text-base"
variant="ghost"
copyIconPosition="right"
/>
)}
</div>
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{destinationChain.name}
</TableCell>
</TableRow>
);
}
@@ -0,0 +1,161 @@
import {
Table,
TableBody,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { Address } from "thirdweb";
import { getRoutes } from "../../../utils";
import { ChainlistPagination } from "../client/pagination";
import { RouteListCard } from "../server/routelist-card";
import { RouteListRow } from "../server/routelist-row";
export type SearchParams = Partial<{
includeDeprecated: boolean;
query: string;
page: number;
type: "origin" | "destination";
// maybe later we'll have a page size param?
// pageSize: number;
view: "table" | "grid";
}>;
// 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;
originChainId?: number;
originTokenAddress?: Address;
destinationChainId?: number;
destinationTokenAddress?: Address;
}> = {};
if (params.type === "origin" || typeof params.type === "undefined") {
if (params.query?.startsWith("0x")) {
filters.originTokenAddress = params.query as Address;
} else if (params.query) {
filters.originChainId = Number(params.query);
}
} else if (params.type === "destination") {
if (params.query?.startsWith("0x")) {
filters.destinationTokenAddress = params.query as Address;
} else if (params.query) {
filters.destinationChainId = Number(params.query);
}
}
// Temporary, will update this after the /routes endpoint
filters.limit = 10_000;
const routes = await getRoutes(filters);
const totalCount = routes.length;
return {
routesToRender: routes,
totalCount,
filteredCount: routes.length,
};
}
export async function RoutesData(props: {
searchParams: SearchParams;
activeView: "table" | "grid";
isLoggedIn: boolean;
}) {
const { routesToRender, totalCount, filteredCount } = await getRoutesToRender(
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);
return (
<>
<main>
{/* empty state */}
{paginatedRoutes.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>
<Table>
<TableHeader className="z-0">
<TableRow>
<TableHead>Origin Token</TableHead>
<TableHead>Origin Chain</TableHead>
<TableHead>Destination Token</TableHead>
<TableHead>Destination Chain</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedRoutes.map((route) => (
<RouteListRow
key={`${route.originToken.chainId}:${route.originToken.address}-${route.destinationToken.chainId}:${route.destinationToken.address}`}
originChainId={route.originToken.chainId}
originTokenAddress={route.originToken.address}
originTokenIconUri={route.originToken.iconUri}
destinationChainId={route.destinationToken.chainId}
destinationTokenAddress={route.destinationToken.address}
destinationTokenIconUri={route.destinationToken.iconUri}
/>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<ul className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{paginatedRoutes.map((route) => (
<li
key={`${route.originToken.chainId}:${route.originToken.address}-${route.destinationToken.chainId}:${route.destinationToken.address}`}
className="h-full"
>
<RouteListCard
originChainId={route.originToken.chainId}
originTokenAddress={route.originToken.address}
originTokenIconUri={route.originToken.iconUri}
destinationChainId={route.destinationToken.chainId}
destinationTokenAddress={route.destinationToken.address}
destinationTokenIconUri={route.destinationToken.iconUri}
/>
</li>
))}
</ul>
)}
</main>
<div className="h-10" />
{totalPages > 1 && (
<ChainlistPagination totalPages={totalPages} activePage={activePage} />
)}
<div className="h-4" />
<p className="text-balance text-center text-muted-foreground text-sm">
Showing{" "}
<span className="text-accent-foreground">{paginatedRoutes.length}</span>{" "}
out of{" "}
{filteredCount !== totalCount ? (
<>
<span className="text-accent-foreground">{filteredCount}</span>{" "}
routes that match filters. (Total:{" "}
<span className="text-accent-foreground">{totalCount}</span>)
</>
) : (
<>
<span className="text-accent-foreground">{totalCount}</span> routes.
</>
)}
</p>
</>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,70 @@
import type { Metadata } from "next";
import { headers } from "next/headers";
import { getAuthToken } from "../../../api/lib/getAuthToken";
import { SearchInput } from "./components/client/search";
import { QueryType } from "./components/client/type";
import { RouteListView } from "./components/client/view";
import {
RoutesData,
type SearchParams,
} from "./components/server/routes-table";
const title = "Routes: Swap, Bridge, and On-Ramp";
const description =
"A list of token routes for swapping, bridging, and on-ramping between EVM chains with thirdweb.";
export const metadata: Metadata = {
title,
description,
openGraph: {
title,
description,
},
};
export default async function ChainListPage(props: {
searchParams: Promise<SearchParams>;
}) {
const authToken = await getAuthToken();
const headersList = await headers();
const viewportWithHint = Number(
headersList.get("Sec-Ch-Viewport-Width") || 0,
);
const searchParams = await props.searchParams;
const activeType = searchParams.type ?? "origin";
// default is driven by viewport hint
const activeView = searchParams.view
? searchParams.view
: viewportWithHint > 1000
? "table"
: "grid";
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">
<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">
<SearchInput />
<QueryType activeType={activeType} />
<RouteListView activeView={activeView} />
</div>
</div>
</div>
</header>
<div className="h-10" />
<RoutesData
searchParams={searchParams}
activeView={activeView}
isLoggedIn={!!authToken}
/>
</section>
);
}
@@ -0,0 +1,14 @@
import type { Address } from "thirdweb";
export type Route = {
originToken: {
address: Address;
chainId: number;
iconUri: string;
};
destinationToken: {
address: Address;
chainId: number;
iconUri: string;
};
};
@@ -0,0 +1,60 @@
import "server-only";
import { BRIDGE_URL, DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/env";
import { getAuthToken } from "app/api/lib/getAuthToken";
import type { Address } from "thirdweb";
import type { Route } from "./types/route";
export async function getRoutes({
limit,
offset,
originChainId,
originTokenAddress,
destinationChainId,
destinationTokenAddress,
}: {
limit?: number;
offset?: number;
originChainId?: number;
originTokenAddress?: Address;
destinationChainId?: number;
destinationTokenAddress?: Address;
} = {}) {
const url = new URL(`${BRIDGE_URL}/v1/routes`);
if (limit) {
url.searchParams.set("limit", limit.toString());
}
if (offset) {
url.searchParams.set("offset", offset.toString());
}
if (originChainId) {
url.searchParams.set("originChainId", originChainId.toString());
}
if (originTokenAddress) {
url.searchParams.set("originTokenAddress", originTokenAddress);
}
if (destinationChainId) {
url.searchParams.set("destinationChainId", destinationChainId.toString());
}
if (destinationTokenAddress) {
url.searchParams.set("destinationTokenAddress", destinationTokenAddress);
}
const token = await getAuthToken();
const routesResponse = await fetch(url, {
headers: token
? {
authorization: `Bearer ${token}`,
}
: {
"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();
return routes.data;
}
@@ -1,8 +1,9 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import type { ThirdwebContract } from "thirdweb";
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import type { MinimalTeamsAndProjects } from "../../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { getContractPageSidebarLinks } from "../_utils/getContractPageSidebarLinks";
@@ -13,6 +14,8 @@ export function ContractPageLayoutClient(props: {
chainMetadata: ChainMetadata;
contract: ThirdwebContract;
children: React.ReactNode;
teamsAndProjects: MinimalTeamsAndProjects | undefined;
client: ThirdwebClient;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
const headerMetadataQuery = useQuery({
@@ -3,8 +3,9 @@ import type { SidebarLink } from "@/components/blocks/Sidebar";
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
import type { DashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
import type { ThirdwebContract } from "thirdweb";
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import type { MinimalTeamsAndProjects } from "../../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
import { ContractMetadata } from "./contract-metadata";
import { PrimaryDashboardButton } from "./primary-dashboard-button";
@@ -14,6 +15,8 @@ export function ContractPageLayout(props: {
children: React.ReactNode;
sidebarLinks: SidebarLink[];
dashboardContractMetadata: DashboardContractMetadata | undefined;
client: ThirdwebClient;
teamsAndProjects: MinimalTeamsAndProjects | undefined;
externalLinks:
| {
name: string;
@@ -27,6 +30,8 @@ export function ContractPageLayout(props: {
sidebarLinks,
dashboardContractMetadata,
externalLinks,
teamsAndProjects,
client,
} = props;
return (
@@ -49,6 +54,8 @@ export function ContractPageLayout(props: {
chainSlug: chainMetadata.slug,
contractAddress: contract.address,
}}
teamsAndProjects={teamsAndProjects}
client={client}
/>
</div>
<DeprecatedAlert chain={chainMetadata} />
@@ -1,19 +1,31 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { UnderlineLink } from "@/components/ui/UnderlineLink";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import type { EVMContractInfo } from "@3rdweb-sdk/react";
import {
useAddContractMutation,
useAllContractList,
} from "@3rdweb-sdk/react/hooks/useRegistry";
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import type { EVMContractInfo } from "@3rdweb-sdk/react";
import { useTrack } from "hooks/analytics/useTrack";
import { useTxNotifications } from "hooks/useTxNotifications";
import { CodeIcon, PlusIcon } from "lucide-react";
import { CircleAlertIcon, ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import type { Chain } from "thirdweb";
import { useActiveAccount } from "thirdweb/react";
import { useState } from "react";
import { toast } from "sonner";
import type { Chain, ThirdwebClient } from "thirdweb";
import {
AddToProjectSelector,
type MinimalTeamsAndProjects,
} from "../../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
import { useAddContractToProject } from "../../../../../team/[team_slug]/[project_slug]/hooks/project-contracts";
const TRACKING_CATEGORY = "add_to_dashboard_upsell";
@@ -22,6 +34,8 @@ type AddToDashboardCardProps = {
chain: Chain;
contractInfo: EVMContractInfo;
hideCodePageLink?: boolean;
teamsAndProjects: MinimalTeamsAndProjects | undefined;
client: ThirdwebClient;
};
export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
@@ -29,40 +43,13 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
chain,
contractInfo,
hideCodePageLink,
teamsAndProjects,
client,
}) => {
const addContract = useAddContractMutation();
const trackEvent = useTrack();
const walletAddress = useActiveAccount()?.address;
const pathname = usePathname();
const registry = useAllContractList(walletAddress);
const { onSuccess: onAddSuccess, onError: onAddError } = useTxNotifications(
"Successfully imported",
"Failed to import",
);
const isInRegistry =
registry.isFetched &&
registry.data?.find(
(c) =>
contractAddress &&
// compare address...
c.address.toLowerCase() === contractAddress.toLowerCase() &&
// ... and chainId
c.chainId === chain.id,
) &&
registry.isSuccess;
if (
!walletAddress ||
!contractAddress ||
!chain ||
pathname?.includes("payments")
) {
return null;
}
if (isInRegistry) {
// if user is not logged in
if (!teamsAndProjects) {
if (hideCodePageLink) {
return null;
}
@@ -84,54 +71,163 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
}
return (
<Button
className="gap-2"
variant="outline"
onClick={() => {
if (!chain) {
return;
}
trackEvent({
category: TRACKING_CATEGORY,
action: "add-to-dashboard",
label: "attempt",
contractAddress,
});
addContract.mutate(
{
contractAddress,
chainId: chain.id,
},
{
onSuccess: () => {
onAddSuccess();
trackEvent({
category: TRACKING_CATEGORY,
action: "add-to-dashboard",
label: "success",
contractAddress,
});
},
onError: (err) => {
onAddError(err);
trackEvent({
category: TRACKING_CATEGORY,
action: "add-to-dashboard",
label: "error",
contractAddress,
error: err,
});
},
},
);
}}
>
{addContract.isPending ? (
<Spinner className="size-4" />
) : (
<PlusIcon className="size-4" />
)}
Import contract
</Button>
<AddToProjectButton
contractAddress={contractAddress}
teamsAndProjects={teamsAndProjects}
chainId={chain.id.toString()}
client={client}
/>
);
};
function AddToProjectButton(props: {
contractAddress: string;
teamsAndProjects: MinimalTeamsAndProjects;
chainId: string;
client: ThirdwebClient;
}) {
return (
<Dialog>
<DialogTrigger asChild>
<Button className="gap-2" variant="outline">
<PlusIcon className="size-4" />
Add To Project
</Button>
</DialogTrigger>
<DialogContent className="gap-0 overflow-hidden p-0">
<DialogHeader className="p-6">
<DialogTitle>Add To Project</DialogTitle>
<DialogDescription>
Add the contract in a project's contract list on dashboard
</DialogDescription>
</DialogHeader>
<AddToProjectModalContent
teamsAndProjects={props.teamsAndProjects}
chainId={props.chainId}
contractAddress={props.contractAddress}
client={props.client}
/>
</DialogContent>
</Dialog>
);
}
function AddToProjectModalContent(props: {
teamsAndProjects: MinimalTeamsAndProjects;
chainId: string;
contractAddress: string;
client: ThirdwebClient;
}) {
const trackEvent = useTrack();
const addContractToProject = useAddContractToProject();
const [importSelection, setImportSelection] = useState({
team: props.teamsAndProjects[0]?.team,
project: props.teamsAndProjects[0]?.projects[0],
});
const selectedTeam = props.teamsAndProjects.find(
(t) => t.team.id === importSelection.team?.id,
);
function handleImport(params: {
teamId: string;
projectId: string;
}) {
trackEvent({
category: TRACKING_CATEGORY,
action: "add-to-dashboard",
label: "attempt",
contractAddress: props.contractAddress,
});
addContractToProject.mutate(
{
contractAddress: props.contractAddress,
teamId: params.teamId,
projectId: params.projectId,
chainId: props.chainId,
},
{
onSuccess: () => {
toast.success("Contract added to the project successfully");
trackEvent({
category: TRACKING_CATEGORY,
action: "add-to-dashboard",
label: "success",
contractAddress: props.contractAddress,
});
},
onError: (err) => {
if (err.message.includes("PROJECT_CONTRACT_ALREADY_EXISTS")) {
toast.error("Contract is already added to the project");
} else {
toast.error("Failed to import contract");
}
trackEvent({
category: TRACKING_CATEGORY,
action: "add-to-dashboard",
label: "error",
contractAddress: props.contractAddress,
error: err,
});
},
},
);
}
const isImportEnabled = !!importSelection.project && !!importSelection.team;
return (
<div>
<div className="flex flex-col gap-4 border-border border-t px-6 py-6">
<AddToProjectSelector
selection={importSelection}
onSelectionChange={setImportSelection}
teamsAndProjects={props.teamsAndProjects}
client={props.client}
/>
{/* No projects alert */}
{selectedTeam?.projects.length === 0 && (
<Alert variant="info">
<CircleAlertIcon className="size-5" />
<AlertTitle> Selected team has no projects </AlertTitle>
<AlertDescription>
<UnderlineLink
href={`/team/${selectedTeam.team.slug}`}
className="inline-flex items-center gap-2"
target="_blank"
>
Create Project <ExternalLinkIcon className="size-3.5" />
</UnderlineLink>
</AlertDescription>
</Alert>
)}
</div>
<div className="mt-3 flex justify-end border-border border-t bg-card p-6">
<Button
disabled={!isImportEnabled}
className="gap-2"
onClick={() => {
if (!importSelection.team || !importSelection.project) {
return;
}
handleImport({
teamId: importSelection.team.id,
projectId: importSelection.project.id,
});
}}
>
{addContractToProject.isPending ? (
<Spinner className="size-4" />
) : (
<PlusIcon className="size-4" />
)}
Add to Project
</Button>
</div>
</div>
);
}

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