Compare commits

..
Author SHA1 Message Date
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
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
ca6c913361 Version Packages (#6132)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-03 22:41:38 -08:00
MananTank cf166e4c04 [TOOL-3216] Dashboard: Add Faucet Refill, Fix toast colors on light mode (#6147)
<!--

## 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 UI and functionality of the dashboard by updating theme management, enhancing the `FaucetButton` component, and integrating a new modal for refilling the faucet. It also modifies color schemes for buttons and introduces a loading state.

### Detailed summary
- Removed `<Toaster richColors />` from `RootLayout`.
- Added `<ToasterSetup />` component in `AppRouterProviders` for dynamic theming.
- Updated button color schemes in `sdk-component-theme.ts`.
- Enhanced `FaucetButton` with loading states and a new modal for refilling.
- Introduced `SendFundsToFaucetModalButton` and `SendFundsToFaucetModalContent` components.
- Added form validation for the amount in the faucet refill modal.

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

<!-- end pr-codex -->
2025-02-04 01:03:52 +00:00
MananTank a51192ffbd [TOOL-3055] Dashboard: Add Refresh Balances button for engine backend wallets (#6153)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the `BackendWalletsTable` component by improving the balance display functionality, adding a refresh feature, and optimizing some data fetching mechanisms.

### Detailed summary
- Updated `backendWalletBalance` keys in `cache-keys.ts`.
- Introduced `backendWalletBalanceAll` function.
- Added `isFetching` state to balance fetching logic.
- Changed balance skeleton size.
- Replaced `LinkButton` with `Link` for external navigation.
- Added a refresh button with tooltip for balance updates.
- Included `queryClient` in dependencies for memoized columns.

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

<!-- end pr-codex -->
2025-02-04 01:00:27 +00:00
MananTank 6fd36aabc7 [TOOL-3286] Dashboard: Add Feedback button on header, Add more links in mobile burger menu (#6145) 2025-02-03 19:43:05 +00:00
kumaryash90 08cc48910d [SDK] Update implementation addr extraction logic (#6093)
TOOL-3258

<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces support for EIP-7702 by adding functionality to extract the implementation address from a specific bytecode pattern. It includes a new utility function and a corresponding test case.

### Detailed summary
- Added EIP-7702 support in `extractMinimalProxyImplementationAddress.ts` to extract the implementation address from bytecode.
- Implemented a check for bytecode length and prefix.
- Added a test case in `extractMinimalProxyImplementationAddress.test.ts` to validate the new functionality.

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

<!-- end pr-codex -->
2025-02-03 17:56:14 +00:00
MananTank f03ded300f [TOOL-3276] SDK: Fix JSDoc comments of download function (#6133)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on updating the import path for the `createThirdwebClient` function from `@thirdweb-dev/sdk` to `thirdweb` in the `download.ts` file.

### Detailed summary
- Changed import of `createThirdwebClient` from `@thirdweb-dev/sdk` to `thirdweb` in multiple instances within the `download.ts` file.

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

<!-- end pr-codex -->
2025-02-02 03:27:49 +00:00
kumaryash90 b651bae4ba Update addresses (#6141)
TOOL-3279

<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refining type imports in `usageV2.ts`, adding a new address in `implementations.ts`, and simplifying the `package.json` formatting for better readability.

### Detailed summary
- Changed import style in `usageV2.ts` to use `import type`.
- Added a new address entry in `implementations.ts`.
- Reformatted `typesVersions` and `files` sections in `package.json` for improved clarity.

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

<!-- end pr-codex -->
2025-02-02 01:19:51 +00:00
Phillip HoandGitHub 66833a832c fix: usageV2 API endpoint (#6140) 2025-01-31 20:20:58 -08:00
Alec AnanianandGitHub 9d5828eeab [SDK] Fix: skip factory entrypoint lookup for ZKsync chains (#6116) 2025-01-31 17:10:33 -08:00
Jonas DanielsandGitHub b191658fcd update login script URL to HTTPS (#6138) 2025-01-31 16:26:11 -08:00
Jonas DanielsandGitHub a6b7e8d818 update dependencies (#6137) 2025-01-31 16:23:07 -08:00
Phillip HoandGitHub f4f93e6353 [service-utils] ESM imports for compression (#6139) 2025-01-31 16:17:41 -08:00
gregfromstl b693b78645 [SDK] Fix: Switch underlying admin wallet with ecosystem smart account (#6119)
<!--

## 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 functionality of smart wallets in the `thirdweb` ecosystem, ensuring they correctly trigger chain switches and improving related wallet checks and configurations.

### Detailed summary
- Fixed smart wallets to trigger chain switch on admin wallets.
- Updated `smartWalletGetCapabilities` to accept a generic `Wallet` type.
- Implemented `isSmartWallet` function to check for smart wallet status.
- Modified `getSmartWallet` to throw an error for non-smart wallets.
- Refactored wallet checks in `createConnectionManager`.
- Added tests for `isSmartWallet` and `getSmartWallet`.

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

<!-- end pr-codex -->
2025-01-31 22:21:58 +00:00
jnsdls daceeed2ec remove deprecated NFT embed functionality (#6136)
fixes: CORE-720
2025-01-31 22:18:32 +00:00
Jonas DanielsandGitHub 5f7c726917 add JWT signing and redirect for login flow (#6129) 2025-01-31 13:44:37 -08:00
Jonas DanielsandGitHub 2405456e5f add knip dependency to login app (#6135) 2025-01-31 13:03:26 -08:00
Phillip HoandGitHub a76e6534c4 [service-utils] switch to lz4 for compression lib (#6131) 2025-01-31 10:41:53 -08:00
Phillip HoandGitHub 0fb586db52 [service-utils] fix: use lz4js for usageV2 (#6130) 2025-01-31 10:22:48 -08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
f8ebbe54d9 Version Packages (#6118)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-31 09:17:07 -08:00
Phillip HoandGitHub ca756819db [service-utils] CJS import for kafkajs-lz4 (#6127) 2025-01-30 17:29:44 -08:00
Jonas DanielsandGitHub 7217e774b2 update login permissions UI and styling (#6128) 2025-01-30 17:26:07 -08:00
Jonas DanielsandGitHub 5de72c5ecd fix arrow function in login script (#6126) 2025-01-30 17:05:46 -08:00
Jonas DanielsandGitHub 60c5c26583 add thirdweb login client script (#6125) 2025-01-30 16:57:22 -08:00
Jonas DanielsandGitHub dc74c3b4b9 add Suspense wrapper to root layout (#6124) 2025-01-30 16:15:42 -08:00
Jonas DanielsandGitHub 69a921d01f tw:login (#6122) 2025-01-30 16:09:20 -08:00
Phillip HoandGitHub 9abdb92028 [service-utils] import LZ4 compression for usageV2 (#6123) 2025-01-30 15:59:08 -08:00
Phillip HoandGitHub 6ed2fd13de [service-utils] fix: Add LZ4 package for usageV2 compression (#6121) 2025-01-30 14:55:37 -08:00
Phillip HoandGitHub a22457a69c [service-utils] Expose broker settings for usageV2 (#6120) 2025-01-30 12:38:52 -08:00
gregfromstl ae675db8c8 [Wagmi Adapter] Fix: use smart account chain in getProvider (#6117)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on fixing the `getProvider` function in the `wagmi-adapter` package to improve the autoconnection behavior with the account factory by enhancing the chain selection logic.

### Detailed summary
- Updated `getProvider` function to include `args.smartAccount?.chain?.id` in chain selection.
- Changed the logic for defining `chain` to prioritize `params?.chainId`, followed by the smart account's chain ID, the last stored chain ID, and defaulting to `1`.

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

<!-- end pr-codex -->
2025-01-30 19:17:54 +00:00
MananTank 54c9333fc9 [TOOL-3268] Dashboard: Fix in-app wallet settings page form submit not working (#6115) 2025-01-30 17:12:59 +00:00
MananTank 541cf0b3e9 [TOOL-3259] Dashboard: Fix Claimable Module UI crashing on for ERC20 token currency set in claim condition (#6114)
<!--

## 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 `ClaimableModule` component by integrating the `useQuery` hook for fetching currency decimals and refining the logic for determining when to fetch this data.

### Detailed summary
- Added `useQuery` to fetch currency decimals based on the `claimConditionCurrency`.
- Refactored the logic to check if currency decimals should be fetched.
- Updated the conditional rendering of fetched data in the `ClaimableModule`.
- Made the "Add Claim Condition" button disabled based on `props.isOwnerAccount`.

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

<!-- end pr-codex -->
2025-01-30 15:25:38 +00:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
c65022174d Version Packages (#6111)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-29 22:45:20 -08:00
Phillip HoandGitHub 6818cbabf2 chore: change to per-product usageV2 topic (#6113) 2025-01-29 22:32:42 -08:00
gregfromstl a7fb844d87 [Dashboard] Fix: Remove learn more button (#6109)
<!--

## 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 simplifying the `WalletAddress` component by removing the "Learn more" button and its associated link to the blog, while also cleaning up the imports by removing the unused `ExternalLinkIcon`.

### Detailed summary
- Removed the unused import of `ExternalLinkIcon` from `lucide-react`.
- Eliminated the "Learn more" button, which included:
  - The button element with `asChild`, `variant`, `className`, and `size` props.
  - The inner `<a>` element linking to the blog post.

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

<!-- end pr-codex -->
2025-01-30 06:05:07 +00:00
Manan TankandGitHub bf431961ec [TOOL-3260] SDK: Fix Pay Modal logging react-query error when opened (#6104) 2025-01-29 16:59:25 -08:00
gregandGitHub c5d4cab8f8 [Portal] Fix: deployContract doc imports (#6108) 2025-01-29 16:55:21 -08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
1ee0dffec4 Version Packages (#6107)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-29 14:52:28 -08:00
Jonas DanielsandGitHub 33611409b0 update dependencies (#6106) 2025-01-29 14:34:20 -08:00
Joaquim VergesandGitHub ab55aec10c [SDK] feat: Optional wallet activation during connection (#6105) 2025-01-29 14:21:08 -08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
ca22e4659e Version Packages (#6097)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-29 12:18:06 -08:00
gregandGitHub 411895c002 [Wagmi Adapter] Fix: Fallback to smart account chain in autoconnect (#6100) 2025-01-29 11:35:51 -08:00
Jonas DanielsGitHubsaminacodesJoaquim Vergesgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>samina
cfa1c20a91 Saminacodes patch 1 (#6103)
Co-authored-by: saminacodes <[email protected]>
Co-authored-by: Joaquim Verges <[email protected]>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: samina <[email protected]>
2025-01-29 11:34:04 -08:00
Jonas DanielsGitHubsaminaJoaquim Vergesgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>samina
2b16f90715 Open ai docs (#6102)
Co-authored-by: samina <[email protected]>
Co-authored-by: Joaquim Verges <[email protected]>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: samina <[email protected]>
2025-01-29 11:33:18 -08:00
Jonas DanielsGitHubJoaquim Vergesgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
e8892b152f Pass team instead of project to rate limit (#6101)
Co-authored-by: Joaquim Verges <[email protected]>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-29 11:32:13 -08:00
378 changed files with 13339 additions and 6339 deletions
+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
+12 -11
View File
@@ -46,12 +46,12 @@
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tooltip": "1.1.7",
"@sentry/nextjs": "8.51.0",
"@sentry/nextjs": "8.53.0",
"@shazow/whatsabi": "^0.19.0",
"@tanstack/react-query": "5.65.0",
"@tanstack/react-query": "5.66.0",
"@tanstack/react-table": "^8.20.6",
"@thirdweb-dev/service-utils": "workspace:*",
"@vercel/functions": "^1.5.2",
"@vercel/functions": "^1.6.0",
"@vercel/og": "^0.6.5",
"abitype": "1.0.8",
"chakra-react-select": "^4.7.6",
@@ -74,7 +74,7 @@
"nextjs-toploader": "^1.6.12",
"openapi-types": "^12.1.3",
"p-limit": "^6.2.0",
"papaparse": "^5.5.1",
"papaparse": "^5.5.2",
"pluralize": "^8.0.0",
"posthog-js": "1.67.1",
"qrcode": "^1.5.3",
@@ -83,17 +83,18 @@
"react-day-picker": "^8.10.1",
"react-dom": "19.0.0",
"react-dropzone": "^14.3.5",
"react-error-boundary": "^4.1.2",
"react-error-boundary": "^5.0.0",
"react-hook-form": "7.54.2",
"react-markdown": "^9.0.1",
"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.2",
"sonner": "^1.7.4",
"spdx-correct": "^3.2.0",
"swagger-ui-react": "^5.18.2",
"swagger-ui-react": "^5.18.3",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"thirdweb": "workspace:*",
@@ -106,7 +107,7 @@
"@chromatic-com/storybook": "3.2.4",
"@next/bundle-analyzer": "15.1.6",
"@next/eslint-plugin-next": "15.1.6",
"@playwright/test": "1.50.0",
"@playwright/test": "1.50.1",
"@storybook/addon-essentials": "8.5.2",
"@storybook/addon-interactions": "8.5.2",
"@storybook/addon-links": "8.5.2",
@@ -117,7 +118,7 @@
"@storybook/react": "8.5.2",
"@storybook/test": "8.5.2",
"@types/color": "4.2.0",
"@types/node": "22.10.10",
"@types/node": "22.13.0",
"@types/papaparse": "^5.3.15",
"@types/pluralize": "^0.0.33",
"@types/qrcode": "^1.5.5",
@@ -129,12 +130,12 @@
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
"autoprefixer": "^10.4.19",
"checkly": "^4.18.0",
"checkly": "^4.19.1",
"eslint": "8.57.0",
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
"eslint-plugin-storybook": "^0.11.1",
"knip": "5.43.5",
"knip": "5.43.6",
"next-sitemap": "^4.2.3",
"postcss": "8.5.1",
"storybook": "8.5.2",
+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",
@@ -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();
+5
View File
@@ -24,6 +24,7 @@ export type Team = {
image?: string;
billingPlan: "pro" | "growth" | "free" | "starter";
billingStatus: "validPayment" | (string & {}) | null;
supportPlan: "pro" | "growth" | "free" | "starter";
billingEmail: string | null;
growthTrialEligible: false;
enabledScopes: EnabledTeamScope[];
@@ -47,6 +48,10 @@ export async function getTeamBySlug(slug: string) {
return null;
}
export function getTeamById(id: string) {
return getTeamBySlug(id);
}
export async function getTeams() {
const token = await getAuthToken();
if (!token) {
@@ -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;
@@ -8,7 +8,7 @@ import {
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
import { useClipboard } from "hooks/useClipboard";
import { Check, Copy, ExternalLinkIcon } from "lucide-react";
import { Check, Copy } from "lucide-react";
import { useMemo } from "react";
import { type ThirdwebClient, isAddress } from "thirdweb";
import { ZERO_ADDRESS } from "thirdweb";
@@ -148,21 +148,6 @@ export function WalletAddress(props: {
);
})
)}
<Button
asChild
variant="upsell"
className="flex flex-row items-center gap-2 text-sm"
size="sm"
>
<a
target="_blank"
href="https://blog.thirdweb.com/changelog/introducing-the-social-sdk?ref=dashboard-social-wallet"
rel="noreferrer"
>
Learn more
<ExternalLinkIcon className="size-4" />
</a>
</Button>
</div>
</HoverCardContent>
</HoverCard>
@@ -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 && (
@@ -95,7 +95,8 @@ export const engineKeys = {
backendWallet: (address: string, chainId: number) =>
["backendWallet", address, chainId] as const,
backendWalletBalance: (address: string, chainId: number) =>
[...engineKeys.backendWallet(address, chainId), "balance"] as const,
["backendWallet", "balance", address, chainId] as const,
backendWalletBalanceAll: () => ["backendWallet", "balance"] as const,
corsUrls: (instance: string) =>
[...engineKeys.all, instance, "corsUrls"] as const,
ipAllowlist: (instance: string) =>
@@ -124,4 +125,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,
};
@@ -39,6 +39,7 @@ export type Account = {
id: string;
isStaff: boolean;
creatorWalletAddress: string;
image?: string | null;
name?: string;
email?: string;
advancedEnabled: boolean;
@@ -548,41 +549,6 @@ export function useResendEmailConfirmation() {
});
}
export function useApiKeys(props: {
isLoggedIn: boolean;
}) {
const address = useActiveAccount()?.address;
return useQuery({
queryKey: apiKeys.keys(address || ""),
queryFn: async () => {
type Result = {
data: ApiKey[];
error?: { message: string };
};
const res = await apiServerProxy<Result>({
pathname: "/v1/keys",
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
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: !!address && props.isLoggedIn,
});
}
export function useCreateApiKey() {
const address = useActiveAccount()?.address;
const queryClient = useQueryClient();
@@ -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,13 +1,32 @@
"use client";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
THIRDWEB_ENGINE_FAUCET_WALLET,
TURNSTILE_SITE_KEY,
} from "@/constants/env";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { zodResolver } from "@hookform/resolvers/zod";
import { Turnstile } from "@marsidev/react-turnstile";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
@@ -17,9 +36,21 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { toUnits } from "thirdweb";
import {
type ThirdwebClient,
prepareTransaction,
toUnits,
toWei,
} from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { useActiveAccount, useWalletBalance } from "thirdweb/react";
import type { Chain } from "thirdweb/chains";
import {
useActiveAccount,
useActiveWalletChain,
useSendTransaction,
useSwitchActiveWalletChain,
useWalletBalance,
} from "thirdweb/react";
import { z } from "zod";
import { isOnboardingComplete } from "../../../../../../login/onboarding/isOnboardingRequired";
@@ -137,6 +168,35 @@ export function FaucetButton({
const form = useForm<z.infer<typeof claimFaucetSchema>>();
// loading state
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
return (
<Button variant="outline" className="w-full gap-2">
Checking Faucet <Spinner className="size-3" />
</Button>
);
}
// faucet is empty
if (isFaucetEmpty) {
return (
<div className="w-full">
<div className="mb-3 text-center text-muted-foreground text-sm">
Faucet is empty right now
</div>
<SendFundsToFaucetModalButton
chain={definedChain}
isLoggedIn={!!twAccount}
client={client}
chainMeta={chain}
onFaucetRefill={() => {
faucetWalletBalanceQuery.refetch();
}}
/>
</div>
);
}
// Force users to log in to claim the faucet
if (!address || !twAccount) {
return (
@@ -164,24 +224,6 @@ export function FaucetButton({
);
}
// loading state
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
return (
<Button variant="outline" className="w-full gap-2">
Checking Faucet <Spinner className="size-3" />
</Button>
);
}
// faucet is empty
if (isFaucetEmpty) {
return (
<Button variant="outline" disabled className="!opacity-100 w-full">
Faucet is empty right now
</Button>
);
}
// Can not claim
if (canClaimFaucetQuery.data && canClaimFaucetQuery.data.canClaim === false) {
return (
@@ -250,3 +292,165 @@ export function FaucetButton({
</div>
);
}
const faucetFormSchema = z.object({
amount: z.coerce.number().refine((value) => value > 0, {
message: "Amount must be greater than 0",
}),
});
function SendFundsToFaucetModalButton(props: {
chain: Chain;
isLoggedIn: boolean;
client: ThirdwebClient;
chainMeta: ChainMetadata;
onFaucetRefill: () => void;
}) {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="default" className="w-full">
Refill Faucet
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="mb-2">
<DialogTitle>Refill Faucet</DialogTitle>
<DialogDescription>Send funds to faucet wallet</DialogDescription>
</DialogHeader>
<SendFundsToFaucetModalContent {...props} />
</DialogContent>
</Dialog>
);
}
function SendFundsToFaucetModalContent(props: {
chain: Chain;
isLoggedIn: boolean;
client: ThirdwebClient;
chainMeta: ChainMetadata;
onFaucetRefill: () => void;
}) {
const account = useActiveAccount();
const activeChain = useActiveWalletChain();
const switchActiveWalletChain = useSwitchActiveWalletChain();
const sendTxMutation = useSendTransaction({
payModal: false,
});
const switchChainMutation = useMutation({
mutationFn: async () => {
await switchActiveWalletChain(props.chain);
},
});
const form = useForm<z.infer<typeof faucetFormSchema>>({
resolver: zodResolver(faucetFormSchema),
defaultValues: {
amount: 0.1,
},
});
function onSubmit(values: z.infer<typeof faucetFormSchema>) {
const sendNativeTokenTx = prepareTransaction({
chain: props.chain,
client: props.client,
to: THIRDWEB_ENGINE_FAUCET_WALLET,
value: toWei(values.amount.toString()),
});
const promise = sendTxMutation.mutateAsync(sendNativeTokenTx);
toast.promise(promise, {
success: `Sent ${values.amount} ${props.chainMeta.nativeCurrency.symbol} to faucet`,
error: `Failed to send ${values.amount} ${props.chainMeta.nativeCurrency.symbol} to faucet`,
});
promise.then(() => {
props.onFaucetRefill();
});
}
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex min-w-0 flex-col gap-5"
>
<div className="min-w-0">
<p className="mb-2 text-foreground text-sm"> Faucet Wallet </p>
<CopyTextButton
copyIconPosition="right"
variant="outline"
className="w-full justify-between bg-card py-2 font-mono"
textToCopy={THIRDWEB_ENGINE_FAUCET_WALLET}
textToShow={THIRDWEB_ENGINE_FAUCET_WALLET}
tooltip={undefined}
/>
</div>
<FormField
control={form.control}
name="amount"
render={({ field }) => (
<FormItem>
<FormLabel>Amount</FormLabel>
<FormControl>
<div className="relative">
<Input
{...field}
type="number"
className="h-auto bg-card text-2xl md:text-2xl [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
/>
<div className="-translate-y-1/2 absolute top-1/2 right-4 text-muted-foreground text-sm">
{props.chainMeta.nativeCurrency.symbol}
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{!account && (
<CustomConnectWallet
chain={props.chain}
loginRequired={false}
isLoggedIn={props.isLoggedIn}
connectButtonClassName="!w-full"
detailsButtonClassName="!w-full"
/>
)}
{account && activeChain && (
<div>
{activeChain.id === props.chain.id ? (
<Button
key="submit"
type="submit"
className="mt-4 w-full gap-2"
disabled={sendTxMutation.isPending}
>
{sendTxMutation.isPending && <Spinner className="size-4" />}
Send funds to faucet
</Button>
) : (
<Button
key="switch"
className="mt-4 w-full gap-2"
disabled={switchChainMutation.isPending}
onClick={() => switchChainMutation.mutate()}
>
Switch to {props.chainMeta.name}{" "}
{switchChainMutation.isPending && (
<Spinner className="size-4" />
)}
</Button>
)}
</div>
)}
</form>
</Form>
);
}
@@ -39,12 +39,6 @@ export function getContractPageSidebarLinks(data: {
href: `${layoutPrefix}/events`,
exactMatch: true,
},
{
label: "Embed",
href: `${layoutPrefix}/embed`,
hide: data.metadata.embedType === null,
exactMatch: true,
},
{
label: "Analytics",
href: `${layoutPrefix}/analytics`,
@@ -1,35 +0,0 @@
"use client";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { EmbedSetup } from "./embed-setup";
export function EmbedSetupClient(props: {
contract: ThirdwebContract;
twAccount: Account | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (metadataQuery.data.embedType === null) {
return <RedirectToContractOverview contract={props.contract} />;
}
return (
<EmbedSetup
contract={props.contract}
ercOrMarketplace={metadataQuery.data.embedType}
twAccount={props.twAccount}
/>
);
}
@@ -1,613 +0,0 @@
"use client";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { CodeClient } from "@/components/ui/code/code.client";
import {
type Account,
useApiKeys,
useCreateApiKey,
} from "@3rdweb-sdk/react/hooks/useApi";
import { Flex, FormControl, Input, Select } from "@chakra-ui/react";
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
import { useTrack } from "hooks/analytics/useTrack";
import { useAllChainsData } from "hooks/chains/allChains";
import { AlertCircleIcon } from "lucide-react";
import { useTheme } from "next-themes";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import type { StoredChain } from "stores/chainStores";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { useActiveAccount } from "thirdweb/react";
import { Card, FormHelperText, FormLabel, Heading, Text } from "tw-components";
interface EmbedSetupProps {
contract: ThirdwebContract;
ercOrMarketplace: string;
twAccount: Account | undefined;
}
// MAKE SURE THIS IS v1 embed hashes!!
const IPFS_URI = "bafybeigdie2yyiazou7grjowoevmuip6akk33nqb55vrpezqdwfssrxyfy";
const ERC721_IPFS_URI =
"bafybeicd3qfzelz4su7ng6n523virdsgobrc5pcbarhwqv3dj3drh645pi";
interface IframeSrcOptions {
clientId: string;
chain: string;
tokenId?: string;
listingId?: string;
listingType?: string;
directListingId?: string;
englishAuctionId?: string;
relayUrl?: string;
theme?: string;
primaryColor?: string;
secondaryColor?: string;
biconomyApiKey?: string;
biconomyApiId?: string;
}
const colorOptions = [
"purple",
"blue",
"orange",
"pink",
"green",
"red",
"teal",
"cyan",
"yellow",
];
const isValidUrl = (url: string | undefined) => {
if (!url) {
return false;
}
try {
new URL(url);
return true;
} catch {
return false;
}
};
const buildIframeSrc = (
contract: ThirdwebContract,
ercOrMarketplace?: string,
options?: IframeSrcOptions,
): string => {
const contractPath =
ercOrMarketplace === "erc721" ? "" : `${ercOrMarketplace}.html`;
const contractEmbedHash =
ercOrMarketplace === "erc721" ? ERC721_IPFS_URI : IPFS_URI;
if (!contract || !options || !contractEmbedHash) {
return "";
}
const {
clientId,
chain,
tokenId,
listingId,
listingType,
directListingId,
englishAuctionId,
relayUrl,
theme,
primaryColor,
secondaryColor,
biconomyApiKey,
biconomyApiId,
} = options;
const url = new URL(
`https://embed.ipfscdn.io/ipfs/${contractEmbedHash}/${contractPath}`,
);
url.searchParams.append("contract", contract.address);
url.searchParams.append("chain", chain);
url.searchParams.append("clientId", clientId);
if (tokenId !== undefined && ercOrMarketplace === "erc1155") {
url.searchParams.append("tokenId", tokenId.toString());
}
if (listingId !== undefined && ercOrMarketplace === "marketplace") {
url.searchParams.append("listingId", listingId.toString());
}
if (
directListingId !== undefined &&
ercOrMarketplace === "marketplace-v3" &&
listingType === "direct-listing"
) {
url.searchParams.append("directListingId", directListingId.toString());
}
if (
englishAuctionId !== undefined &&
ercOrMarketplace === "marketplace-v3" &&
listingType === "english-auction"
) {
url.searchParams.append("englishAuctionId", englishAuctionId.toString());
}
if (isValidUrl(relayUrl)) {
url.searchParams.append("relayUrl", relayUrl || "");
}
if (biconomyApiKey) {
url.searchParams.append("biconomyApiKey", biconomyApiKey);
}
if (biconomyApiId) {
url.searchParams.append("biconomyApiId", biconomyApiId);
}
if (theme) {
url.searchParams.append("theme", theme);
}
if (primaryColor && primaryColor !== "default") {
url.searchParams.append("primaryColor", primaryColor);
}
if (secondaryColor && secondaryColor !== "orange") {
url.searchParams.append("secondaryColor", secondaryColor);
}
return url.toString();
};
// taken from @thirdweb-dev /chains
// @internal
function minimizeChain(
chain: ChainMetadata,
): Pick<
ChainMetadata,
| "name"
| "chain"
| "rpc"
| "nativeCurrency"
| "shortName"
| "chainId"
| "testnet"
| "slug"
| "icon"
> {
const [firstRpc] = chain.rpc;
return {
name: chain.name,
chain: chain.chain,
rpc: [firstRpc || ""],
nativeCurrency: chain.nativeCurrency,
shortName: chain.shortName,
chainId: chain.chainId,
testnet: chain.testnet,
slug: chain.slug,
icon: chain.icon,
};
}
// taken from @thirdweb-dev /chains
// @internal
type ChainConfiguration = {
rpc?: string | string[];
};
function configureChain(
chain: ChainMetadata,
chainConfig: ChainConfiguration,
): ChainMetadata {
let additionalRPCs: string[] = [];
if (chainConfig?.rpc) {
if (typeof chainConfig.rpc === "string") {
additionalRPCs = [chainConfig.rpc];
} else {
additionalRPCs = chainConfig.rpc;
}
}
// prepend additional RPCs to the chain's RPCs
return { ...chain, rpc: [...additionalRPCs, ...chain.rpc] };
}
export const EmbedSetup: React.FC<EmbedSetupProps> = ({
contract,
ercOrMarketplace,
twAccount,
}) => {
const trackEvent = useTrack();
const { theme } = useTheme();
const apiKeys = useApiKeys({
isLoggedIn: !!twAccount,
});
const createKeyMutation = useCreateApiKey();
const validApiKey = (apiKeys.data || []).find(
(apiKey) =>
(apiKey.domains.includes("*") ||
apiKey.domains.includes("embed.ipfscdn.io") ||
apiKey.domains.includes("*.ipfscdn.io")) &&
(apiKey.services || [])
.find((service) => service.name === "storage")
?.actions.includes("read") &&
!!(apiKey.services || []).find((service) => service.name === "rpc"),
);
const chainId = contract.chain.id;
const { idToChain } = useAllChainsData();
const chain: StoredChain = (chainId ? idToChain.get(chainId) : undefined) || {
name: "Unknown Chain",
chainId: chainId || -1,
rpc: [],
chain: "unknown",
nativeCurrency: {
name: "Ethereum",
symbol: "ETH",
decimals: 18,
},
shortName: "unknown",
slug: "unknown",
testnet: false,
stackType: "",
};
const { register, watch } = useForm<{
rpcUrl: string;
relayUrl: string;
tokenId: string;
listingId: string;
listingType: string;
directListingId: string;
englishAuctionId: string;
theme: string;
primaryColor: string;
secondaryColor: string;
biconomyApiKey: string;
biconomyApiId: string;
gasless: string;
}>({
defaultValues: {
rpcUrl: chain?.rpc[0],
tokenId: "0",
listingId: "0",
directListingId: "0",
englishAuctionId: "0",
theme: theme === "light" ? "light" : "dark",
primaryColor: "purple",
secondaryColor: "orange",
},
reValidateMode: "onChange",
});
const configuredChainWithNewRpc = configureChain(chain, {
rpc: watch("rpcUrl"),
});
const minimizedChain = minimizeChain(configuredChainWithNewRpc);
const iframeSrc = buildIframeSrc(contract, ercOrMarketplace, {
chain: JSON.stringify(minimizedChain),
clientId: validApiKey?.key || "",
...watch(),
});
const embedCode = useMemo(
() =>
`<iframe
src="${iframeSrc}"
width="100%"
height="750px"
style="max-width:100%;"
frameborder="0"
></iframe>`,
[iframeSrc],
);
const [showCreateAPIKeyModal, setShowCreateAPIKeyModal] = useState(false);
const activeAccount = useActiveAccount();
const pathname = usePathname();
return (
<div>
<LazyCreateAPIKeyDialog
prefill={{
name: "Embed API Key",
domains: "embed.ipfscdn.io",
}}
open={showCreateAPIKeyModal}
onOpenChange={setShowCreateAPIKeyModal}
onCreateAndComplete={() => {
trackEvent({
category: "api-keys",
action: "create",
label: "success",
fromEmbed: true,
});
apiKeys.refetch();
}}
enableNebulaServiceByDefault={false}
teamSlug={undefined}
/>
<Alert variant="warning">
<AlertCircleIcon className="size-5" />
<AlertTitle>Deprecated</AlertTitle>
<AlertDescription className="leading-relaxed">
<span>
thirdweb NFT Embeds are deprecated and not actively maintained
</span>{" "}
<br />
<span>
Use the{" "}
<Link
href="https://github.com/thirdweb-example/nft-minting-template"
target="_blank"
className="underline underline-offset-2 hover:text-foreground"
>
{" "}
NFT Minting Template
</Link>{" "}
or build a custom NFT minting app using{" "}
<Link
href="https://portal.thirdweb.com/react/v5"
className="underline underline-offset-2 hover:text-foreground"
>
thirdweb React SDK components
</Link>{" "}
instead
</span>
</AlertDescription>
</Alert>
<div className="h-8" />
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div>
<h2 className="mb-3 font-semibold text-xl tracking-tight">
Configuration
</h2>
<Card className="flex w-full flex-col gap-5 bg-card">
{ercOrMarketplace === "marketplace" ? (
<FormControl>
<FormLabel>Listing ID</FormLabel>
<Input type="number" {...register("listingId")} />
<FormHelperText>
The listing ID the embed should display
</FormHelperText>
</FormControl>
) : null}
{ercOrMarketplace === "marketplace-v3" ? (
<FormControl>
<FormLabel>Listing type</FormLabel>
<Select {...register("listingType")}>
<option value="direct-listing">Direct Listing</option>
<option value="english-auction">English Auction</option>
</Select>
<FormHelperText>
The type of listing the embed should display
</FormHelperText>
</FormControl>
) : null}
{ercOrMarketplace === "marketplace-v3" &&
watch("listingType") === "direct-listing" ? (
<FormControl>
<FormLabel>Direct Listing ID</FormLabel>
<Input type="number" {...register("directListingId")} />
<FormHelperText>
The direct listing ID the embed should display
</FormHelperText>
</FormControl>
) : null}
{ercOrMarketplace === "marketplace-v3" &&
watch("listingType") === "english-auction" ? (
<FormControl>
<FormLabel>English Auction ID</FormLabel>
<Input type="number" {...register("englishAuctionId")} />
<FormHelperText>
The english auction ID the embed should display
</FormHelperText>
</FormControl>
) : null}
{ercOrMarketplace === "erc1155" ? (
<FormControl>
<FormLabel>Token ID</FormLabel>
<Input type="number" {...register("tokenId")} />
<FormHelperText>
The token ID the embed should display
</FormHelperText>
</FormControl>
) : null}
<FormControl>
<FormLabel>Client ID</FormLabel>
{!activeAccount ? (
<Button asChild className="w-full">
<Link
href={`/login${
pathname ? `?next=${encodeURIComponent(pathname)}` : ""
}`}
>
Sign in to create a client ID
</Link>
</Button>
) : validApiKey ? (
<Input
readOnly
disabled
value={`${validApiKey?.name} - ${validApiKey?.key}`}
/>
) : (
<Button
className="w-full"
onClick={() => {
trackEvent({
category: "api-keys",
action: "create",
label: "attempt",
fromEmbed: true,
});
setShowCreateAPIKeyModal(true);
}}
disabled={createKeyMutation.isPending}
>
Create Client ID
</Button>
)}
<FormHelperText>
You need a client ID to use embeds.{" "}
<Link
href="https://portal.thirdweb.com/account/api-keys"
className="text-link-foreground hover:text-foreground"
target="_blank"
>
Learn more
</Link>
.
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>RPC Url</FormLabel>
<Input type="url" {...register("rpcUrl")} />
<FormHelperText>
RPC the embed should use to connect to the blockchain.
</FormHelperText>
</FormControl>
{ercOrMarketplace === "marketplace" ||
ercOrMarketplace === "marketplace-v3" ? null : (
<FormControl gap={4}>
<Heading size="title.sm" mb={2}>
Gasless
</Heading>
<Select {...register("gasless")} mb={4}>
<option value="false">Disabled</option>
<option value="openZeppelin">OpenZeppelin Relayer</option>
<option value="biconomy">Biconomy Relayer</option>
</Select>
{watch("gasless") === "openZeppelin" && (
<FormControl>
<FormLabel>OpenZeppelin Relayer URL</FormLabel>
<Input type="url" {...register("relayUrl")} />
</FormControl>
)}
{watch("gasless") === "biconomy" && (
<Flex gap={4} flexDir="column">
<FormControl>
<FormLabel>Biconomy API key</FormLabel>
<Input type="url" {...register("biconomyApiKey")} />
</FormControl>
<FormControl>
<FormLabel>Biconomy API ID</FormLabel>
<Input type="url" {...register("biconomyApiId")} />
</FormControl>
</Flex>
)}
<FormHelperText>
A relayer can be used to make the transaction gasless for the
end user.{" "}
<Link
target="_blank"
className="text-link-foreground hover:text-foreground"
href="https://blog.thirdweb.com/guides/setup-gasless-transactions"
>
Learn more
</Link>
</FormHelperText>
</FormControl>
)}
<FormControl>
<FormLabel>Theme</FormLabel>
<Select {...register("theme")}>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">User system</option>
</Select>
<FormHelperText>
Selecting system will make it so the embed would change
depending on the user system&apos;s preferences.
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>Primary Color</FormLabel>
<Select {...register("primaryColor")}>
{ercOrMarketplace === "erc721" && (
<option value="default">Default</option>
)}
{colorOptions.map((color) => (
<option key={color} value={color}>
{color[0]?.toUpperCase() + color.substring(1)}
</option>
))}
</Select>
<FormHelperText>
Used for the main actions button backgrounds.
</FormHelperText>
</FormControl>
{ercOrMarketplace === "marketplace" ||
ercOrMarketplace === "marketplace-v3" ? (
<FormControl>
<FormLabel>Secondary Color</FormLabel>
<Select {...register("secondaryColor")}>
{colorOptions.map((color) => (
<option key={color} value={color}>
{color[0]?.toUpperCase() + color.substring(1)}
</option>
))}
</Select>
<FormHelperText>
Use for secondary actions (like when the user is connected to
the wrong network)
</FormHelperText>
</FormControl>
) : null}
</Card>
</div>
<div>
<h2 className="mb-3 font-semibold text-xl tracking-tight">
Embed Code
</h2>
<CodeClient
code={embedCode}
lang="html"
onCopy={() => {
trackEvent({
category: "embed",
action: "click",
label: "copy-code",
address: contract.address,
chainId,
});
}}
/>
</div>
</div>
<div className="h-8" />
<h2 className="mb-3 font-semibold text-xl tracking-tight">Preview</h2>
<div className="overflow-hidden rounded-lg border border-border">
{!validApiKey ? (
<Text>You need to create a client ID to use embeds</Text>
) : iframeSrc ? (
<iframe
title="thirdweb embed"
src={iframeSrc}
width="100%"
height="750px"
frameBorder="0"
/>
) : null}
</div>
</div>
);
};
@@ -1,42 +0,0 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getRawAccount } from "../../../../../account/settings/getAccount";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { EmbedSetupClient } from "./EmbedSetup.client";
import { EmbedSetup } from "./embed-setup";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const account = await getRawAccount();
const { contract } = info;
if (contract.chain.id === localhost.id) {
return <EmbedSetupClient contract={contract} twAccount={account} />;
}
const { embedType } = await getContractPageMetadata(contract);
if (embedType === null) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
}
return (
<EmbedSetup
contract={contract}
ercOrMarketplace={embedType}
twAccount={account}
/>
);
}
@@ -26,6 +26,7 @@ import { ToolTipLabel } from "@/components/ui/tooltip";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { TransactionButton } from "components/buttons/TransactionButton";
import { addDays, fromUnixTime } from "date-fns";
import { useAllChainsData } from "hooks/chains/allChains";
@@ -119,22 +120,39 @@ function ClaimableModule(props: ModuleInstanceProps) {
claimConditionQuery.data?.startTimestamp === 0 &&
claimConditionQuery.data?.endTimestamp === 0;
const currencyContract = getContract({
address: claimConditionQuery.data?.currency || "",
chain: props.contract.chain,
client: props.contract.client,
});
const claimConditionCurrency = claimConditionQuery.data?.currency;
const shouldFetchTokenDecimals =
claimConditionQuery.data &&
claimConditionQuery.data?.currency !== ZERO_ADDRESS;
const shouldFetchCurrencyDecimals =
!!claimConditionCurrency && claimConditionCurrency !== ZERO_ADDRESS;
const currencyDecimalsQuery = useReadContract(decimals, {
contract: currencyContract,
queryOptions: {
enabled: shouldFetchTokenDecimals,
const currencyDecimalsQuery = useQuery({
queryKey: [
"currency-decimals",
{
contractAddress: props.contract.address,
chainId: props.contract.chain.id,
claimConditionCurrency,
},
],
queryFn: async () => {
if (!claimConditionCurrency) {
throw new Error();
}
const currencyContract = getContract({
address: claimConditionCurrency,
chain: props.contract.chain,
client: props.contract.client,
});
const decimalsVal = await decimals({
contract: currencyContract,
});
return decimalsVal;
},
enabled: shouldFetchCurrencyDecimals,
});
const tokenDecimalsQuery = useReadContract(decimals, {
contract: contract,
queryOptions: {
@@ -265,7 +283,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
// claim conditions data is present
claimConditionQuery.data &&
// token decimals is fetched if it should be fetched
(shouldFetchTokenDecimals ? currencyDecimalsQuery.isFetched : true)
(shouldFetchCurrencyDecimals ? currencyDecimalsQuery.isFetched : true)
? {
claimCondition: claimConditionQuery.data,
currencyDecimals: currencyDecimalsQuery.data,
@@ -276,7 +294,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
tokenId,
isLoading:
claimConditionQuery.isLoading ||
(!!shouldFetchTokenDecimals && currencyDecimalsQuery.isLoading),
(!!shouldFetchCurrencyDecimals && currencyDecimalsQuery.isLoading),
}}
isOwnerAccount={!!ownerAccount}
name={props.contractInfo.name}
@@ -565,6 +583,7 @@ function ClaimConditionSection(props: {
onClick={() => setAddClaimConditionButtonClicked(true)}
variant="outline"
className="w-full"
disabled={!props.isOwnerAccount}
>
Add Claim Condition
</Button>
@@ -1,6 +1,10 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { fetchPublishedContractVersions } from "components/contract-components/fetch-contracts-with-versions";
import {
fetchLatestPublishedContractVersion,
fetchPublishedContractVersions,
} from "components/contract-components/fetch-contracts-with-versions";
import { isAddress } from "thirdweb";
import type { FetchDeployMetadataResult } from "thirdweb/contract";
import { resolveAddress } from "thirdweb/extensions/ens";
import invariant from "tiny-invariant";
import type { ModuleMeta } from "./install-module-params";
@@ -14,16 +18,24 @@ export async function getModuleInstalledParams(ext: ModuleMeta) {
client: getThirdwebClient(),
name: ext.publisherAddress,
});
const allPublishedModules = await fetchPublishedContractVersions(
publisherAddress,
ext.moduleName,
);
// find the version we want
const publishedModule =
ext.moduleVersion === "latest"
? allPublishedModules[0]
: allPublishedModules.find((v) => v.version === ext.moduleVersion);
let publishedModule: FetchDeployMetadataResult | undefined = undefined;
if (ext.moduleVersion === "latest") {
publishedModule = await fetchLatestPublishedContractVersion(
publisherAddress,
ext.moduleName,
);
} else {
const allPublishedModules = await fetchPublishedContractVersions(
publisherAddress,
ext.moduleName,
);
publishedModule = allPublishedModules.find(
(v) => v.version === ext.moduleVersion,
);
}
invariant(
publishedModule,
@@ -25,6 +25,7 @@ export function ContractSettingsPageClient(props: {
contract={props.contract}
functionSelectors={metadataQuery.data.functionSelectors}
twAccount={props.twAccount}
hasDefaultFeeConfig={true}
/>
);
}
@@ -79,8 +79,9 @@ export function ContractSettingsPage(props: {
contract: ThirdwebContract;
functionSelectors: string[];
twAccount: Account | undefined;
hasDefaultFeeConfig: boolean;
}) {
const { functionSelectors, contract, twAccount } = props;
const { functionSelectors, contract, twAccount, hasDefaultFeeConfig } = props;
return (
<ContractSettingsPageInner
contract={contract}
@@ -96,10 +97,13 @@ export function ContractSettingsPage(props: {
CommonExt.isGetDefaultRoyaltyInfoSupported(functionSelectors),
CommonExt.isSetDefaultRoyaltyInfoSupported(functionSelectors),
].every(Boolean)}
isPlatformFeesSupported={[
CommonExt.isGetPlatformFeeInfoSupported(functionSelectors),
CommonExt.isSetPlatformFeeInfoSupported(functionSelectors),
].every(Boolean)}
isPlatformFeesSupported={
!hasDefaultFeeConfig &&
[
CommonExt.isGetPlatformFeeInfoSupported(functionSelectors),
CommonExt.isSetPlatformFeeInfoSupported(functionSelectors),
].every(Boolean)
}
twAccount={twAccount}
/>
);
@@ -132,9 +132,8 @@ export const SettingsPlatformFees = ({
<SolidityInput
solidityType="address"
formContext={form}
variant="filled"
{...form.register("platform_fee_recipient")}
isDisabled={!address || sendAndConfirmTx.isPending}
disabled={!address || sendAndConfirmTx.isPending}
/>
<FormErrorMessage>
{
@@ -155,7 +154,6 @@ export const SettingsPlatformFees = ({
>
<FormLabel>Percentage</FormLabel>
<BasisPointsInput
variant="filled"
value={form.watch("platform_fee_basis_points")}
onChange={(value) =>
form.setValue("platform_fee_basis_points", value, {
@@ -163,7 +161,7 @@ export const SettingsPlatformFees = ({
shouldTouch: true,
})
}
isDisabled={sendAndConfirmTx.isPending}
disabled={sendAndConfirmTx.isPending}
/>
<FormErrorMessage>
{
@@ -128,10 +128,9 @@ export const SettingsPrimarySale = ({
>
<FormLabel>Recipient Address</FormLabel>
<SolidityInput
isDisabled={mutation.isPending || !address}
disabled={mutation.isPending || !address}
solidityType="address"
formContext={form}
variant="filled"
{...form.register("primary_sale_recipient")}
/>
<FormErrorMessage>
@@ -144,9 +144,8 @@ export const SettingsRoyalties = ({
<SolidityInput
solidityType="address"
formContext={form}
variant="filled"
{...form.register("fee_recipient")}
isDisabled={!address}
disabled={!address}
/>
<FormErrorMessage>
{
@@ -165,7 +164,6 @@ export const SettingsRoyalties = ({
>
<FormLabel>Percentage</FormLabel>
<BasisPointsInput
variant="filled"
value={form.watch("seller_fee_basis_points")}
onChange={(value) =>
form.setValue("seller_fee_basis_points", value, {
@@ -1,5 +1,7 @@
import { DEFAULT_FEE_RECIPIENT } from "constants/addresses";
import { notFound } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getPlatformFeeInfo } from "thirdweb/extensions/common";
import { getRawAccount } from "../../../../../account/settings/getAccount";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
@@ -33,11 +35,19 @@ export default async function Page(props: {
getContractPageMetadata(info.contract),
]);
let hasDefaultFeeConfig = true;
try {
const feeInfo = await getPlatformFeeInfo({ contract });
hasDefaultFeeConfig =
feeInfo[0].toLowerCase() === DEFAULT_FEE_RECIPIENT.toLowerCase();
} catch {}
return (
<ContractSettingsPage
contract={info.contract}
functionSelectors={metadata.functionSelectors}
twAccount={account}
hasDefaultFeeConfig={hasDefaultFeeConfig}
/>
);
}
@@ -166,7 +166,7 @@ export async function ChainsData(props: {
) : props.activeView === "table" ? (
<TableContainer>
<Table>
<TableHeader>
<TableHeader className="z-0">
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Chain ID</TableHead>
@@ -83,14 +83,14 @@ export async function ChainListRow({
<TableCell>
<div className="flex w-[520px] flex-row items-center gap-14 ">
<div className="z-10 flex items-center gap-7">
<div className="z-10 flex items-center gap-4">
{products.map((p) => {
return (
<ProductIcon
key={p.name}
icon={p.icon}
label={p.name}
href={pidToHref(p.id)}
href={p.link}
isEnabled={enabledServices.includes(p.id)}
/>
);
@@ -102,32 +102,6 @@ export async function ChainListRow({
);
}
function pidToHref(pid: (typeof products)[number]["id"]) {
switch (pid) {
case "account-abstraction": {
return "https://portal.thirdweb.com/connect/account-abstraction/overview";
}
case "contracts": {
return "https://portal.thirdweb.com/contracts";
}
case "connect-sdk": {
return "https://portal.thirdweb.com/connect";
}
case "engine": {
return "https://portal.thirdweb.com/engine";
}
case "pay": {
return "https://portal.thirdweb.com/connect/pay/overview";
}
case "rpc-edge": {
return "https://portal.thirdweb.com/infrastructure/rpc-edge/overview";
}
case "insight": {
return "https://portal.thirdweb.com/insight";
}
}
}
function ProductIcon(props: {
icon: React.FC<{ className?: string }>;
label: string;
@@ -148,8 +122,15 @@ function ProductIcon(props: {
<Link
href={props.href}
target={props.href.startsWith("http") ? "_blank" : undefined}
className="group rounded-lg p-2 hover:bg-accent"
>
<props.icon className={cn("size-8", !props.isEnabled && "grayscale")} />
<props.icon
className={cn(
"size-6 text-foreground group-hover:text-link-foreground",
!props.isEnabled &&
"text-muted-foreground opacity-50 group-hover:opacity-100",
)}
/>
</Link>
</ToolTipLabel>
);
@@ -106,9 +106,8 @@ export function StarButton(props: {
) : (
<Star
className={cn(
"size-6 transition-all",
"size-6 text-foreground transition-all",
props.iconClassName,
isPreferred ? "text-yellow-400" : "text-foreground",
)}
fill={isPreferred ? "currentColor" : "none"}
strokeWidth="1px"
@@ -7,85 +7,15 @@ export function ConnectSDKIcon(props: { className?: string }) {
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={props.className}
role="presentation"
>
<title>Connect SDK</title>
<rect
x="3"
y="17.9988"
width="14.9988"
height="10.6499"
rx="2"
transform="rotate(-90 3 17.9988)"
fill="url(#paint0_linear_195_4047)"
/>
<g filter="url(#filter0_bi_195_4047)">
<path
d="M3 9C3 7.89543 3.89543 7 5 7H19C20.1046 7 21 7.89543 21 9V19C21 20.1046 20.1046 21 19 21H5C3.89543 21 3 20.1046 3 19V9Z"
fill="#B9DDFF"
fillOpacity="0.8"
/>
</g>
<path
d="M18.3004 12.6995V12.6995C17.5822 13.4177 17.5822 14.5822 18.3004 15.3004V15.3004"
stroke="white"
strokeWidth="1.5"
d="M16 8.00007V4.50073C16 3.66899 16 3.25312 15.8248 2.99755C15.6717 2.77425 15.4346 2.62257 15.1678 2.57715C14.8623 2.52517 14.4847 2.69944 13.7295 3.04799L4.85901 7.14206C4.18551 7.45291 3.84875 7.60834 3.60211 7.84939C3.38406 8.06249 3.21762 8.32262 3.1155 8.6099C3 8.93486 3 9.30575 3 10.0475V15.0001M16.5 14.5001H16.51M3 11.2001L3 17.8001C3 18.9202 3 19.4802 3.21799 19.9081C3.40973 20.2844 3.71569 20.5903 4.09202 20.7821C4.51984 21.0001 5.07989 21.0001 6.2 21.0001H17.8C18.9201 21.0001 19.4802 21.0001 19.908 20.7821C20.2843 20.5903 20.5903 20.2844 20.782 19.9081C21 19.4802 21 18.9202 21 17.8001V11.2001C21 10.08 21 9.51992 20.782 9.09209C20.5903 8.71577 20.2843 8.40981 19.908 8.21806C19.4802 8.00007 18.9201 8.00007 17.8 8.00007L6.2 8.00007C5.0799 8.00007 4.51984 8.00007 4.09202 8.21806C3.7157 8.40981 3.40973 8.71577 3.21799 9.09209C3 9.51991 3 10.08 3 11.2001ZM17 14.5001C17 14.7762 16.7761 15.0001 16.5 15.0001C16.2239 15.0001 16 14.7762 16 14.5001C16 14.2239 16.2239 14.0001 16.5 14.0001C16.7761 14.0001 17 14.2239 17 14.5001Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<defs>
<filter
id="filter0_bi_195_4047"
x="1"
y="5"
width="22"
height="18"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1" />
<feComposite
in2="SourceAlpha"
operator="in"
result="effect1_backgroundBlur_195_4047"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_backgroundBlur_195_4047"
result="shape"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="0.2" />
<feGaussianBlur stdDeviation="0.05" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="shape"
result="effect2_innerShadow_195_4047"
/>
</filter>
<linearGradient
id="paint0_linear_195_4047"
x1="15.026"
y1="22.4602"
x2="3.73188"
y2="27.2831"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#2567FF" />
<stop offset="1" stopColor="#22A7FF" />
</linearGradient>
</defs>
</svg>
);
}
@@ -1,98 +1,21 @@
export function ContractIcon(props: { className?: string }) {
return (
<svg
className={props.className}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={props.className}
role="presentation"
>
<title>Contract</title>
<g>
<rect
x="3"
y="21"
width="14.9988"
height="10.6499"
rx="1.5"
transform="rotate(-90 3 21)"
fill="url(#paint0_linear_281_45147)"
/>
<g filter="url(#filter0_bi_281_45147)">
<path
d="M7.39429 4.5C7.39429 3.67157 8.06586 3 8.89429 3H19.5C20.3285 3 21 3.67157 21 4.5V19.5C21 20.3284 20.3285 21 19.5 21H8.89429C8.06586 21 7.39429 20.3284 7.39429 19.5V4.5Z"
fill="#EBA4D2"
fillOpacity="0.8"
/>
</g>
<path
d="M11.1971 7L17.1971 7.0001"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
/>
<path
d="M11.1971 10L14.1971 10.0001"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
/>
</g>
<defs>
<filter
id="filter0_bi_281_45147"
x="5.39429"
y="1"
width="17.6057"
height="22"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1" />
<feComposite
in2="SourceAlpha"
operator="in"
result="effect1_backgroundBlur_281_45147"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_backgroundBlur_281_45147"
result="shape"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="0.2" />
<feGaussianBlur stdDeviation="0.05" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="shape"
result="effect2_innerShadow_281_45147"
/>
</filter>
<linearGradient
id="paint0_linear_281_45147"
x1="-2.77663"
y1="30.016"
x2="10.9305"
y2="13.6496"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F4009F" />
<stop offset="1" stopColor="#F856C8" />
</linearGradient>
</defs>
<path
d="M14 11H8M10 15H8M16 7H8M20 6.8V17.2C20 18.8802 20 19.7202 19.673 20.362C19.3854 20.9265 18.9265 21.3854 18.362 21.673C17.7202 22 16.8802 22 15.2 22H8.8C7.11984 22 6.27976 22 5.63803 21.673C5.07354 21.3854 4.6146 20.9265 4.32698 20.362C4 19.7202 4 18.8802 4 17.2V6.8C4 5.11984 4 4.27976 4.32698 3.63803C4.6146 3.07354 5.07354 2.6146 5.63803 2.32698C6.27976 2 7.11984 2 8.8 2H15.2C16.8802 2 17.7202 2 18.362 2.32698C18.9265 2.6146 19.3854 3.07354 19.673 3.63803C20 4.27976 20 5.11984 20 6.8Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
@@ -7,77 +7,15 @@ export function EngineIcon(props: { className?: string }) {
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={props.className}
role="presentation"
>
<title>Engine</title>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.9523 11.9988C14.9523 11.9981 14.9518 11.9975 14.951 11.9975C14.9089 11.9992 14.8665 12 14.8239 12C13.0588 12 11.6278 10.5691 11.6278 8.8039C11.6278 7.03873 13.0588 5.60779 14.8239 5.60779C15.9762 5.60779 16.986 6.21756 17.5484 7.13202C17.695 7.37046 17.9908 7.48805 18.2658 7.43583C18.4057 7.40926 18.5501 7.39536 18.6978 7.39536C19.9694 7.39536 21.0001 8.42614 21.0001 9.69767C21.0001 10.9692 19.9694 12 18.6979 12C18.6979 12 18.6979 12 18.6979 12C18.6979 12 18.6979 12 18.6978 12H14.9536C14.9529 12 14.9523 11.9994 14.9523 11.9988Z"
fill="url(#paint0_linear_195_3943)"
d="M6.5 19C4.01472 19 2 16.9853 2 14.5C2 12.1564 3.79151 10.2313 6.07974 10.0194C6.54781 7.17213 9.02024 5 12 5C14.9798 5 17.4522 7.17213 17.9203 10.0194C20.2085 10.2313 22 12.1564 22 14.5C22 16.9853 19.9853 19 17.5 19C13.1102 19 10.3433 19 6.5 19Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<g filter="url(#filter0_bi_195_3943)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.18764 18.9251C6.17601 18.9253 6.16437 18.9253 6.15271 18.9253C4.41138 18.9253 2.99976 17.5137 2.99976 15.7724C2.99976 14.031 4.41138 12.6194 6.15271 12.6194C6.18301 12.6194 6.20685 12.5932 6.20379 12.563C6.18785 12.4057 6.1797 12.2463 6.1797 12.085C6.1797 9.37046 8.48997 7.16992 11.3398 7.16992C13.8545 7.16992 15.949 8.88324 16.407 11.1512C16.4682 11.4541 16.7615 11.6671 17.0697 11.6434C17.1634 11.6361 17.2581 11.6325 17.3536 11.6325C19.3675 11.6325 21 13.265 21 15.2789C21 17.2928 19.3675 18.9253 17.3536 18.9253C17.285 18.9253 17.2169 18.9234 17.1493 18.9197C17.1063 18.9234 17.0628 18.9253 17.0188 18.9253H6.20985C6.20243 18.9253 6.19503 18.9252 6.18764 18.9251Z"
fill="#BFB5FF"
fillOpacity="0.8"
/>
</g>
<defs>
<filter
id="filter0_bi_195_3943"
x="0.999756"
y="5.16992"
width="22.0003"
height="15.7554"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1" />
<feComposite
in2="SourceAlpha"
operator="in"
result="effect1_backgroundBlur_195_3943"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_backgroundBlur_195_3943"
result="shape"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="0.2" />
<feGaussianBlur stdDeviation="0.05" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="shape"
result="effect2_innerShadow_195_3943"
/>
</filter>
<linearGradient
id="paint0_linear_195_3943"
x1="13.1976"
y1="10.8463"
x2="28.7967"
y2="2.94276"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#3F2DAF" />
<stop offset="1" stopColor="#917FFB" />
</linearGradient>
</defs>
</svg>
);
}
@@ -7,47 +7,15 @@ export function InsightIcon(props: { className?: string }) {
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={props.className}
role="presentation"
>
<title>Insight</title>
<circle
cx="8.11963"
cy="12.9889"
r="4.505"
fill="url(#paint0_linear_1095_479)"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M21.2163 7.97235C21.2163 7.96914 21.2164 7.96592 21.2164 7.96271C21.2164 5.60751 18.4863 3.69824 15.1186 3.69824C11.7509 3.69824 9.02089 5.60751 9.02089 7.96271C9.02089 7.96324 9.02089 7.96378 9.02089 7.96431H9.02087V17.0369C9.02087 17.0372 9.02087 17.0374 9.02087 17.0376C9.02137 19.3925 11.7512 21.3014 15.1186 21.3014C18.486 21.3014 21.2158 19.3925 21.2163 17.0376C21.2163 17.0374 21.2163 17.0372 21.2163 17.0369V7.97235Z"
fill="#06B6D4"
/>
<path
d="M11.1232 10.3516V10.3516C13.5336 11.9944 16.704 11.9944 19.1145 10.3515V10.3515"
stroke="white"
strokeWidth="1.5"
d="M20.5 7.27783L12 12.0001M12 12.0001L3.49997 7.27783M12 12.0001L12 21.5001M21 12.0001V7.94153C21 7.59889 21 7.42757 20.9495 7.27477C20.9049 7.13959 20.8318 7.01551 20.7354 6.91082C20.6263 6.79248 20.4766 6.70928 20.177 6.54288L12.777 2.43177C12.4934 2.27421 12.3516 2.19543 12.2015 2.16454C12.0685 2.13721 11.9315 2.13721 11.7986 2.16454C11.6484 2.19543 11.5066 2.27421 11.223 2.43177L3.82297 6.54288C3.52345 6.70928 3.37368 6.79248 3.26463 6.91082C3.16816 7.01551 3.09515 7.13959 3.05048 7.27477C3 7.42757 3 7.59889 3 7.94153V16.0586C3 16.4013 3 16.5726 3.05048 16.7254C3.09515 16.8606 3.16816 16.9847 3.26463 17.0893C3.37368 17.2077 3.52346 17.2909 3.82297 17.4573L11.223 21.5684C11.5066 21.726 11.6484 21.8047 11.7986 21.8356C11.9315 21.863 12.0685 21.863 12.2015 21.8356C12.3516 21.8047 12.4934 21.726 12.777 21.5684L13 21.4445M7.5 4.50008L16.5 9.50008M22 21.5001L21 20.5001M22 18.0001C22 19.6569 20.6569 21.0001 19 21.0001C17.3431 21.0001 16 19.6569 16 18.0001C16 16.3432 17.3431 15.0001 19 15.0001C20.6569 15.0001 22 16.3432 22 18.0001Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M11.1232 15.0107V15.0107C13.5336 16.6536 16.704 16.6535 19.1145 15.0107V15.0107"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<defs>
<linearGradient
id="paint0_linear_1095_479"
x1="8.11963"
y1="16.3861"
x2="-1.14166"
y2="3.67577"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#155E75" />
<stop offset="1" stopColor="#27B0DB" />
</linearGradient>
</defs>
</svg>
);
}
@@ -7,79 +7,15 @@ export function PayIcon(props: { className?: string }) {
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={props.className}
role="presentation"
>
<title>Pay</title>
<circle cx="15.9849" cy="16" r="5" fill="url(#paint0_linear_585_2)" />
<g filter="url(#filter0_bi_585_2)">
<path
d="M3 4.5C3 3.67157 3.67157 3 4.5 3H17.5C18.3284 3 19 3.67157 19 4.5V13.5C19 14.3284 18.3284 15 17.5 15H4.5C3.67157 15 3 14.3284 3 13.5V4.5Z"
fill="#B9DDFF"
fillOpacity="0.8"
/>
</g>
<rect
x="4.98955"
y="9.44861"
width="3.54886"
height="3.54886"
rx="1"
fill="white"
<path
d="M22 10H2M2 8.2L2 15.8C2 16.9201 2 17.4802 2.21799 17.908C2.40973 18.2843 2.71569 18.5903 3.09202 18.782C3.51984 19 4.07989 19 5.2 19L18.8 19C19.9201 19 20.4802 19 20.908 18.782C21.2843 18.5903 21.5903 18.2843 21.782 17.908C22 17.4802 22 16.9201 22 15.8V8.2C22 7.0799 22 6.51984 21.782 6.09202C21.5903 5.7157 21.2843 5.40974 20.908 5.21799C20.4802 5 19.9201 5 18.8 5L5.2 5C4.0799 5 3.51984 5 3.09202 5.21799C2.7157 5.40973 2.40973 5.71569 2.21799 6.09202C2 6.51984 2 7.07989 2 8.2Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path d="M3.00316 5.99185H19" stroke="white" />
<defs>
<filter
id="filter0_bi_585_2"
x="1"
y="1"
width="20"
height="16"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1" />
<feComposite
in2="SourceAlpha"
operator="in"
result="effect1_backgroundBlur_585_2"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_backgroundBlur_585_2"
result="shape"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="0.2" />
<feGaussianBlur stdDeviation="0.05" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="shape"
result="effect2_innerShadow_585_2"
/>
</filter>
<linearGradient
id="paint0_linear_585_2"
x1="19.003"
y1="15.1892"
x2="10.8494"
y2="17.6614"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#2567FF" />
<stop offset="1" stopColor="#22A7FF" />
</linearGradient>
</defs>
</svg>
);
}
@@ -7,92 +7,15 @@ export function RPCIcon(props: { className?: string }) {
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={props.className}
role="presentation"
>
<title>RPC</title>
<rect
x="12.0015"
y="12.5007"
width="8.99854"
height="8.99854"
rx="1.5"
fill="url(#paint0_linear_195_3994)"
/>
<g filter="url(#filter0_i_195_3994)">
<path
d="M5 18C3.89543 18 3 17.1046 3 16L3 5C3 3.89543 3.89543 3 5 3L16 3C17.1046 3 18 3.89543 18 5L18 16C18 17.1046 17.1046 18 16 18L5 18Z"
fill="#BFB5FF"
fillOpacity="0.8"
/>
</g>
<path
d="M6.10608 9.20007C7.27946 8.04993 8.85702 7.4057 10.5001 7.4057C12.1431 7.4057 13.7207 8.04993 14.8941 9.20007"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8.30289 11.3971C8.88958 10.822 9.67836 10.4999 10.4999 10.4999C11.3214 10.4999 12.1102 10.822 12.6969 11.3971"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10.5 13.5941H10.5058"
stroke="white"
d="M4 11C6.38695 11 8.67613 11.9482 10.364 13.636C12.0518 15.3239 13 17.6131 13 20M4 4C8.24346 4 12.3131 5.68571 15.3137 8.68629C18.3143 11.6869 20 15.7565 20 20M6 19C6 19.5523 5.55228 20 5 20C4.44772 20 4 19.5523 4 19C4 18.4477 4.44772 18 5 18C5.55228 18 6 18.4477 6 19Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<defs>
<filter
id="filter0_i_195_3994"
x="3"
y="3"
width="15"
height="15.2764"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="0.276368" />
<feGaussianBlur stdDeviation="0.138184" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="shape"
result="effect1_innerShadow_195_3994"
/>
</filter>
<linearGradient
id="paint0_linear_195_3994"
x1="13.5087"
y1="19.8752"
x2="30.3227"
y2="14.0648"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#3F2DAF" />
<stop offset="1" stopColor="#917FFB" />
</linearGradient>
</defs>
</svg>
);
}
@@ -7,86 +7,15 @@ export function SmartAccountIcon(props: { className?: string }) {
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={props.className}
role="presentation"
>
<title>Smart Account</title>
<rect
x="11.6693"
y="16.9566"
width="13.4583"
height="9.33069"
rx="1.49536"
transform="rotate(-90 11.6693 16.9566)"
fill="url(#paint0_linear_195_3925)"
/>
<g filter="url(#filter0_bi_195_3925)">
<path
d="M8.18321 21.2503C8.7189 21.581 9.37914 21.581 9.91484 21.2503C11.5871 20.2182 15.098 17.6396 15.098 14.1832V10.2763C15.098 9.44263 14.5809 8.69639 13.8003 8.40366L9.75127 6.88528C9.2985 6.71549 8.79955 6.71549 8.34678 6.88528L4.29775 8.40366C3.51715 8.69639 3 9.44263 3 10.2763V14.1832C3 17.6396 6.51095 20.2182 8.18321 21.2503Z"
fill="#B9DDFF"
fillOpacity="0.8"
/>
</g>
<path
d="M9.04903 14.4024C9.98975 14.4024 10.7524 13.6398 10.7524 12.6991C10.7524 11.7583 9.98975 10.9957 9.04903 10.9957C8.10831 10.9957 7.3457 11.7583 7.3457 12.6991C7.3457 13.6398 8.10831 14.4024 9.04903 14.4024Z"
fill="white"
d="M13 7L11.8845 4.76892C11.5634 4.1268 11.4029 3.80573 11.1634 3.57116C10.9516 3.36373 10.6963 3.20597 10.4161 3.10931C10.0992 3 9.74021 3 9.02229 3H5.2C4.0799 3 3.51984 3 3.09202 3.21799C2.71569 3.40973 2.40973 3.71569 2.21799 4.09202C2 4.51984 2 5.0799 2 6.2V7M2 7H17.2C18.8802 7 19.7202 7 20.362 7.32698C20.9265 7.6146 21.3854 8.07354 21.673 8.63803C22 9.27976 22 10.1198 22 11.8V16.2C22 17.8802 22 18.7202 21.673 19.362C21.3854 19.9265 20.9265 20.3854 20.362 20.673C19.7202 21 18.8802 21 17.2 21H6.8C5.11984 21 4.27976 21 3.63803 20.673C3.07354 20.3854 2.6146 19.9265 2.32698 19.362C2 18.7202 2 17.8802 2 16.2V7ZM12 17.5C12 17.5 15 16.0701 15 13.9252V11.4229L12.8124 10.6412C12.2868 10.4529 11.712 10.4529 11.1864 10.6412L9 11.4229V13.9252C9 16.0701 12 17.5 12 17.5Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.04906 13.6652C9.3792 13.6652 9.6472 13.9415 9.6472 14.2828V16.7528C9.6472 17.0937 9.37954 17.3704 9.04906 17.3704C8.71893 17.3704 8.45093 17.094 8.45093 16.7528V14.2828C8.45093 13.9419 8.71859 13.6652 9.04906 13.6652Z"
fill="white"
/>
<defs>
<filter
id="filter0_bi_195_3925"
x="1"
y="4.75793"
width="16.098"
height="18.7404"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1" />
<feComposite
in2="SourceAlpha"
operator="in"
result="effect1_backgroundBlur_195_3925"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_backgroundBlur_195_3925"
result="shape"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="0.2" />
<feGaussianBlur stdDeviation="0.05" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="shape"
result="effect2_innerShadow_195_3925"
/>
</filter>
<linearGradient
id="paint0_linear_195_3925"
x1="22.4602"
y1="20.8654"
x2="12.4019"
y2="25.2643"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#2567FF" />
<stop offset="1" stopColor="#22A7FF" />
</linearGradient>
</defs>
</svg>
);
}
@@ -1,3 +1,4 @@
import { NebulaIcon } from "../../../../nebula-app/(app)/icons/NebulaIcon";
import type { ChainSupportedService } from "../../types/chain";
import { ConnectSDKIcon } from "./icons/ConnectSDKIcon";
import { ContractIcon } from "./icons/ContractIcon";
@@ -13,14 +14,14 @@ export const products = [
id: "contracts",
icon: ContractIcon,
description: "Pre-built contracts & deployment tools",
link: "https://portal.thirdweb.com/contracts",
link: "https://thirdweb.com/contracts",
},
{
name: "Connect SDK",
id: "connect-sdk",
icon: ConnectSDKIcon,
description: "Client-side SDKs for wallets and blockchain interactions",
link: "https://portal.thirdweb.com/connect",
link: "https://thirdweb.com/connect",
},
{
name: "RPC Edge",
@@ -34,7 +35,7 @@ export const products = [
id: "engine",
icon: EngineIcon,
description: "Backend server that reads, writes, and deploys contracts",
link: "https://portal.thirdweb.com/engine",
link: "https://thirdweb.com/engine",
},
{
name: "Account Abstraction",
@@ -55,7 +56,14 @@ export const products = [
id: "insight",
icon: InsightIcon,
description: "Query, transform and analyze blockchain data",
link: "https://portal.thirdweb.com/insight",
link: "https://thirdweb.com/insight",
},
{
name: "Nebula",
id: "nebula",
icon: NebulaIcon,
description: "The most powerful AI for interacting with the blockchain",
link: "https://thirdweb.com/nebula",
},
] satisfies Array<{
name: string;
@@ -5,6 +5,7 @@ export type ChainSupportedService =
| "connect-sdk"
| "engine"
| "account-abstraction"
| "nebula"
| "pay"
| "rpc-edge"
| "chainsaw"
@@ -5,7 +5,7 @@ import { ContractPublishForm } from "components/contract-components/contract-pub
import { revalidatePath } from "next/cache";
import { notFound, redirect } from "next/navigation";
import { fetchDeployMetadata } from "thirdweb/contract";
import { getPublishedContractsWithPublisherMapping } from "../../../published-contract/[publisher]/[contract_id]/utils/getPublishedContractsWithPublisherMapping";
import { getLatestPublishedContractsWithPublisherMapping } from "../../../published-contract/[publisher]/[contract_id]/utils/getPublishedContractsWithPublisherMapping";
type DirectDeployPageProps = {
params: Promise<{
@@ -46,18 +46,12 @@ export default async function PublishContractPage(
// - get the publish metadata with name+publisher address
// - merge the two objects with publishMetadataFromUri taking higher precedence
if (!publishMetadataFromUri.version) {
const publishedContractVersions =
await getPublishedContractsWithPublisherMapping({
const publishedContract =
await getLatestPublishedContractsWithPublisherMapping({
publisher: address,
contract_id: publishMetadataFromUri.name,
});
if (!publishedContractVersions) {
notFound();
}
const publishedContract = publishedContractVersions[0];
if (publishedContract) {
publishMetadata = {
...publishedContract,
@@ -131,3 +131,5 @@ export default async function ExploreCategoryPage(
</div>
);
}
export const dynamic = "force-static";
@@ -48,3 +48,5 @@ export default async function ExplorePage() {
</div>
);
}
export const dynamic = "force-static";
@@ -1,6 +1,5 @@
import { notFound } from "next/navigation";
import { PublishedContractBreadcrumbs } from "./components/breadcrumbs.client";
import { getPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping";
import { getLatestPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping";
type Params = { publisher: string; contract_id: string };
@@ -20,16 +19,11 @@ export async function generateMetadata(props: { params: Promise<Params> }) {
const params = await props.params;
const { publisher, contract_id } = params;
const publishedContracts = await getPublishedContractsWithPublisherMapping({
publisher: publisher,
contract_id: contract_id,
});
if (!publishedContracts) {
notFound();
}
const publishedContract = publishedContracts[0];
const publishedContract =
await getLatestPublishedContractsWithPublisherMapping({
publisher: publisher,
contract_id: contract_id,
});
if (!publishedContract) {
return {
@@ -3,7 +3,7 @@ import { format } from "date-fns/format";
import { resolveEns } from "lib/ens";
import { correctAndUniqueLicenses } from "lib/licenses";
import { getSocialProfiles } from "thirdweb/social";
import { getPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping";
import { getLatestPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping";
import { publishedContractOGImageTemplate } from "./utils/publishedContractOGImageTemplate";
export const runtime = "edge";
@@ -22,8 +22,8 @@ export default async function Image(props: {
const client = getThirdwebClient();
const { publisher, contract_id } = props.params;
const [publishedContracts, socialProfiles] = await Promise.all([
getPublishedContractsWithPublisherMapping({
const [publishedContract, socialProfiles] = await Promise.all([
getLatestPublishedContractsWithPublisherMapping({
publisher: publisher,
contract_id: contract_id,
}),
@@ -42,12 +42,6 @@ export default async function Image(props: {
};
})();
if (!publishedContracts) {
return null;
}
const publishedContract = publishedContracts[0];
if (!publishedContract) {
return null;
}
@@ -1,5 +1,8 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { fetchPublishedContractVersions } from "components/contract-components/fetch-contracts-with-versions";
import {
fetchLatestPublishedContractVersion,
fetchPublishedContractVersions,
} from "components/contract-components/fetch-contracts-with-versions";
import { isAddress } from "thirdweb";
import { resolveAddress } from "thirdweb/extensions/ens";
@@ -36,3 +39,24 @@ export async function getPublishedContractsWithPublisherMapping(options: {
return undefined;
}
}
export async function getLatestPublishedContractsWithPublisherMapping(options: {
publisher: string;
contract_id: string;
}) {
const { publisher, contract_id } = options;
try {
// resolve ENS
const publisherAddress = isAddress(publisher)
? publisher
: await resolveAddress({
client: getThirdwebClient(),
name: mapThirdwebPublisher(publisher),
});
return fetchLatestPublishedContractVersion(publisherAddress, contract_id);
} catch {
return undefined;
}
}
@@ -33,9 +33,9 @@ const PackageIcon: React.FC = () => (
viewBox="0 0 24 24"
fill="none"
stroke="rgba(255,255,255,.5)"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="16.5" y1="9.4" x2="7.5" y2="4.21" />
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
@@ -53,9 +53,9 @@ const FileTextIcon: React.FC = () => (
viewBox="0 0 24 24"
fill="none"
stroke="rgba(255,255,255,.5)"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
@@ -74,9 +74,9 @@ const CalendarIcon: React.FC = () => (
viewBox="0 0 24 24"
fill="none"
stroke="rgba(255,255,255,.5)"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
@@ -94,9 +94,9 @@ const VersionIcon: React.FC = () => (
viewBox="0 0 24 24"
fill="none"
stroke="rgba(255,255,255,.5)"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="6" y1="3" x2="6" y2="15" />
<circle cx="18" cy="6" r="3" />
@@ -1,41 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { shareLink } from "@/lib/shareLink";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
export function DeployActions(props: {
publisher: string;
contract_id: string;
version?: string;
displayName: string;
}) {
const searchparams = useSearchParams();
const stringifiedSearchParams = searchparams?.toString();
return (
<div className="flex gap-3">
<Button asChild variant="outline">
<Link
href={`/${props.publisher}/${props.contract_id}${props.version ? `/${props.version}` : ""}${stringifiedSearchParams ? `?${stringifiedSearchParams}` : ""}`}
target="_blank"
>
Contract
</Link>
</Button>
<Button
variant="outline"
onClick={() => {
shareLink({
title: `Deploy ${props.displayName}`,
});
}}
>
Share
</Button>
</div>
);
}
@@ -19,6 +19,7 @@ export function PublishedActions(props: {
<div className="flex gap-3">
<Button
variant="outline"
className="bg-card"
onClick={() => {
shareLink({
title: `Deploy ${props.displayName}`,
@@ -27,7 +28,7 @@ export function PublishedActions(props: {
>
Share
</Button>
<Button asChild variant="primary" className="gap-2">
<Button asChild className="gap-2">
<Link
href={`/${props.publisher}/${props.contract_id}${props.version ? `/${props.version}` : ""}/deploy${stringifiedSearchParams ? `?${stringifiedSearchParams}` : ""}`}
>
@@ -18,7 +18,7 @@ export function DeployContractInfo(props: {
<div className="flex flex-col gap-2">
<div className="flex flex-1 items-center gap-4">
{contractImageLink && (
<div className="hidden shrink-0 items-center justify-center rounded-xl border border-border p-2 md:flex">
<div className="hidden shrink-0 items-center justify-center rounded-xl border border-border bg-card p-2 md:flex">
{/*eslint-disable-next-line @next/next/no-img-element*/}
<img className="size-12" alt={props.name} src={contractImageLink} />
</div>
@@ -1,4 +1,3 @@
import { Separator } from "@/components/ui/separator";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import {
fetchPublishedContractVersion,
@@ -7,7 +6,6 @@ import {
import { isAddress } from "thirdweb";
import { fetchDeployMetadata } from "thirdweb/contract";
import { resolveAddress } from "thirdweb/extensions/ens";
import { DeployActions } from "./contract-actions-deploy.client";
import { DeployContractHeader } from "./contract-header";
import { DeployFormForUri } from "./uri-based-deploy";
@@ -73,23 +71,17 @@ export async function DeployFormForPublishInfo(props: PublishBasedDeployProps) {
]);
return (
<>
<div className="mx-auto flex w-full max-w-[1000px] flex-col gap-8 pb-20">
<DeployContractHeader
{...props}
allVersions={publishedContractVersions}
activeVersion={publishedContract}
>
<DeployActions
{...props}
displayName={publishedContract.displayName || publishedContract.name}
/>
</DeployContractHeader>
<Separator />
/>
<DeployFormForUri
contractMetadata={contractMetadata}
modules={fetchedModules.filter((m) => m !== null)}
pathname={`/${props.publisher}/${props.contract_id}${props.version ? `/${props.version}` : ""}/deploy`}
/>
</>
</div>
);
}
@@ -56,7 +56,7 @@ export function DeployContractVersionSelector({
}
}}
>
<SelectTrigger className="min-w-[180px] bg-transparent hover:bg-accent">
<SelectTrigger className="min-w-[180px] bg-card hover:bg-accent">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -1,10 +1,10 @@
"use server";
import "server-only";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { API_SERVER_URL } from "@/constants/env";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getTeamById } from "@/api/team";
import { getRawAccount } from "../../../../account/settings/getAccount";
import { getAuthTokenWalletAddress } from "../../../../api/lib/getAuthToken";
import { loginRedirect } from "../../../../login/loginRedirect";
type State = {
success: boolean;
@@ -71,33 +71,37 @@ export async function createTicketAction(
_previousState: State,
formData: FormData,
) {
const cookieManager = await cookies();
const activeAccount = cookieManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
const token = activeAccount
? cookieManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
: null;
if (!activeAccount || !token) {
// user is not logged in, make them log in
redirect(`/login?next=${encodeURIComponent("/support")}`);
}
const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (accountRes.status !== 200) {
// user is not logged in, make them log in
redirect(`/login?next=${encodeURIComponent("/support")}`);
const teamId = formData.get("teamId")?.toString();
if (!teamId) {
return {
success: false,
message: "teamId is required",
};
}
const account = (await accountRes.json()) as {
data: { name: string; email: string; plan: string; id: string };
};
const team = await getTeamById(teamId);
const customerId = isValidPlan(account.data.plan)
? planToCustomerId[account.data.plan]
: undefined;
if (!team) {
return {
success: false,
message: `Team with id "${teamId}" not found`,
};
}
const [walletAddress, account] = await Promise.all([
getAuthTokenWalletAddress(),
getRawAccount(),
]);
if (!walletAddress || !account) {
loginRedirect("/support");
}
const customerId = isValidPlan(team.supportPlan)
? planToCustomerId[team.supportPlan]
: // fallback to "free" tier
planToCustomerId.free;
const product = formData.get("product")?.toString() || "";
const problemArea = formData.get("extraInfo_Problem_Area")?.toString() || "";
@@ -105,8 +109,8 @@ export async function createTicketAction(
const title = prepareEmailTitle(
product,
problemArea,
account.data.email,
account.data.name,
account.email || "",
account.name || "",
);
const keyVal: Record<string, string> = {};
@@ -117,10 +121,10 @@ export async function createTicketAction(
const markdown = prepareEmailBody({
product,
markdownInput: keyVal.markdown || "",
email: account.data.email,
name: account.data.name,
email: account.email || "",
name: account.name || "",
extraInfoInput: keyVal,
walletAddress: activeAccount,
walletAddress: walletAddress,
});
const content = {
@@ -129,9 +133,9 @@ export async function createTicketAction(
markdown,
status: "open",
onBehalfOf: {
email: account.data.email,
name: account.data.name,
id: account.data.id,
email: account.email,
name: account.name,
id: account.id,
},
customerId,
emailInboxId: process.env.UNTHREAD_EMAIL_INBOX_ID,
@@ -3,6 +3,7 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { SupportForm_SelectInput } from "components/help/contact-forms/shared/SupportForm_SelectInput";
import dynamic from "next/dynamic";
import {
@@ -14,6 +15,7 @@ import {
} from "react";
import { useFormStatus } from "react-dom";
import { toast } from "sonner";
import { SupportForm_TeamSelection } from "../../../../../components/help/contact-forms/shared/SupportForm_TeamSelection";
import { createTicketAction } from "./create-ticket.action";
const ConnectSupportForm = dynamic(
@@ -75,14 +77,46 @@ const productOptions: { label: string; component: ReactElement }[] = [
},
];
export function CreateTicket() {
function ProductAreaSelection(props: {
productLabel: string;
setProductLabel: (val: string) => void;
}) {
const { productLabel, setProductLabel } = props;
return (
<div className="flex flex-col gap-6">
<SupportForm_SelectInput
formLabel="What do you need help with?"
name="product"
options={productOptions.map((o) => o.label)}
promptText="Select a product"
onValueChange={setProductLabel}
value={productLabel}
required={true}
/>
{productOptions.find((o) => o.label === productLabel)?.component}
</div>
);
}
export function CreateTicket(props: {
teams: {
name: string;
id: string;
}[];
}) {
const formRef = useRef<HTMLFormElement>(null);
const [selectedTeamId, setSelectedTeamId] = useState<string | undefined>(
props.teams[0]?.id,
);
const [productLabel, setProductLabel] = useState("");
const [state, formAction] = useActionState(createTicketAction, {
message: "",
success: false,
});
// needed here
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
@@ -112,16 +146,19 @@ export function CreateTicket() {
<div className="h-7" />
<div className="flex flex-col gap-6">
<SupportForm_SelectInput
formLabel="What do you need help with?"
name="product"
options={productOptions.map((o) => o.label)}
promptText="Select a product"
onValueChange={setProductLabel}
value={productLabel}
required={true}
{/* Don't conditionally render this - it has be rendered to submit the input values */}
<div className={cn(props.teams.length === 1 && "hidden")}>
<SupportForm_TeamSelection
selectedTeamId={selectedTeamId}
onChange={(teamId) => setSelectedTeamId(teamId)}
teams={props.teams}
/>
</div>
<ProductAreaSelection
productLabel={productLabel}
setProductLabel={setProductLabel}
/>
{productOptions.find((o) => o.label === productLabel)?.component}
</div>
</div>
@@ -1,3 +1,4 @@
import { getTeams } from "@/api/team";
import {
Breadcrumb,
BreadcrumbItem,
@@ -7,17 +8,16 @@ import {
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import Link from "next/link";
import { redirect } from "next/navigation";
import { getAuthToken } from "../../../api/lib/getAuthToken";
import { loginRedirect } from "../../../login/loginRedirect";
import { CreateTicket } from "./components/create-ticket.client";
export default async function Page() {
const authToken = await getAuthToken();
const teams = await getTeams();
if (!authToken) {
return redirect(
`/login?next=${encodeURIComponent("/support/create-ticket")}`,
);
const pagePath = "/support/create-ticket";
if (!teams || teams.length === 0) {
loginRedirect(pagePath);
}
return (
@@ -36,7 +36,12 @@ export default async function Page() {
</BreadcrumbList>
</Breadcrumb>
<div className="container max-w-[750px] py-10">
<CreateTicket />
<CreateTicket
teams={teams.map((t) => ({
name: t.name,
id: t.id,
}))}
/>
</div>
</div>
);
@@ -86,6 +86,7 @@ function TailwindTheme(props: { children: React.ReactNode }) {
disableTransitionOnChange
enableSystem={false}
defaultTheme="dark"
enableColorScheme={true}
>
{props.children}
</ThemeProvider>
@@ -1,207 +0,0 @@
import { Box, Container, Flex } from "@chakra-ui/react";
import { ChakraNextImage } from "components/Image";
import { LandingDynamicSelector } from "components/landing-pages/dynamic-selector";
import { LandingEndCTA } from "components/landing-pages/end-cta";
import { LandingGuidesShowcase } from "components/landing-pages/guide-showcase";
import { LandingHeroWithSideImage } from "components/landing-pages/hero-with-side-image";
import { LandingShowcaseImage } from "components/landing-pages/showcase-image";
import type { Metadata } from "next";
import { Heading, Text } from "tw-components";
// images
import smartWalletMiniImage from "../../../../public/assets/product-icons/smart-wallet.png";
import accountAbstractionImage from "../../../../public/assets/product-pages/smart-wallet/account-abstraction.png";
import batchTxImage from "../../../../public/assets/product-pages/smart-wallet/batch-txns.png";
import dashboardImage from "../../../../public/assets/product-pages/smart-wallet/dashboard.png";
import desktopHero from "../../../../public/assets/product-pages/smart-wallet/desktop-hero.png";
import fullyProgrammaticImage from "../../../../public/assets/product-pages/smart-wallet/full-programmability.png";
import getStartedImage from "../../../../public/assets/product-pages/smart-wallet/get-started.png";
import invisibleWalletsImage from "../../../../public/assets/product-pages/smart-wallet/invisible-wallet.png";
import managedInfrastructureImage from "../../../../public/assets/product-pages/smart-wallet/managed-infrastructure.png";
import mobileHero from "../../../../public/assets/product-pages/smart-wallet/mobile-hero.png";
import pairAnyWalletImage from "../../../../public/assets/product-pages/smart-wallet/pair-any-wallet.png";
import smartContractsImage from "../../../../public/assets/product-pages/smart-wallet/smart-contracts.png";
import uiComponentsImage from "../../../../public/assets/product-pages/smart-wallet/ui-components.png";
import chooseContractImage from "../../../../public/assets/product-pages/smart-wallet/which-contract.png";
import { getAbsoluteUrl } from "../../../lib/vercel-utils";
const GUIDES = [
{
title: "The Quick-Start Guide to Account Abstraction",
image: getStartedImage,
link: "https://portal.thirdweb.com/wallets/smart-wallet/get-started",
},
{
title: "Choosing Between Simple, Managed, & Dynamic Smart Accounts",
image: chooseContractImage,
link: "https://blog.thirdweb.com/smart-contract-deep-dive-building-smart-wallets-for-individuals-and-teams/",
},
{
title: "How to Enable Batch Transactions with Account Abstraction",
image: batchTxImage,
link: "https://blog.thirdweb.com/guides/how-to-batch-transactions-with-the-thirdweb-sdk/",
},
];
const TRACKING_CATEGORY = "smart-wallet-landing";
const title = "The Complete Account Abstraction Toolkit";
const description =
"Add account abstraction to your web3 app & unlock powerful features for seamless onboarding, customizable transactions, & maximum security. Learn more.";
export const metadata: Metadata = {
title,
description,
openGraph: {
title,
description,
images: [
{
url: `${getAbsoluteUrl()}/assets/og-image/smart-wallet.png`,
width: 1200,
height: 630,
},
],
},
};
export default function Page() {
return (
<Container
maxW="container.page"
as={Flex}
flexDir="column"
gap={{ base: "80px", md: "120px" }}
>
<LandingHeroWithSideImage
miniTitle="Powered by ERC-4337"
title="The complete toolkit for"
titleWithGradient="Account Abstraction"
subtitle="Transform your app's user experience with signless transactions, multi-signature security, account recovery and more."
trackingCategory={TRACKING_CATEGORY}
ctaLink="https://portal.thirdweb.com/connect/account-abstraction/overview"
contactUsTitle="Book Demo"
gradient="linear(to-r, #4490FF, #4490FF)"
image={desktopHero}
mobileImage={mobileHero}
miniImage={smartWalletMiniImage}
/>
<LandingDynamicSelector
title="What can you build with account abstraction?"
blackToWhiteTitle=""
TRACKING_CATEGORY={TRACKING_CATEGORY}
items={[
{
title: "Transform your apps UX",
description:
"Infinitely-customizable wallet transactions — with spend limits, batch transactions, and a signless & gasless combo for web2 users.",
Component: (
<ChakraNextImage
src={invisibleWalletsImage}
alt=""
borderRadius="lg"
/>
),
},
{
title: "Instant onboarding for every user",
description:
"Auth for the most popular web3 wallets and web2 login flows — with just an email, phone number, social account, or passkeys.",
Component: (
<ChakraNextImage
src={pairAnyWalletImage}
alt=""
borderRadius="lg"
/>
),
},
{
title: "Enterprise-grade security",
description:
"Wallet recovery, 2FA, and multi-signature support for ultimate peace of mind — for users & teams.",
Component: (
<ChakraNextImage
src={fullyProgrammaticImage}
alt=""
borderRadius="lg"
/>
),
},
]}
/>
<Flex flexDir="column" gap={6} alignItems="center">
<Heading
as="h2"
size="display.md"
textAlign="center"
px={{ base: 2, md: 0 }}
>
An all-in-one solution for
<Box bgGradient="linear(to-r, #3385FF, #7BB0FF)" bgClip="text">
Account Abstraction
</Box>
</Heading>
<Text textAlign="center" size="body.xl" w={{ base: "100%", md: "60%" }}>
Implement account abstraction into any web3 app — with a best-in-class
SDK, full wallet customizability, and managed infrastructure.
</Text>
</Flex>
<LandingShowcaseImage
miniTitle="Smart Contracts"
titleWithGradient="Fast Account Abstraction"
title="factory deployment."
gradient="linear(to-r, #3385FF, #7BB0FF)"
description="Pre-built, audited, & customizable factory contracts with options for simple, managed, or dynamic permissions — deployable to any EVM network."
image={smartContractsImage}
/>
<LandingShowcaseImage
miniTitle="SDK"
titleWithGradient="Integrate account abstraction"
title="into your web3 apps."
gradient="linear(to-r, #3385FF, #7BB0FF)"
description="A best-in-class SDK with everything you need to build apps powered by account abstraction & generate accounts for your users — with full ERC-4337 compatibility."
image={accountAbstractionImage}
imagePosition="left"
/>
<LandingShowcaseImage
miniTitle="UI Components"
titleWithGradient="Drag and drop"
title="UI Components."
gradient="linear(to-r, #3385FF, #7BB0FF)"
description="The easiest way to give accounts to your users with account abstraction — with UI components that are purpose-built for account abstraction & compatible with any existing ERC-4337 account, out of the box."
image={uiComponentsImage}
/>
<LandingShowcaseImage
miniTitle="Managed Infrastructure"
titleWithGradient="Support millions"
title="of users."
gradient="linear(to-r, #3385FF, #7BB0FF)"
description="Managed infrastructure with everything you need to scale to millions of users — handling bundlers, paymasters, & all of the complexity so you can focus on building your app."
image={managedInfrastructureImage}
imagePosition="left"
/>
<LandingShowcaseImage
miniTitle="Dashboard & Analytics"
titleWithGradient="Manage accounts"
title="& view onchain analytics."
gradient="linear(to-r, #3385FF, #7BB0FF)"
description="A single source of truth to view accounts, manage signers, and view balances for your web3 app — complete with onchain analytics."
image={dashboardImage}
/>
<LandingGuidesShowcase
title="Get started with Account Abstraction"
category={TRACKING_CATEGORY}
description="See our quick-start guides to implement account abstraction into your web3 app"
solution="Account Abstraction"
guides={GUIDES}
/>
<LandingEndCTA
title="Add account abstraction"
titleWithGradient="to your web3 app."
trackingCategory={TRACKING_CATEGORY}
ctaLink="https://portal.thirdweb.com/connect/account-abstraction/overview"
gradient="linear(to-r, #3385FF, #7BB0FF)"
/>
</Container>
);
}
@@ -14,7 +14,6 @@ import analyticsImage from "../../../../public/assets/landingpage/desktop/analyt
import authDesktopImage from "../../../../public/assets/landingpage/desktop/auth.png";
import crossPlatformDesktopImage from "../../../../public/assets/landingpage/desktop/cross-platform.png";
import enterpriseSecurityImage from "../../../../public/assets/landingpage/desktop/enterprise-security.png";
import guestImage from "../../../../public/assets/landingpage/desktop/guest.png";
import magicImage from "../../../../public/assets/landingpage/desktop/magic.png";
import onboardImage from "../../../../public/assets/landingpage/desktop/onboard.png";
import powerfulImage from "../../../../public/assets/landingpage/desktop/powerful.png";
@@ -24,7 +23,6 @@ import analyticsMobileImage from "../../../../public/assets/landingpage/mobile/a
import authMobileImage from "../../../../public/assets/landingpage/mobile/auth.png";
import crossPlatformMobileImage from "../../../../public/assets/landingpage/mobile/cross-platform.png";
import enterpriseSecurityMobileImage from "../../../../public/assets/landingpage/mobile/enterprise-security.png";
import guestMobileImage from "../../../../public/assets/landingpage/mobile/guest.png";
import magicMobileImage from "../../../../public/assets/landingpage/mobile/magic.png";
import mobileOnboardImage from "../../../../public/assets/landingpage/mobile/onboard.png";
import powerfulMobileImage from "../../../../public/assets/landingpage/mobile/powerful.png";
@@ -190,16 +188,6 @@ export default function Page() {
href="https://portal.thirdweb.com/connect/in-app-wallet/how-to/enable-gasless"
/>
<LandingCardWithImage
title="Guest mode"
description={`Allow anyone to use your app in seconds — with a wallet that's generated automatically when they press "Continue as guest."`}
image={guestImage}
mobileImage={guestMobileImage}
TRACKING_CATEGORY={TRACKING_CATEGORY}
href="https://portal.thirdweb.com/references/wallets/latest/LocalWallet"
colSpan={1}
/>
<LandingCardWithImage
title="Bring your own auth"
description="Integrate your authentication system and spin up in-app wallets for your users — new and existing."
@@ -17,7 +17,7 @@ export type AccountHeaderCompProps = {
connectButton: React.ReactNode;
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
createProject: (team: Team) => void;
account: Pick<Account, "email" | "id"> | undefined;
account: Pick<Account, "email" | "id" | "image"> | undefined;
client: ThirdwebClient;
};
@@ -41,10 +41,9 @@ export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
href="/account"
className="flex flex-row items-center gap-2 font-normal text-sm"
>
{/* TODO - set account Image */}
<GradientAvatar
id={props.account?.id || "default"}
src={""}
src={props.account?.image || ""}
className="size-6"
client={props.client}
/>
@@ -90,10 +89,9 @@ export function AccountHeaderMobileUI(props: AccountHeaderCompProps) {
"flex flex-row items-center gap-2 font-normal text-foreground text-sm",
)}
>
{/* TODO - set account image */}
<GradientAvatar
id={props.account?.id}
src={props.account ? "" : undefined}
src={props.account?.image || ""}
className="size-6"
client={props.client}
/>
@@ -5,6 +5,7 @@ import { updateAccount } from "@/actions/updateAccount";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import type { ThirdwebClient } from "thirdweb";
import { upload } from "thirdweb/storage";
import { AccountSettingsPageUI } from "./AccountSettingsPageUI";
export function AccountSettingsPage(props: {
@@ -23,9 +24,25 @@ export function AccountSettingsPage(props: {
</div>
<div className="container max-w-[950px] grow pt-8 pb-20">
<AccountSettingsPageUI
// TODO - remove hide props these when these fields are functional
hideAvatar
hideDeleteAccount
client={props.client}
updateAccountAvatar={async (file) => {
let uri: string | undefined = undefined;
if (file) {
// upload to IPFS
uri = await upload({
client: props.client,
files: [file],
});
}
await updateAccount({
image: uri,
});
router.refresh();
}}
account={props.account}
updateEmailWithOTP={async (otp) => {
const res = await confirmEmailWithOTP(otp);
@@ -1,4 +1,5 @@
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { Toaster } from "sonner";
@@ -33,6 +34,8 @@ export const Mobile: Story = {
},
};
const client = getThirdwebClient();
function Variants() {
const [isVerifiedEmail, setIsVerifiedEmail] = useState(true);
const [sendEmailFails, setSendEmailFails] = useState(false);
@@ -74,6 +77,10 @@ function Variants() {
? new Date().toISOString()
: undefined,
}}
client={client}
updateAccountAvatar={async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
}}
updateEmailWithOTP={async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
if (emailConfirmationFails) {
@@ -30,6 +30,7 @@ import {
InputOTPSlot,
} from "@/components/ui/input-otp";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
import { cn } from "@/lib/utils";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -39,30 +40,35 @@ import { EllipsisIcon } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type { ThirdwebClient } from "thirdweb";
import { z } from "zod";
import { FileInput } from "../../../components/shared/FileInput";
type MinimalAccount = Pick<
Account,
"name" | "email" | "emailConfirmedAt" | "unconfirmedEmail"
"name" | "email" | "emailConfirmedAt" | "unconfirmedEmail" | "image"
>;
export function AccountSettingsPageUI(props: {
account: MinimalAccount;
// TODO - remove hide props these when these fields are functional
hideAvatar?: boolean;
hideDeleteAccount?: boolean;
sendEmail: (email: string) => Promise<void>;
updateName: (name: string) => Promise<void>;
updateEmailWithOTP: (otp: string) => Promise<void>;
updateAccountAvatar: (avatar: File | undefined) => Promise<void>;
client: ThirdwebClient;
}) {
return (
<div className="flex flex-col gap-8">
{!props.hideAvatar && <AccountAvatarFormControl />}
<AccountNameFormControl
name={props.account.name || ""}
updateName={(name) => props.updateName(name)}
/>
<AccountAvatarFormControl
updateAccountAvatar={props.updateAccountAvatar}
avatar={props.account.image || undefined}
client={props.client}
/>
<AccountEmailFormControl
email={props.account.email || ""}
status={props.account.emailConfirmedAt ? "verified" : "unverified"}
@@ -75,18 +81,24 @@ export function AccountSettingsPageUI(props: {
);
}
function AccountAvatarFormControl() {
function AccountAvatarFormControl(props: {
updateAccountAvatar: (avatar: File | undefined) => Promise<void>;
avatar: string | undefined;
client: ThirdwebClient;
}) {
const accountAvatarUrl = resolveSchemeWithErrorHandler({
client: props.client,
uri: props.avatar,
});
const [avatar, setAvatar] = useState<File>();
// TODO - implement
const updateAvatarMutation = useMutation({
mutationFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 3000));
mutationFn: (_avatar: File | undefined) => {
return props.updateAccountAvatar(_avatar);
},
});
function handleSave() {
const promises = updateAvatarMutation.mutateAsync();
const promises = updateAvatarMutation.mutateAsync(avatar);
toast.promise(promises, {
success: "Account avatar updated successfully",
error: "Failed to update account avatar",
@@ -118,6 +130,7 @@ function AccountAvatarFormControl() {
setValue={setAvatar}
className="w-20 rounded-full lg:w-28"
disableHelperText
fileUrl={accountAvatarUrl}
/>
</div>
</SettingsCard>
@@ -30,19 +30,27 @@ export function SecondaryNavLinks() {
<ResourcesDropdownButton />
<Link
href="https://portal.thirdweb.com"
className="text-muted-foreground text-sm hover:text-foreground"
target="_blank"
href="https://thirdweb.com/support"
>
Docs
</Link>
<Link
target="_blank"
href="/support"
className="text-muted-foreground text-sm hover:text-foreground"
>
Support
</Link>
<Link
href="https://portal.thirdweb.com/"
className="text-muted-foreground text-sm hover:text-foreground"
target="_blank"
href="https://feedback.thirdweb.com"
className="text-muted-foreground text-sm hover:text-foreground"
>
Docs
Feedback
</Link>
</div>
);
@@ -18,7 +18,7 @@ import type { ThirdwebClient } from "thirdweb";
export function AccountButton(props: {
logout: () => void;
connectButton: React.ReactNode;
account?: Pick<Account, "email" | "id">;
account?: Pick<Account, "email" | "id" | "image">;
client: ThirdwebClient;
}) {
const { setTheme, theme } = useTheme();
@@ -35,11 +35,10 @@ export function AccountButton(props: {
>
{/* Don't remove the div */}
<div>
{/* TODO - set account image */}
<GradientAvatar
id={props.account?.id || "default"}
src={""}
className="size-9"
src={props.account?.image || ""}
className="size-9 border"
client={props.client}
/>
</div>
@@ -159,6 +159,34 @@ export function MobileBurgerMenuButton(
</div>
<div className="mt-auto">
<div className="flex flex-col gap-5">
<Link
target="_blank"
href="https://portal.thirdweb.com"
className="text-muted-foreground hover:text-foreground "
>
Docs
</Link>
<Link
target="_blank"
href="/support"
className="text-muted-foreground hover:text-foreground "
>
Support
</Link>
<Link
target="_blank"
href="https://feedback.thirdweb.com"
className="text-muted-foreground hover:text-foreground "
>
Feedback
</Link>
</div>
<div className="h-6" />
<Separator />
<div className="h-6" />
@@ -25,8 +25,8 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
accentText: "hsl(var(--link-foreground))",
accentButtonBg: "hsl(var(--primary))",
accentButtonText: "hsl(var(--primary-foreground))",
primaryButtonBg: "hsl(var(--background))",
primaryButtonText: "hsl(var(--foreground))",
primaryButtonBg: "hsl(var(--inverted))",
primaryButtonText: "hsl(var(--inverted-foreground))",
secondaryButtonText: "hsl(var(--secondary-foreground))",
tooltipBg: "hsl(var(--popover))",
tooltipText: "hsl(var(--popover-foreground))",
-2
View File
@@ -1,5 +1,4 @@
import "@/styles/globals.css";
import { Toaster } from "@/components/ui/sonner";
import { DashboardRouterTopProgressBar } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
import type { Metadata } from "next";
@@ -82,7 +81,6 @@ export default function RootLayout({
<EnsureValidConnectedWalletLoginServer />
</Suspense>
</AppRouterProviders>
<Toaster richColors />
<DashboardRouterTopProgressBar />
<NextTopLoader
color="hsl(var(--primary))"
+12 -4
View File
@@ -55,7 +55,15 @@ export function LoginAndOnboardingPage(props: {
<ThirdwebMiniLogo className="size-7 md:size-8" />
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<Link
href="https://portal.thirdweb.com/"
className="px-2 text-muted-foreground text-sm hover:text-foreground"
target="_blank"
>
Docs
</Link>
<Link
href="/support"
target="_blank"
@@ -65,11 +73,11 @@ export function LoginAndOnboardingPage(props: {
</Link>
<Link
href="https://portal.thirdweb.com/"
className="px-2 text-muted-foreground text-sm hover:text-foreground"
target="_blank"
href="https://feedback.thirdweb.com"
className="px-2 text-muted-foreground text-sm hover:text-foreground"
>
Docs
Feedback
</Link>
<ColorModeToggle />
@@ -8,6 +8,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useTrack } from "hooks/analytics/useTrack";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import {
type AccountValidationSchema,
accountValidationSchema,
@@ -81,15 +82,21 @@ export const AccountForm: React.FC<AccountFormProps> = ({
data,
});
},
onError: (err) => {
const error = err as Error;
onError: (error) => {
console.error(error);
if (
onDuplicateError &&
error?.message.match(/email address already exists/)
) {
onDuplicateError(values.email);
return;
} else if (error.message.includes("INVALID_EMAIL_ADDRESS")) {
toast.error("Invalid Email Address");
} else {
toast.error(error.message || "Failed to update account");
}
trackEvent({
category: "account",
action: "update",
@@ -2,42 +2,33 @@ import { NEXT_PUBLIC_NEBULA_URL } from "@/constants/env";
// TODO - copy the source of this library to dashboard
import { stream } from "fetch-event-stream";
import type { NebulaTxData } from "../components/Chats";
import type { ExecuteConfig } from "./types";
export type ContextFilters = {
chainIds?: string[];
contractAddresses?: string[];
walletAddresses?: string[];
export type NebulaContext = {
chainIds: string[] | null;
walletAddress: string | null;
};
export async function promptNebula(params: {
message: string;
sessionId: string;
config: ExecuteConfig | null;
authToken: string;
handleStream: (res: ChatStreamedResponse) => void;
abortController: AbortController;
contextFilters: undefined | ContextFilters;
context: undefined | NebulaContext;
}) {
const body: Record<string, string | boolean | object> = {
message: params.message,
user_id: "default-user",
session_id: params.sessionId,
stream: true,
session_id: params.sessionId,
};
if (params.contextFilters) {
body.context_filter = {
chain_ids: params.contextFilters.chainIds || [],
contract_addresses: params.contextFilters.contractAddresses || [],
wallet_addresses: params.contextFilters.walletAddresses || [],
if (params.context) {
body.context = {
chain_ids: params.context.chainIds || [],
wallet_address: params.context.walletAddress,
};
}
if (params.config) {
body.execute_config = params.config;
}
const events = await stream(`${NEXT_PUBLIC_NEBULA_URL}/chat`, {
method: "POST",
headers: {
@@ -1,33 +1,23 @@
import { NEXT_PUBLIC_NEBULA_URL } from "@/constants/env";
import { fetchWithAuthToken } from "../../../../utils/fetchWithAuthToken";
import type { ContextFilters } from "./chat";
import type { NebulaContext } from "./chat";
import type {
DeletedSessionInfo,
ExecuteConfig,
SessionInfo,
TruncatedSessionInfo,
UpdatedSessionInfo,
} from "./types";
// TODO - get the spec for return types on /session POST and PUT
export async function createSession(params: {
authToken: string;
config: ExecuteConfig | null;
contextFilters: ContextFilters | undefined;
context: NebulaContext | undefined;
}) {
const body: Record<string, string | boolean | object> = {
can_execute: !!params.config,
};
if (params.config) {
body.execute_config = params.config;
}
const body: Record<string, string | boolean | object> = {};
if (params.contextFilters) {
body.context_filter = {
chain_ids: params.contextFilters.chainIds || [],
contract_addresses: params.contextFilters.contractAddresses || [],
wallet_addresses: params.contextFilters.walletAddresses || [],
if (params.context) {
body.context = {
chain_ids: params.context.chainIds || [],
wallet_address: params.context.walletAddress,
};
}
@@ -48,22 +38,15 @@ export async function createSession(params: {
export async function updateSession(params: {
authToken: string;
config: ExecuteConfig | null;
sessionId: string;
contextFilters: ContextFilters | undefined;
contextFilters: NebulaContext | undefined;
}) {
const body: Record<string, string | boolean | object> = {
can_execute: !!params.config,
};
if (params.config) {
body.execute_config = params.config;
}
const body: Record<string, string | boolean | object> = {};
if (params.contextFilters) {
body.context_filter = {
body.context = {
chain_ids: params.contextFilters.chainIds || [],
contract_addresses: params.contextFilters.contractAddresses || [],
wallet_addresses: params.contextFilters.walletAddresses || [],
wallet_address: params.contextFilters.walletAddress,
};
}
@@ -1,28 +1,6 @@
type EngineConfig = {
mode: "engine";
engine_url: string;
engine_authorization_token: string;
engine_backend_wallet_address: string;
};
type SessionKeyConfig = {
mode: "session_key";
smart_account_address: string;
smart_account_factory_address: string;
smart_account_session_key: string;
};
type ClientConfig = {
mode: "client";
signer_wallet_address: string;
};
export type ExecuteConfig = EngineConfig | SessionKeyConfig | ClientConfig;
type SessionContextFilter = {
chain_ids: string[] | null;
contract_addresses: string[] | null;
wallet_addresses: string[] | null;
wallet_address: string | null;
};
export type SessionInfo = {
@@ -31,7 +9,6 @@ export type SessionInfo = {
modal_name: string;
archive_at: string | null;
can_execute: boolean;
execute_config: ExecuteConfig | null;
created_at: string;
deleted_at: string | null;
history: Array<{
@@ -43,7 +20,7 @@ export type SessionInfo = {
archived_at: string | null;
title: string | null;
is_public: boolean | null;
context_filter: SessionContextFilter | null;
context: SessionContextFilter | null;
// memory
// action: array<object> | null; <-- type of this is not available on https://nebula-api.thirdweb-dev.com/docs#/default/get_session_session__session_id__get
};
@@ -52,8 +29,7 @@ export type UpdatedSessionInfo = {
title: string;
modal_name: string;
account_id: string;
execute_config: ExecuteConfig | null;
context_filter: SessionContextFilter | null;
context: SessionContextFilter | null;
};
export type DeletedSessionInfo = {
@@ -12,11 +12,11 @@ import { useThirdwebClient } from "@/constants/thirdweb.client";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { ArrowRightIcon } from "lucide-react";
import Link from "next/link";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
import { type ContextFilters, promptNebula } from "../api/chat";
import { type NebulaContext, promptNebula } from "../api/chat";
import { createSession, updateSession } from "../api/session";
import type { ExecuteConfig, SessionInfo } from "../api/types";
import type { SessionInfo } from "../api/types";
import { newChatPageUrlStore, newSessionsStore } from "../stores";
import { ChatBar } from "./ChatBar";
import { type ChatMessage, Chats } from "./Chats";
@@ -81,27 +81,26 @@ export function ChatPageContent(props: {
useState(false);
const [contextFilters, _setContextFilters] = useState<
ContextFilters | undefined
NebulaContext | undefined
>(() => {
const contextFilterRes = props.session?.context_filter;
const value: ContextFilters = {
chainIds: contextFilterRes?.chain_ids || undefined,
contractAddresses: contextFilterRes?.contract_addresses || undefined,
walletAddresses: contextFilterRes?.wallet_addresses || undefined,
const contextRes = props.session?.context;
const value: NebulaContext = {
chainIds: contextRes?.chain_ids || null,
walletAddress: contextRes?.wallet_address || null,
};
return value;
});
const setContextFilters = useCallback((v: ContextFilters | undefined) => {
const setContextFilters = useCallback((v: NebulaContext | undefined) => {
_setContextFilters(v);
setHasUserUpdatedContextFilters(true);
}, []);
const isNewSession = !props.session;
// if this is a new session, user has not manually updated context filters
// update the context filters to the current user's wallet address and chain id
// if this is a new session, user has not manually updated context
// update the context to the current user's wallet address and chain id
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (!isNewSession || hasUserUpdatedContextFilters) {
@@ -109,11 +108,16 @@ export function ChatPageContent(props: {
}
_setContextFilters((_contextFilters) => {
const updatedContextFilters: ContextFilters = _contextFilters
? { ..._contextFilters }
: {};
const updatedContextFilters: NebulaContext = _contextFilters
? {
..._contextFilters,
}
: {
chainIds: [],
walletAddress: null,
};
updatedContextFilters.walletAddresses = address ? [address] : [];
updatedContextFilters.walletAddress = address || null;
updatedContextFilters.chainIds = activeChain
? [activeChain.id.toString()]
: [];
@@ -122,15 +126,6 @@ export function ChatPageContent(props: {
});
}, [address, isNewSession, hasUserUpdatedContextFilters, activeChain]);
const config: ExecuteConfig | null = useMemo(() => {
return address
? {
mode: "client",
signer_wallet_address: address,
}
: null;
}, [address]);
const [sessionId, _setSessionId] = useState<string | undefined>(
props.session?.id,
);
@@ -163,12 +158,11 @@ export function ChatPageContent(props: {
const initSession = useCallback(async () => {
const session = await createSession({
authToken: props.authToken,
config,
contextFilters,
context: contextFilters,
});
setSessionId(session.id);
return session;
}, [config, contextFilters, props.authToken, setSessionId]);
}, [contextFilters, props.authToken, setSessionId]);
const handleSendMessage = useCallback(
async (message: string) => {
@@ -221,7 +215,6 @@ export function ChatPageContent(props: {
abortController,
message: message,
sessionId: currentSessionId,
config: config,
authToken: props.authToken,
handleStream(res) {
if (abortController.signal.aborted) {
@@ -308,7 +301,7 @@ export function ChatPageContent(props: {
}
}
},
contextFilters: contextFilters,
context: contextFilters,
});
} catch (error) {
if (abortController.signal.aborted) {
@@ -338,7 +331,6 @@ export function ChatPageContent(props: {
[
sessionId,
contextFilters,
config,
props.authToken,
messages.length,
initSession,
@@ -363,13 +355,12 @@ export function ChatPageContent(props: {
const showEmptyState = !userHasSubmittedMessage && messages.length === 0;
const handleUpdateContextFilters = async (
values: ContextFilters | undefined,
values: NebulaContext | undefined,
) => {
// if session is not yet created, don't need to update sessions - starting a chat will create a session with the context filters
// if session is not yet created, don't need to update sessions - starting a chat will create a session with the context
if (sessionId) {
await updateSession({
authToken: props.authToken,
config,
sessionId,
contextFilters: values,
});
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { Toaster } from "sonner";
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
import type { ContextFilters } from "../api/chat";
import type { NebulaContext } from "../api/chat";
import ContextFiltersButton from "./ContextFilters";
const meta = {
@@ -37,6 +37,7 @@ function Story() {
<Variant
contextFilters={{
chainIds: ["137"],
walletAddress: null,
}}
label="1 chain"
/>
@@ -44,57 +45,25 @@ function Story() {
<Variant
contextFilters={{
chainIds: ["137", "10", "421614"],
walletAddress: null,
}}
label="Few chains"
/>
<Variant
contextFilters={{
contractAddresses: ["0x1E51e33F9838A5a043E099C60409f62aA564272f"],
}}
label="1 contract"
/>
<Variant
contextFilters={{
contractAddresses: [
"0x1E51e33F9838A5a043E099C60409f62aA564272f",
"0xF61c8d5492139b40af09bDB353733d5F0a348aCf",
],
}}
label="Few contracts"
/>
<Variant
contextFilters={{
walletAddresses: ["0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37"],
walletAddress: "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
chainIds: null,
}}
label="1 wallet"
/>
<Variant
contextFilters={{
walletAddresses: [
"0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
"0x83Dd93fA5D8343094f850f90B3fb90088C1bB425",
],
}}
label="Few wallets"
/>
<Variant
contextFilters={{
chainIds: ["137", "10", "421614"],
contractAddresses: [
"0x1E51e33F9838A5a043E099C60409f62aA564272f",
"0xF61c8d5492139b40af09bDB353733d5F0a348aCf",
],
walletAddresses: [
"0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
"0x83Dd93fA5D8343094f850f90B3fb90088C1bB425",
],
walletAddress: "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
}}
label="chains + wallets + contracts"
label="chains + wallet"
/>
<Toaster richColors />
</div>
@@ -103,10 +72,10 @@ function Story() {
function Variant(props: {
label: string;
contextFilters: ContextFilters | undefined;
contextFilters: NebulaContext | undefined;
}) {
const [contextFilters, setContextFilters] = useState<
ContextFilters | undefined
NebulaContext | undefined
>(props.contextFilters);
return (
<BadgeContainer label={props.label}>
@@ -114,7 +83,7 @@ function Variant(props: {
contextFilters={contextFilters}
setContextFilters={setContextFilters}
updateContextFilters={async (values) => {
console.log("Updating context filters", values);
console.log("Updating context", values);
await new Promise((resolve) => setTimeout(resolve, 1000));
}}
/>
@@ -21,7 +21,7 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { AutoResizeTextarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
@@ -31,52 +31,34 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { isAddress } from "thirdweb";
import { z } from "zod";
import type { ContextFilters } from "../api/chat";
import type { NebulaContext } from "../api/chat";
export default function ContextFiltersButton(props: {
contextFilters: ContextFilters | undefined;
setContextFilters: (filters: ContextFilters | undefined) => void;
updateContextFilters: (filters: ContextFilters | undefined) => Promise<void>;
contextFilters: NebulaContext | undefined;
setContextFilters: (filters: NebulaContext | undefined) => void;
updateContextFilters: (filters: NebulaContext | undefined) => Promise<void>;
}) {
const [isOpen, setIsOpen] = useState(false);
const chainIds = props.contextFilters?.chainIds;
const contractAddresses = props.contextFilters?.contractAddresses;
const walletAddresses = props.contextFilters?.walletAddresses;
const walletAddress = props.contextFilters?.walletAddress;
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button size="sm" variant="outline" className="max-w-full gap-2">
<SlidersHorizontalIcon className="size-3.5 shrink-0 text-muted-foreground" />
Context Filters
Context
<div className="flex gap-1 overflow-hidden">
{chainIds && chainIds.length > 0 && (
<Badge className="gap-1 truncate" variant="secondary">
Chain
<span className="max-sm:hidden">{chainIds.join(", ")}</span>
</Badge>
)}
{contractAddresses && contractAddresses.length > 0 && (
<Badge className="gap-1 truncate" variant="secondary">
Contract
<span className="max-sm:hidden">
{contractAddresses
.map((add) => `${add.trim().slice(0, 6)}..`)
.join(", ")}
</span>
</Badge>
)}
{walletAddresses && walletAddresses.length > 0 && (
{walletAddress && (
<Badge className="gap-1 truncate" variant="secondary">
Wallet
<span className="max-sm:hidden">
{walletAddresses
.map((add) => `${add.trim().slice(0, 6)}..`)
.join(", ")}
</span>
</Badge>
)}
</div>
@@ -84,9 +66,7 @@ export default function ContextFiltersButton(props: {
</DialogTrigger>
<DialogContent className="overflow-hidden p-0">
<DialogHeader className="px-6 pt-6 pb-1">
<DialogTitle className="font-semibold text-xl">
Context Filters
</DialogTitle>
<DialogTitle className="font-semibold text-xl">Context</DialogTitle>
<DialogDescription className="text-muted-foreground">
Provide context to Nebula for your prompts
</DialogDescription>
@@ -105,17 +85,21 @@ export default function ContextFiltersButton(props: {
);
}
const commaSeparateListOfAddresses = z.string().refine(
(s) => {
if (s.trim() === "") {
return true;
}
return s.split(",").every((s) => isAddress(s.trim()));
},
{
message: "Must be a comma-separated list of valid addresses",
},
);
const optionalAddressSchema = z
.string()
.transform((v) => v.trim())
.optional()
.refine(
(s) => {
if (!s) {
return true;
}
return isAddress(s);
},
{
message: "Must be a valid addresses",
},
);
const formSchema = z.object({
chainIds: z.string().refine(
@@ -132,14 +116,13 @@ const formSchema = z.object({
message: "Chain IDs must be a comma-separated list of integers",
},
),
contractAddresses: commaSeparateListOfAddresses,
walletAddresses: commaSeparateListOfAddresses,
walletAddress: optionalAddressSchema,
});
export function ContextFiltersForm(props: {
contextFilters: ContextFilters | undefined;
updateContextFilters: (filters: ContextFilters | undefined) => Promise<void>;
setContextFilters: (filters: ContextFilters | undefined) => void;
contextFilters: NebulaContext | undefined;
updateContextFilters: (filters: NebulaContext | undefined) => Promise<void>;
setContextFilters: (filters: NebulaContext | undefined) => void;
formBodyClassName?: string;
formActionContainerClassName?: string;
modal:
@@ -158,40 +141,26 @@ export function ContextFiltersForm(props: {
chainIds: props.contextFilters?.chainIds
? props.contextFilters.chainIds.join(",")
: "",
contractAddresses: props.contextFilters?.contractAddresses
? props.contextFilters.contractAddresses.join(",")
: "",
walletAddresses: props.contextFilters?.walletAddresses
? props.contextFilters.walletAddresses.join(",")
: "",
walletAddress: props.contextFilters?.walletAddress || "",
},
reValidateMode: "onChange",
});
function onSubmit(values: z.infer<typeof formSchema>) {
const { chainIds, contractAddresses, walletAddresses } = values;
const { chainIds, walletAddress } = values;
const chainIdsArray = chainIds.split(",").filter((id) => id.trim());
const contractAddressesArray = contractAddresses
.split(",")
.filter((v) => v.trim());
const walletAddressesArray = walletAddresses
.split(",")
.filter((v) => v.trim());
const data = {
chainIds: chainIdsArray,
contractAddresses: contractAddressesArray,
walletAddresses: walletAddressesArray,
walletAddress: walletAddress || null,
};
const promise = updateMutation.mutateAsync(data);
toast.promise(promise, {
success: "Context filters updated",
error: "Failed to update context filters",
success: "Context updated",
error: "Failed to update context",
});
promise.then(() => {
@@ -228,7 +197,6 @@ export function ContextFiltersForm(props: {
.filter(Boolean)
.map(Number)}
onChange={(values) => {
console.log("values", values);
form.setValue("chainIds", values.join(","));
}}
/>
@@ -240,33 +208,15 @@ export function ContextFiltersForm(props: {
<FormField
control={form.control}
name="contractAddresses"
name="walletAddress"
render={({ field }) => (
<FormItem>
<FormLabel>Contract Addresses</FormLabel>
<FormLabel>Wallet Address</FormLabel>
<FormControl>
<AutoResizeTextarea
<Input
{...field}
placeholder="0x123..., 0x456..."
className="!leading-relaxed min-h-[52px] resize-none md:text-xs"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="walletAddresses"
render={({ field }) => (
<FormItem>
<FormLabel>Wallet Addresses</FormLabel>
<FormControl>
<AutoResizeTextarea
{...field}
placeholder="0x123..., 0x456..."
className="!leading-relaxed min-h-[52px] resize-none md:text-xs"
placeholder="0x123.."
className="md:text-xs md:placeholder:text-xs"
/>
</FormControl>
<FormMessage />
@@ -61,7 +61,7 @@ export function NebulaAccountButton(props: {
<div className="shrink-0">
<GradientAvatar
id={props.account?.id || "default"}
src={""}
src={props.account?.image || ""}
className="size-9 rounded-lg"
client={client}
/>
@@ -88,7 +88,7 @@ export function NebulaAccountButton(props: {
<div className="shrink-0">
<GradientAvatar
id={props.account?.id || "default"}
src={""}
src={props.account?.image || ""}
className="size-9 rounded-full"
client={client}
/>
File diff suppressed because one or more lines are too long
+8 -1
View File
@@ -1,8 +1,9 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider } from "next-themes";
import { ThemeProvider, useTheme } from "next-themes";
import { useEffect, useMemo } from "react";
import { Toaster } from "sonner";
import {
ThirdwebProvider,
useActiveAccount,
@@ -31,6 +32,7 @@ export function AppRouterProviders(props: { children: React.ReactNode }) {
enableSystem={false}
defaultTheme="dark"
>
<ToasterSetup />
<SanctionedAddressesChecker>
{props.children}
</SanctionedAddressesChecker>
@@ -40,6 +42,11 @@ export function AppRouterProviders(props: { children: React.ReactNode }) {
);
}
function ToasterSetup() {
const { theme } = useTheme();
return <Toaster richColors theme={theme === "light" ? "light" : "dark"} />;
}
function SyncChainDefinitionsToConnectionManager() {
const { allChainsV5 } = useAllChainsData();
const connectionManager = useConnectionManager();
@@ -38,6 +38,10 @@ const sidebarLinkMeta: Array<{ pathId: string; label: string }> = [
pathId: "alerts",
label: "Alerts",
},
{
pathId: "wallet-credentials",
label: "Wallet Credentials",
},
{
pathId: "configuration",
label: "Configuration",
@@ -0,0 +1,129 @@
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import {
type EngineInstance,
type SetWalletConfigInput,
useEngineSetWalletConfig,
} from "@3rdweb-sdk/react/hooks/useEngine";
import { useTrack } from "hooks/analytics/useTrack";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
interface CircleConfigProps {
instance: EngineInstance;
authToken: string;
}
export const CircleConfig: React.FC<CircleConfigProps> = ({
instance,
authToken,
}) => {
const { mutate: setCircleConfig, isPending } = useEngineSetWalletConfig({
instanceUrl: instance.url,
authToken,
});
const trackEvent = useTrack();
const defaultValues: SetWalletConfigInput = {
type: "circle" as const,
circleApiKey: "",
};
const form = useForm<SetWalletConfigInput>({
defaultValues,
values: defaultValues,
resetOptions: {
keepDirty: true,
keepDirtyValues: true,
},
});
const onSubmit = (data: SetWalletConfigInput) => {
setCircleConfig(data, {
onSuccess: () => {
toast.success("Configuration set successfully");
trackEvent({
category: "engine",
action: "set-wallet-config",
type: "circle",
label: "success",
});
},
onError: (error) => {
toast.error("Failed to set configuration", {
description: error.message,
});
trackEvent({
category: "engine",
action: "set-wallet-config",
type: "circle",
label: "error",
error,
});
},
});
};
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<p className="text-muted-foreground">
Circle wallets require an API Key from your Circle account with
sufficient permissions. Created wallets are stored in your AWS
account. Configure your Circle API Key to use Circle wallets. Learn
more about{" "}
<TrackedLinkTW
href="https://portal.thirdweb.com/engine/features/backend-wallets#circle-wallet"
target="_blank"
label="learn-more"
category="engine"
className="text-link-foreground hover:text-foreground"
>
how to get an API Key
</TrackedLinkTW>
.
</p>
</div>
<Form {...form}>
<form
className="flex flex-col gap-4"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormFieldSetup
label="Circle API Key"
errorMessage={
form.getFieldState("circleApiKey", form.formState).error?.message
}
htmlFor="circle-api-key"
isRequired
tooltip={null}
>
<Input
id="circle-api-key"
placeholder="TEST_API_KEY:..."
autoComplete="off"
type="password"
{...form.register("circleApiKey")}
/>
</FormFieldSetup>
<div className="flex items-center justify-end gap-4">
<Button
type="submit"
className="min-w-28 gap-2"
disabled={isPending}
>
{isPending && <Spinner className="size-4" />}
Save
</Button>
</div>
</form>
</Form>
</div>
);
};
@@ -1,9 +1,11 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { TabButtons } from "@/components/ui/tabs";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import {
type EngineInstance,
useEngineWalletConfig,
useHasEngineFeature,
} from "@3rdweb-sdk/react/hooks/useEngine";
import {
EngineBackendWalletOptions,
@@ -13,6 +15,7 @@ import { CircleAlertIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { Heading } from "tw-components";
import { CircleConfig } from "./circle-config";
import { KmsAwsConfig } from "./kms-aws-config";
import { KmsGcpConfig } from "./kms-gcp-config";
import { LocalConfig } from "./local-config";
@@ -33,11 +36,28 @@ export const EngineWalletConfig: React.FC<EngineWalletConfigProps> = ({
authToken,
});
const { isSupported: isWalletCredentialsSupported } = useHasEngineFeature(
instance.url,
"WALLET_CREDENTIALS",
);
const filteredWalletOptions = EngineBackendWalletOptions.filter(
({ key }) =>
// circle wallets were only added with the WALLET_CREDENTIALS feature flag
(key !== "circle" || isWalletCredentialsSupported) &&
// smart wallets don't need separate configuration
!key.startsWith("smart:"),
);
const tabContent: Partial<Record<EngineBackendWalletType, React.ReactNode>> =
{
local: <LocalConfig />,
"aws-kms": <KmsAwsConfig instance={instance} authToken={authToken} />,
"gcp-kms": <KmsGcpConfig instance={instance} authToken={authToken} />,
// circle wallets were only added with the WALLET_CREDENTIALS feature flag
...(isWalletCredentialsSupported && {
circle: <CircleConfig instance={instance} authToken={authToken} />,
}),
} as const;
const [activeTab, setActiveTab] = useState<EngineBackendWalletType>("local");
@@ -62,7 +82,7 @@ export const EngineWalletConfig: React.FC<EngineWalletConfigProps> = ({
</div>
<TabButtons
tabs={EngineBackendWalletOptions.map(({ key, name }) => ({
tabs={filteredWalletOptions.map(({ key, name }) => ({
key,
name,
isActive: activeTab === key,
@@ -81,6 +101,17 @@ export const EngineWalletConfig: React.FC<EngineWalletConfigProps> = ({
tabClassName="font-medium !text-sm"
/>
{!isWalletCredentialsSupported && activeTab === "circle" && (
<Alert variant="warning" className="mt-4">
<CircleAlertIcon className="size-4" />
<AlertTitle>Update Required</AlertTitle>
<AlertDescription>
Circle wallet support requires a newer version of Engine. Please
update your Engine instance to use this feature.
</AlertDescription>
</Alert>
)}
{tabContent[activeTab]}
</div>
);
@@ -6,6 +6,8 @@ import { Button } from "@/components/ui/button";
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
import { FormItem } from "@/components/ui/form";
import { Skeleton } from "@/components/ui/skeleton";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { engineKeys } from "@3rdweb-sdk/react";
import {
type BackendWallet,
useEngineBackendWalletBalance,
@@ -30,7 +32,7 @@ import {
type UseDisclosureReturn,
useDisclosure,
} from "@chakra-ui/react";
import { useQuery } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { type ColumnDef, createColumnHelper } from "@tanstack/react-table";
import { ChainIcon } from "components/icons/ChainIcon";
import { TWTable } from "components/shared/TWTable";
@@ -40,18 +42,21 @@ import { EngineBackendWalletOptions } from "lib/engine";
import { useV5DashboardChain } from "lib/v5-adapter";
import {
DownloadIcon,
ExternalLinkIcon,
PencilIcon,
RefreshCcwIcon,
TrashIcon,
TriangleAlertIcon,
UploadIcon,
} from "lucide-react";
import Link from "next/link";
import QRCode from "qrcode";
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { getAddress } from "thirdweb";
import { shortenAddress } from "thirdweb/utils";
import { FormHelperText, FormLabel, LinkButton, Text } from "tw-components";
import { FormHelperText, FormLabel, Text } from "tw-components";
import { prettyPrintCurrency } from "./utils";
interface BackendWalletsTableProps {
@@ -83,16 +88,17 @@ const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
authToken,
chainId,
}) => {
const { data: backendWalletBalance } = useEngineBackendWalletBalance({
instanceUrl: instanceUrl,
address,
authToken,
chainId,
});
const { data: backendWalletBalance, isFetching } =
useEngineBackendWalletBalance({
instanceUrl: instanceUrl,
address,
authToken,
chainId,
});
const chain = useV5DashboardChain(chainId);
if (!backendWalletBalance) {
return <Skeleton className="h-5 w-32 rounded-lg" />;
if (!backendWalletBalance || isFetching) {
return <Skeleton className="h-6 w-36 rounded-lg" />;
}
const balanceDisplay = prettyPrintCurrency({
@@ -100,9 +106,7 @@ const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
symbol: backendWalletBalance.symbol || chain.nativeCurrency?.symbol,
});
const balanceComponent = (
<div className="text-muted-foreground">{balanceDisplay}</div>
);
const balanceComponent = <span>{balanceDisplay}</span>;
const explorer = chain.blockExplorers?.[0];
if (!explorer) {
@@ -110,14 +114,14 @@ const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
}
return (
<LinkButton
variant="ghost"
isExternal
size="xs"
<Link
target="_blank"
href={`${explorer.url}/address/${address}`}
className="inline-flex items-center gap-2.5 rounded-lg px-2 py-1 text-muted-foreground hover:bg-muted hover:text-foreground"
>
{balanceComponent}
</LinkButton>
<ExternalLinkIcon className="size-4" />
</Link>
);
};
@@ -133,6 +137,7 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
const receiveDisclosure = useDisclosure();
const sendDisclosure = useDisclosure();
const deleteDisclosure = useDisclosure();
const queryClient = useQueryClient();
const columns = useMemo(() => {
return [
@@ -160,7 +165,31 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
},
}),
columnHelper.accessor("address", {
header: "Balance",
header: () => (
<div className="flex w-[180px] items-center gap-1.5">
Balance
<ToolTipLabel
label="Refresh Balance"
contentClassName="capitalize font-normal tracking-normal leading-normal"
>
<Button
className="z-20 h-auto p-1.5 [&[data-pending='true']_svg]:animate-spin"
variant="ghost"
size="sm"
onClick={async (e) => {
const buttonEl = e.currentTarget;
buttonEl.setAttribute("data-pending", "true");
await queryClient.invalidateQueries({
queryKey: engineKeys.backendWalletBalanceAll(),
});
buttonEl.setAttribute("data-pending", "false");
}}
>
<RefreshCcwIcon className="size-4" />
</Button>
</ToolTipLabel>
</div>
),
cell: (cell) => {
const address = cell.getValue();
return (
@@ -175,7 +204,7 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
id: "balance",
}),
];
}, [instanceUrl, authToken, chainId]);
}, [instanceUrl, authToken, chainId, queryClient]);
const [selectedBackendWallet, setSelectedBackendWallet] =
useState<BackendWallet>();
@@ -2,6 +2,7 @@ import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
DialogContent,
DialogFooter,
@@ -102,6 +103,9 @@ export const CreateBackendWalletButton: React.FC<
);
invariant(selectedOption, "Selected a valid backend wallet type.");
const isCircleWallet =
walletType === "circle" || walletType === "smart:circle";
// List all wallet types only if Engine is updated to support it.
let walletTypeOptions = [selectedOption];
if (supportsSmartBackendWallets) {
@@ -126,7 +130,7 @@ export const CreateBackendWalletButton: React.FC<
</Button>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent
className="z-[10001] p-0"
className="z-[10001] overflow-hidden p-0"
dialogOverlayClassName="z-[10000]"
>
<Form {...form}>
@@ -138,7 +142,7 @@ export const CreateBackendWalletButton: React.FC<
</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-3">
{/* Wallet type */}
<FormFieldSetup
label="Wallet Type"
@@ -201,25 +205,107 @@ export const CreateBackendWalletButton: React.FC<
</AlertDescription>
</Alert>
) : (
// Label
<FormFieldSetup
key="label"
label="Label"
errorMessage={
form.getFieldState("label", form.formState).error
?.message
}
htmlFor="wallet-label"
isRequired
tooltip={null}
>
<Input
id="wallet-label"
type="text"
placeholder="A description to identify this backend wallet"
{...form.register("label", { required: true })}
/>
</FormFieldSetup>
<>
{/* Label */}
<FormFieldSetup
key="label"
label="Label"
errorMessage={
form.getFieldState("label", form.formState).error
?.message
}
htmlFor="wallet-label"
isRequired
tooltip={null}
>
<Input
id="wallet-label"
type="text"
placeholder="A description to identify this backend wallet"
{...form.register("label", { required: true })}
/>
</FormFieldSetup>
{/* Credential ID for Circle wallets */}
{isCircleWallet && (
<>
<FormFieldSetup
key="credentialId"
label="Credential ID"
errorMessage={
form.getFieldState("credentialId", form.formState)
.error?.message
}
htmlFor="credential-id"
isRequired
tooltip={null}
>
<Input
id="credential-id"
type="text"
placeholder="Enter the Circle credential ID"
{...form.register("credentialId", {
required: isCircleWallet,
})}
/>
<FormDescription className="py-2">
The ID of the Circle credential to use for this
wallet. You can find this in the{" "}
<Link
href={`/team/${teamSlug}/~/engine/${instance.id}/wallet-credentials`}
className="text-link-foreground hover:text-foreground"
>
Wallet Credentials
</Link>{" "}
section.
</FormDescription>
</FormFieldSetup>
<FormFieldSetup
key="isTestnet"
label="Testnet"
errorMessage={
form.getFieldState("isTestnet", form.formState)
.error?.message
}
htmlFor="is-testnet"
tooltip={null}
isRequired={false}
>
<div className="flex flex-col gap-2">
<div className="flex items-center space-x-2">
<Checkbox
id="is-testnet"
checked={form.watch("isTestnet")}
onCheckedChange={(checked) =>
form.setValue("isTestnet", !!checked)
}
/>
<label
htmlFor="is-testnet"
className="text-sm leading-none opacity-70 peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Use testnet mode for creating backend wallet
</label>
</div>
<FormDescription className="py-2">
If your engine is configured with a testnet API
Key for Circle, you can only create testnet
wallets. A production API Key cannot be used for
testnet transactions, and vice versa.{" "}
<Link
href="https://developers.circle.com/w3s/sandbox-vs-production"
target="_blank"
className="text-link-foreground hover:text-foreground"
>
Learn more
</Link>
</FormDescription>
</div>
</FormFieldSetup>
</>
)}
</>
)}
</div>
</div>
@@ -1,5 +1,6 @@
"use client";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import {
type EngineInstance,
useEngineBackendWallets,
@@ -80,13 +81,15 @@ function BackendWalletsSection(props: {
Configuration
</Link>{" "}
tab, or{" "}
<Link
<TrackedLinkTW
href="https://portal.thirdweb.com/infrastructure/engine/features/backend-wallets"
target="_blank"
className="underline decoration-muted-foreground/50 decoration-dotted underline-offset-4 hover:text-foreground hover:decoration-foreground hover:decoration-solid"
label="learn-more"
category="engine"
className="text-link-foreground hover:text-foreground"
>
learn more about backend wallets.
</Link>
</TrackedLinkTW>
</p>
</div>
@@ -52,6 +52,7 @@ import Link from "next/link";
import { type Dispatch, type SetStateAction, useMemo, useState } from "react";
import { toTokens } from "thirdweb";
import { FormLabel, LinkButton, Text } from "tw-components";
import { normalizeTime } from "../../../../../../../../../../lib/time";
import { TransactionTimeline } from "./transaction-timeline";
export type EngineStatus =
@@ -496,9 +497,7 @@ export function TransactionCharts(props: {
if (!tx.queuedAt || !tx.status) {
continue;
}
const normalizedDate = new Date(tx.queuedAt);
normalizedDate.setHours(0, 0, 0, 0); // normalize time
const time = normalizedDate.getTime();
const time = normalizeTime(new Date(tx.queuedAt)).getTime();
const entry = dayToTxCountMap.get(time) ?? {};
entry[tx.status] = (entry[tx.status] ?? 0) + 1;
uniqueStatuses.add(tx.status);
@@ -0,0 +1,78 @@
import { Button } from "@/components/ui/button";
import { useEngineCreateWalletCredential } from "@3rdweb-sdk/react/hooks/useEngine";
import { useTrack } from "hooks/analytics/useTrack";
import { Plus } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { CredentialForm } from "./credential-form";
import type { CredentialFormData } from "./types";
interface CreateWalletCredentialButtonProps {
instanceUrl: string;
authToken: string;
}
export const CreateWalletCredentialButton: React.FC<
CreateWalletCredentialButtonProps
> = ({ instanceUrl, authToken }) => {
const [isOpen, setIsOpen] = useState(false);
const trackEvent = useTrack();
const createCredential = useEngineCreateWalletCredential({
instanceUrl,
authToken,
});
const handleSubmit = async (data: CredentialFormData) => {
const promise = createCredential.mutateAsync(data);
toast.promise(promise, {
success: () => {
setIsOpen(false);
trackEvent({
category: "engine",
action: "create-wallet-credential",
label: "success",
type: data.type,
});
return "Wallet credential created successfully";
},
error: (error) => {
trackEvent({
category: "engine",
action: "create-wallet-credential",
label: "error",
type: data.type,
error,
});
return "Failed to create wallet credential";
},
});
};
return (
<>
<Button
onClick={() => {
trackEvent({
category: "engine",
action: "open-create-wallet-credential",
});
setIsOpen(true);
}}
>
<Plus className="mr-2 h-4 w-4" />
Create Wallet Credential
</Button>
<CredentialForm
isOpen={isOpen}
onOpenChange={setIsOpen}
onSubmit={async (data) => handleSubmit(data as CredentialFormData)}
title="Create Wallet Credential"
submitButtonText="Create"
isPending={createCredential.isPending}
/>
</>
);
};
@@ -0,0 +1,174 @@
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import {
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { Dialog } from "@radix-ui/react-dialog";
import { useForm } from "react-hook-form";
import { CircleCredentialFields } from "./credential-type-fields/circle";
import {
CREDENTIAL_TYPE_OPTIONS,
type CredentialFormData,
type CredentialType,
type CredentialUpdateFormData,
} from "./types";
interface CredentialFormProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (
data: CredentialFormData | CredentialUpdateFormData,
) => Promise<void>;
title: string;
submitButtonText: string;
isPending: boolean;
defaultValues?: Partial<CredentialFormData | CredentialUpdateFormData>;
hideTypeSelect?: boolean;
isUpdate?: boolean;
}
export const CredentialForm = ({
isOpen,
onOpenChange,
onSubmit,
title,
submitButtonText,
isPending,
defaultValues,
hideTypeSelect = false,
isUpdate = false,
}: CredentialFormProps) => {
const form = useForm<CredentialFormData | CredentialUpdateFormData>({
defaultValues: {
type: "circle" as CredentialType,
label: "",
...defaultValues,
},
mode: "onChange",
});
const selectedType = form.watch("type");
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent
className="z-[10001] overflow-hidden p-0"
dialogOverlayClassName="z-[10000]"
>
<Form {...form}>
<form
onSubmit={form.handleSubmit((data) => {
return onSubmit(data);
})}
>
<div className="p-6">
<DialogHeader className="mb-4">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>
<TrackedLinkTW
href="https://portal.thirdweb.com/infrastructure/engine/features/wallet-credentials"
target="_blank"
label="learn-more"
category="engine"
className="text-link-foreground hover:text-foreground"
>
Learn more about wallet credentials
</TrackedLinkTW>
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-5">
{/* Type */}
{!hideTypeSelect && (
<FormFieldSetup
label="Type"
errorMessage={
form.getFieldState("type", form.formState).error?.message
}
htmlFor="credential-type"
isRequired
tooltip={null}
>
<Select
onValueChange={(value) =>
form.setValue("type", value as CredentialType)
}
value={form.watch("type")}
>
<SelectTrigger className="bg-card">
<SelectValue />
</SelectTrigger>
<SelectContent className="z-[10001]">
<SelectGroup>
{CREDENTIAL_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormFieldSetup>
)}
{/* Label */}
<FormFieldSetup
label="Label"
errorMessage={
form.getFieldState("label", form.formState).error?.message
}
htmlFor="credential-label"
isRequired
tooltip={null}
>
<Input
id="credential-label"
type="text"
placeholder="A description to identify this credential"
className="bg-card"
{...form.register("label", { required: true })}
/>
</FormFieldSetup>
{/* Type-specific fields */}
{selectedType === "circle" && (
<CircleCredentialFields form={form} isUpdate={isUpdate} />
)}
</div>
</div>
<DialogFooter className="mt-4 gap-4 border-border border-t bg-card p-6 lg:gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
type="submit"
className="min-w-28 gap-2"
disabled={!form.formState.isValid || isPending}
>
{isPending && <Spinner className="size-4" />}
{submitButtonText}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,62 @@
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
import { Input } from "@/components/ui/input";
import Link from "next/link";
import type { UseFormReturn } from "react-hook-form";
import type { CredentialFormData, CredentialUpdateFormData } from "../types";
interface CircleCredentialFieldsProps {
form: UseFormReturn<CredentialFormData | CredentialUpdateFormData>;
isUpdate?: boolean;
}
export const CircleCredentialFields: React.FC<CircleCredentialFieldsProps> = ({
form,
isUpdate = false,
}) => {
return (
<>
<FormFieldSetup
label="Entity Secret"
errorMessage={
form.getFieldState("entitySecret", form.formState).error?.message
}
htmlFor="entity-secret"
isRequired={!isUpdate}
tooltip={null}
helperText={
<>
Entity Secret is a 32-byte private key designed to secure your
Developer-Controlled wallets{" "}
<Link
href="https://developers.circle.com/w3s/entity-secret-management"
target="_blank"
className="text-link-foreground hover:text-foreground"
>
Learn more about entity secret management
</Link>
</>
}
>
<Input
id="entity-secret"
type="password"
className="bg-card"
placeholder={
isUpdate
? "Leave empty to keep existing secret"
: "Your Circle entity secret"
}
{...form.register("entitySecret", {
required: !isUpdate,
pattern: {
value: /^([0-9a-fA-F]{64})?$/,
message:
"Entity secret must be a 32-byte hex string (64 characters)",
},
setValueAs: (value: string) => (value === "" ? undefined : value),
})}
/>
</FormFieldSetup>
</>
);
};
@@ -0,0 +1,97 @@
import { Button } from "@/components/ui/button";
import {
type WalletCredential,
useEngineUpdateWalletCredential,
} from "@3rdweb-sdk/react/hooks/useEngine";
import { useTrack } from "hooks/analytics/useTrack";
import { PencilIcon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { CredentialForm } from "./credential-form";
import type { CredentialUpdateFormData } from "./types";
interface EditWalletCredentialButtonProps {
credential: WalletCredential;
instanceUrl: string;
authToken: string;
}
export const EditWalletCredentialButton: React.FC<
EditWalletCredentialButtonProps
> = ({ credential, instanceUrl, authToken }) => {
const [isOpen, setIsOpen] = useState(false);
const trackEvent = useTrack();
const updateCredential = useEngineUpdateWalletCredential({
instanceUrl,
authToken,
});
const handleSubmit = async (data: CredentialUpdateFormData) => {
const promise = updateCredential.mutateAsync({
id: credential.id,
...data,
});
toast.promise(promise, {
success: () => {
setIsOpen(false);
trackEvent({
category: "engine",
action: "update-wallet-credential",
label: "success",
type: data.type,
});
return "Wallet credential updated successfully";
},
error: (error) => {
console.log(error);
trackEvent({
category: "engine",
action: "update-wallet-credential",
label: "error",
type: data.type,
error,
});
return "Failed to update wallet credential";
},
});
};
return (
<>
<Button
variant="ghost"
size="icon"
onClick={() => {
trackEvent({
category: "engine",
action: "open-edit-wallet-credential",
});
setIsOpen(true);
}}
className="size-8"
>
<PencilIcon className="size-4" />
</Button>
<CredentialForm
isOpen={isOpen}
onOpenChange={setIsOpen}
onSubmit={async (data) => {
return new Promise<void>((resolve) => {
handleSubmit(data as CredentialUpdateFormData).then(resolve);
});
}}
title="Edit Wallet Credential"
submitButtonText="Save Changes"
isPending={updateCredential.isPending}
defaultValues={{
type: credential.type as "circle",
label: credential.label || "",
}}
hideTypeSelect
isUpdate
/>
</>
);
};
@@ -0,0 +1,23 @@
interface BaseCredentialFormData {
type: string;
label: string;
}
type CircleCredentialFormData = BaseCredentialFormData & {
type: "circle";
entitySecret: string;
};
type CircleCredentialUpdateFormData = BaseCredentialFormData & {
type: "circle";
entitySecret?: string;
};
export type CredentialFormData = CircleCredentialFormData;
export type CredentialUpdateFormData = CircleCredentialUpdateFormData;
export const CREDENTIAL_TYPE_OPTIONS = [
{ value: "circle", label: "Circle" },
] as const;
export type CredentialType = (typeof CREDENTIAL_TYPE_OPTIONS)[number]["value"];
@@ -0,0 +1,106 @@
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import {} from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { WalletCredential } from "@3rdweb-sdk/react/hooks/useEngine";
import { format } from "date-fns";
import { EditWalletCredentialButton } from "./edit-wallet-credential-button";
interface WalletCredentialsTableProps {
credentials: WalletCredential[];
isPending: boolean;
isFetched: boolean;
instanceUrl: string;
authToken: string;
}
export const WalletCredentialsTable: React.FC<WalletCredentialsTableProps> = ({
credentials,
isPending,
isFetched,
instanceUrl,
authToken,
}) => {
if (isPending && !isFetched) {
return (
<div className="flex min-h-[300px] grow items-center justify-center">
<Spinner className="size-10" />
</div>
);
}
if (credentials.length === 0) {
return (
<div className="flex min-h-[300px] grow items-center justify-center">
<p className="text-muted-foreground text-sm">No credentials found</p>
</div>
);
}
return (
<TableContainer className="border-none">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Label</TableHead>
<TableHead>Type</TableHead>
<TableHead>Created</TableHead>
<TableHead>Updated</TableHead>
<TableHead>Default</TableHead>
<TableHead className="w-0" />
</TableRow>
</TableHeader>
<TableBody>
{credentials.map((credential) => (
<TableRow key={credential.id}>
<TableCell>
<CopyTextButton
textToCopy={credential.id}
textToShow={`${credential.id.slice(0, 8)}...`}
variant="secondary"
className="font-mono text-xs"
tooltip="Copy credential ID"
copyIconPosition="right"
/>
</TableCell>
<TableCell>{credential.label || "-"}</TableCell>
<TableCell>
<Badge variant="outline">{credential.type}</Badge>
</TableCell>
<TableCell>
{format(new Date(credential.createdAt), "MMM d, yyyy")}
</TableCell>
<TableCell>
{format(new Date(credential.updatedAt), "MMM d, yyyy")}
</TableCell>
<TableCell>
{credential.isDefault ? (
<Badge variant="success">Yes</Badge>
) : (
<Badge variant="outline">No</Badge>
)}
</TableCell>
<TableCell>
<EditWalletCredentialButton
credential={credential}
instanceUrl={instanceUrl}
authToken={authToken}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
@@ -0,0 +1,93 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
useEngineWalletCredentials,
useHasEngineFeature,
} from "@3rdweb-sdk/react/hooks/useEngine";
import { CircleAlertIcon } from "lucide-react";
import Link from "next/link";
import { CreateWalletCredentialButton } from "./create-wallet-credential-button";
import { WalletCredentialsTable } from "./wallet-credentials-table";
interface WalletCredentialsProps {
instanceUrl: string;
authToken: string;
}
export const WalletCredentialsSection: React.FC<WalletCredentialsProps> = ({
instanceUrl,
authToken,
}) => {
const {
data: credentials = [],
isPending,
isFetched,
} = useEngineWalletCredentials({
instanceUrl,
authToken,
});
const {
isSupported: supportsWalletCredentials,
query: { isPending: isFeatureLoading },
} = useHasEngineFeature(instanceUrl, "WALLET_CREDENTIALS");
if (isFeatureLoading) {
return (
<div className="flex min-h-[500px] grow items-center justify-center rounded-lg border border-border">
<Spinner className="size-10" />
</div>
);
}
if (!supportsWalletCredentials) {
return (
<Alert variant="warning">
<CircleAlertIcon className="size-4" />
<AlertTitle>Update Required</AlertTitle>
<AlertDescription>
Wallet credentials support requires a newer version of Engine. Please
update your Engine instance to use this feature.
</AlertDescription>
</Alert>
);
}
return (
<section className="rounded-lg border border-border bg-card">
<div className="flex flex-col gap-5 border-border border-b p-6 lg:flex-row lg:items-center lg:justify-between">
<div>
<h2 className="mb-1 font-semibold text-xl tracking-tight">
Wallet Credentials
</h2>
<p className="text-muted-foreground text-sm leading-relaxed">
Manage your wallet credentials for different providers.{" "}
<Link
href="https://portal.thirdweb.com/infrastructure/engine/features/wallet-credentials"
target="_blank"
>
Learn more about wallet credentials.
</Link>
</p>
</div>
<div className="flex flex-col items-end gap-4 border-border max-md:w-full max-md:border-t max-md:pt-6">
<CreateWalletCredentialButton
instanceUrl={instanceUrl}
authToken={authToken}
/>
</div>
</div>
<WalletCredentialsTable
credentials={credentials}
isPending={isPending}
isFetched={isFetched}
instanceUrl={instanceUrl}
authToken={authToken}
/>
</section>
);
};
@@ -0,0 +1,18 @@
import { engineInstancePageHandler } from "../../../_utils/getEngineInstancePageMeta";
import type { EngineInstancePageProps } from "../types";
import { WalletCredentialsSection } from "./components/wallet-credentials";
export default async function Page(props: EngineInstancePageProps) {
const params = await props.params;
const { instance, authToken } = await engineInstancePageHandler({
engineId: params.engineId,
teamSlug: params.team_slug,
});
return (
<WalletCredentialsSection
instanceUrl={instance.url}
authToken={authToken}
/>
);
}
@@ -1,25 +1,45 @@
import { getTeamBySlug } from "@/api/team";
import { redirect } from "next/navigation";
import { getValidAccount } from "../../../../../account/settings/getAccount";
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
import { loginRedirect } from "../../../../../login/loginRedirect";
import { NebulaAnalyticsPage } from "../../../[project_slug]/nebula/components/analytics/nebula-analytics-ui";
import { NebulaWaitListPage } from "../../../[project_slug]/nebula/components/nebula-waitlist-page";
export default async function Page(props: {
params: Promise<{
team_slug: string;
}>;
searchParams: Promise<{
from: string | undefined | string[];
to: string | undefined | string[];
interval: string | undefined | string[];
}>;
}) {
const params = await props.params;
const team = await getTeamBySlug(params.team_slug);
const [params, searchParams] = await Promise.all([
props.params,
props.searchParams,
]);
if (!team) {
const [account, authToken, team] = await Promise.all([
getValidAccount(),
getAuthToken(),
getTeamBySlug(params.team_slug),
]);
if (!team || !authToken) {
loginRedirect(`/team/${params.team_slug}/~/nebula`);
}
// if nebula access is already granted, redirect to nebula web app
const hasNebulaAccess = team.enabledScopes.includes("nebula");
if (hasNebulaAccess) {
redirect("https://nebula.thirdweb.com");
return (
<NebulaAnalyticsPage
accountId={account.id}
authToken={authToken}
searchParams={searchParams}
/>
);
}
return <NebulaWaitListPage team={team} />;
@@ -7,7 +7,7 @@ import { getTeamSettingsLinks } from "./getTeamSettingsLinks";
export function TeamSettingsSidebar(props: {
team: Team;
account: Pick<Account, "id"> | undefined;
account: Pick<Account, "id" | "image"> | undefined;
client: ThirdwebClient;
}) {
const teamLinks = getTeamSettingsLinks(props.team.slug);
@@ -34,7 +34,7 @@ export function TeamSettingsSidebar(props: {
team={props.team}
titleAvatarIcon={{
id: props.account?.id,
src: "", // TODO - set account image
src: props.account?.image || "",
}}
client={props.client}
/>
@@ -60,7 +60,7 @@ function RenderLinkGroup(props: {
<div className="flex items-center gap-1.5 px-4">
<GradientAvatar
src={props.titleAvatarIcon.src}
className="size-4"
className="size-[18px]"
id={props.titleAvatarIcon.id}
client={props.client}
/>
@@ -1,24 +1,22 @@
"use client";
import type { Team } from "@/api/team";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ThirdwebClient } from "thirdweb";
import { upload } from "thirdweb/storage";
import { TeamGeneralSettingsPageUI } from "./TeamGeneralSettingsPageUI";
import { updateTeam } from "./updateTeam";
export function TeamGeneralSettingsPage(props: {
team: Team;
authToken: string;
client: ThirdwebClient;
}) {
const router = useDashboardRouter();
const client = useThirdwebClient();
return (
<TeamGeneralSettingsPageUI
team={props.team}
client={client}
client={props.client}
updateTeamField={async (teamValue) => {
await updateTeam({
teamId: props.team.id,
@@ -38,7 +36,7 @@ export function TeamGeneralSettingsPage(props: {
if (file) {
// upload to IPFS
uri = await upload({
client: getThirdwebClient(props.authToken),
client: props.client,
files: [file],
});
}
@@ -188,7 +188,7 @@ function TeamAvatarFormControl(props: {
disabled: false,
isPending: updateTeamAvatarMutation.isPending,
}}
noPermissionText={undefined} // TODO
noPermissionText={undefined}
errorText={undefined}
>
<div className="flex flex-row gap-4 md:justify-between">
@@ -1,4 +1,5 @@
import { getTeamBySlug } from "@/api/team";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { notFound } from "next/navigation";
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
import { TeamGeneralSettingsPage } from "./general/TeamGeneralSettingsPage";
@@ -14,5 +15,7 @@ export default async function Page(props: {
notFound();
}
return <TeamGeneralSettingsPage team={team} authToken={token} />;
return (
<TeamGeneralSettingsPage team={team} client={getThirdwebClient(token)} />
);
}
@@ -1,5 +1,5 @@
import { getProjects } from "@/api/projects";
import { getTeamNebulaWaitList, getTeams } from "@/api/team";
import { getTeams } from "@/api/team";
import { notFound, redirect } from "next/navigation";
import { getValidAccount } from "../../../account/settings/getAccount";
import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client";
@@ -45,9 +45,6 @@ export default async function TeamLayout(props: {
redirect(`/team/${params.team_slug}`);
}
const isOnNebulaWaitList = (await getTeamNebulaWaitList(team.slug))
?.onWaitlist;
return (
<div className="flex grow flex-col">
<div className="bg-card">
@@ -59,7 +56,6 @@ export default async function TeamLayout(props: {
/>
<ProjectTabs
layoutPath={`/team/${params.team_slug}/${params.project_slug}`}
isOnNebulaWaitList={!!isOnNebulaWaitList}
/>
</div>
<div className="flex grow flex-col">{props.children}</div>
@@ -0,0 +1,52 @@
import "server-only";
import { unstable_cache } from "next/cache";
export type NebulaAnalyticsDataItem = {
date: string;
totalPromptTokens: number;
totalCompletionTokens: number;
totalSessions: number;
totalRequests: number;
};
export const fetchNebulaAnalytics = unstable_cache(
async (params: {
accountId: string;
authToken: string;
from: string;
to: string;
interval: "day" | "week";
}) => {
const analyticsEndpoint = process.env.ANALYTICS_SERVICE_URL as string;
const url = new URL(`${analyticsEndpoint}/v1/nebula/usage`);
url.searchParams.set("accountId", params.accountId);
url.searchParams.set("from", params.from);
url.searchParams.set("to", params.to);
url.searchParams.set("interval", params.interval);
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${params.authToken}`,
},
});
if (!res.ok) {
const error = await res.text();
return {
ok: false as const,
error: error,
};
}
const resData = await res.json();
return {
ok: true as const,
data: resData.data as NebulaAnalyticsDataItem[],
};
},
["nebula-analytics"],
{
revalidate: 60 * 60, // 1 hour
},
);
@@ -0,0 +1,53 @@
"use client";
import {
useResponsiveSearchParams,
useSetResponsiveSearchParams,
} from "responsive-rsc";
import { DateRangeSelector } from "../../../../../../../components/analytics/date-range-selector";
import { IntervalSelector } from "../../../../../../../components/analytics/interval-selector";
import {
getNebulaFiltersFromSearchParams,
normalizeTimeISOString,
} from "../../../../../../../lib/time";
export function NebulaAnalyticsFilter() {
const responsiveSearchParams = useResponsiveSearchParams();
const setResponsiveSearchParams = useSetResponsiveSearchParams();
const { range, interval } = getNebulaFiltersFromSearchParams({
from: responsiveSearchParams.from,
to: responsiveSearchParams.to,
interval: responsiveSearchParams.interval,
});
return (
<div className="flex items-center gap-3">
<DateRangeSelector
range={range}
popoverAlign="end"
setRange={(newRange) => {
setResponsiveSearchParams((v) => {
return {
...v,
from: normalizeTimeISOString(newRange.from),
to: normalizeTimeISOString(newRange.to),
};
});
}}
/>
<IntervalSelector
intervalType={interval}
setIntervalType={(newInterval) => {
setResponsiveSearchParams((v) => {
return {
...v,
interval: newInterval,
};
});
}}
/>
</div>
);
}
@@ -0,0 +1,115 @@
import { TabButtons } from "@/components/ui/tabs";
import type { Meta, StoryObj } from "@storybook/react";
import { subDays } from "date-fns";
import { useState } from "react";
import { mobileViewport } from "../../../../../../../stories/utils";
import type { NebulaAnalyticsDataItem } from "./fetch-nebula-analytics";
import { NebulaAnalyticsDashboardUI } from "./nebula-analytics-ui";
const meta = {
title: "Nebula/Analytics",
component: Story,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
type VariantTab = "30-day" | "7-day" | "pending" | "60-day";
function Story() {
const [tab, setTab] = useState<VariantTab>("60-day");
return (
<div className="container flex flex-col gap-8 py-10">
<div>
<h2 className="mb-1 font-semibold text-xl tracking-tight">
Story Variants
</h2>
<TabButtons
tabs={[
{
name: "60 Days",
onClick: () => setTab("60-day"),
isActive: tab === "60-day",
isEnabled: true,
},
{
name: "30 Days",
onClick: () => setTab("30-day"),
isActive: tab === "30-day",
isEnabled: true,
},
{
name: "7 Days",
onClick: () => setTab("7-day"),
isActive: tab === "7-day",
isEnabled: true,
},
{
name: "Pending",
onClick: () => setTab("pending"),
isActive: tab === "pending",
isEnabled: true,
},
]}
/>
</div>
{tab === "60-day" && (
<NebulaAnalyticsDashboardUI
data={generateRandomNebulaAnalyticsData(60)}
isPending={false}
/>
)}
{tab === "30-day" && (
<NebulaAnalyticsDashboardUI
data={generateRandomNebulaAnalyticsData(30)}
isPending={false}
/>
)}
{tab === "7-day" && (
<NebulaAnalyticsDashboardUI
data={generateRandomNebulaAnalyticsData(7)}
isPending={false}
/>
)}
{tab === "pending" && (
<NebulaAnalyticsDashboardUI data={[]} isPending={true} />
)}
</div>
);
}
function generateRandomNebulaAnalyticsData(
days: number,
): NebulaAnalyticsDataItem[] {
return Array.from({ length: days }, (_, i) => ({
date: subDays(new Date(), i).toISOString(),
totalPromptTokens: randomInt(1000),
totalCompletionTokens: randomInt(1000),
totalSessions: randomInt(100),
totalRequests: randomInt(4000),
}));
}
function randomInt(max: number) {
return Math.floor(Math.random() * max);
}
@@ -0,0 +1,257 @@
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
import { SkeletonContainer } from "@/components/ui/skeleton";
import {
ActivityIcon,
MessageCircleQuestionIcon,
MessageSquareIcon,
MessageSquareQuoteIcon,
} from "lucide-react";
import { useMemo } from "react";
import {
ResponsiveSearchParamsProvider,
ResponsiveSuspense,
} from "responsive-rsc";
import { normalizeTimeISOString } from "../../../../../../../lib/time";
import {
type NebulaAnalyticsDataItem,
fetchNebulaAnalytics,
} from "./fetch-nebula-analytics";
import { NebulaAnalyticsFilter } from "./nebula-analytics-filter";
import { getNebulaAnalyticsRangeFromSearchParams } from "./utils";
type ChartData = {
time: Date;
totalPromptTokens: number;
totalCompletionTokens: number;
totalSessions: number;
totalRequests: number;
};
export function NebulaAnalyticsPage(props: {
searchParams: {
from: string | undefined | string[];
to: string | undefined | string[];
interval: string | undefined | string[];
};
accountId: string;
authToken: string;
}) {
return (
<ResponsiveSearchParamsProvider value={props.searchParams}>
<header className="border-b">
<div className="container flex flex-col items-start gap-3 py-10 md:flex-row md:items-center">
<div className="flex-1">
<h1 className="font-semibold text-2xl tracking-tight md:text-3xl">
Nebula
</h1>
</div>
<NebulaAnalyticsFilter />
</div>
</header>
<div className="container pt-8 pb-20">
<ResponsiveSuspense
searchParamsUsed={["from", "to", "interval"]}
fallback={<NebulaAnalyticsDashboardUI data={[]} isPending={true} />}
>
<NebulaAnalyticDashboard
searchParams={props.searchParams}
accountId={props.accountId}
authToken={props.authToken}
/>
</ResponsiveSuspense>
</div>
</ResponsiveSearchParamsProvider>
);
}
async function NebulaAnalyticDashboard(props: {
accountId: string;
authToken: string;
searchParams: {
from: string | undefined | string[];
to: string | undefined | string[];
interval: string | undefined | string[];
};
}) {
const { range, interval } = getNebulaAnalyticsRangeFromSearchParams(
props.searchParams,
);
const res = await fetchNebulaAnalytics({
accountId: props.accountId,
authToken: props.authToken,
from: normalizeTimeISOString(range.from),
to: normalizeTimeISOString(range.to),
interval,
});
if (!res.ok) {
return (
<div className="flex min-h-[300px] grow items-center justify-center rounded-lg border">
<div>
<p className="mb-2 text-center text-destructive-text">
Failed to fetch Nebula analytics
</p>
<p className="text-muted-foreground">{res.error}</p>
</div>
</div>
);
}
return <NebulaAnalyticsDashboardUI data={res.data} isPending={false} />;
}
export function NebulaAnalyticsDashboardUI(props: {
data: NebulaAnalyticsDataItem[];
isPending: boolean;
}) {
const data = useMemo(() => {
const val: {
totalPromptTokens: number;
totalCompletionTokens: number;
totalSessions: number;
totalRequests: number;
chartData: ChartData[];
} = {
totalPromptTokens: 0,
totalCompletionTokens: 0,
totalSessions: 0,
totalRequests: 0,
chartData: [],
};
for (const item of props.data) {
val.totalPromptTokens += item.totalPromptTokens;
val.totalCompletionTokens += item.totalCompletionTokens;
val.totalSessions += item.totalSessions;
val.totalRequests += item.totalRequests;
val.chartData.push({
totalPromptTokens: item.totalPromptTokens,
totalCompletionTokens: item.totalCompletionTokens,
totalSessions: item.totalSessions,
totalRequests: item.totalRequests,
time: new Date(item.date),
});
}
return val;
}, [props.data]);
return (
<div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
<StatCard
title="Requests sent"
value={data.totalRequests}
icon={ActivityIcon}
isPending={props.isPending}
/>
<StatCard
title="Sessions created"
value={data.totalSessions}
icon={MessageSquareIcon}
isPending={props.isPending}
/>
<StatCard
title="Prompt tokens received"
value={data.totalPromptTokens}
icon={MessageCircleQuestionIcon}
isPending={props.isPending}
/>
<StatCard
title="Response tokens sent"
value={data.totalCompletionTokens}
icon={MessageSquareQuoteIcon}
isPending={props.isPending}
/>
</div>
<div className="h-8" />
<div className="flex flex-col gap-8">
<ThirdwebBarChart
data={data.chartData}
isPending={props.isPending}
title="Requests sent"
description="Total number of requests sent to the Nebula from users"
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
config={{
totalRequests: {
label: "Total requests",
color: "hsl(var(--chart-1))",
},
}}
/>
<ThirdwebBarChart
data={data.chartData}
isPending={props.isPending}
title="Sessions created"
description="Total chat sessions created by users. A session is a conversation thread between a user and the Nebula"
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
config={{
totalSessions: {
label: "Total sessions",
color: "hsl(var(--chart-3))",
},
}}
/>
<ThirdwebBarChart
data={data.chartData}
isPending={props.isPending}
title="Prompt tokens received"
description="Total tokens sent from users to Nebula as prompts"
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
config={{
totalPromptTokens: {
label: "Total prompt tokens",
color: "hsl(var(--chart-2))",
},
}}
/>
<ThirdwebBarChart
data={data.chartData}
isPending={props.isPending}
title="Response tokens sent"
description="Total tokens sent from Nebula to users as responses"
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
config={{
totalCompletionTokens: {
label: "Total response tokens",
color: "hsl(var(--chart-5))",
},
}}
/>
</div>
</div>
);
}
function StatCard(props: {
title: string;
value: number;
icon: React.FC<{ className?: string }>;
isPending: boolean;
}) {
return (
<div className="rounded-lg border bg-card p-4">
<div className="mb-1 flex items-center justify-between">
<h2 className="text-sm lg:text-base">{props.title}</h2>
<props.icon className="size-4 text-muted-foreground" />
</div>
<SkeletonContainer
className="inline-block"
skeletonData={10000}
loadedData={props.isPending ? undefined : props.value}
render={(v) => (
<p className="font-semibold text-2xl tracking-tight lg:text-3xl">
{v}
</p>
)}
/>
</div>
);
}
@@ -0,0 +1,13 @@
import { getNebulaFiltersFromSearchParams } from "../../../../../../../lib/time";
export function getNebulaAnalyticsRangeFromSearchParams(searchParams: {
from: string | undefined | string[];
to: string | undefined | string[];
interval: string | undefined | string[];
}) {
return getNebulaFiltersFromSearchParams({
from: searchParams.from,
to: searchParams.to,
interval: searchParams.interval,
});
}

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