Compare commits

...
Author SHA1 Message Date
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
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
073b25afd7 Version Packages (#6083)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-28 17:40:13 -08:00
Joaquim VergesandGitHub 4550bb2663 [SDK] feat: Expose Account and Wallet components in React Native (#6082) 2025-01-28 16:29:50 -08:00
Manan TankandGitHub f5f8a40320 [TOOL-3251] Dashboard: Add Rotate Secret Key feature in Project Settings page (#6087) 2025-01-28 14:21:25 -08:00
Joaquim VergesandGitHub 3569763501 [Dashboard] fix: Use API key for in-app wallet analytics (#6086) 2025-01-29 09:44:02 +13:00
Joaquim VergesandGitHub 1401f8d039 [SDK] fix: Auto-connection of in-app wallets in React Native (#6076) 2025-01-29 07:13:09 +13:00
Jonas DanielsandGitHub 1616b7f619 update dependencies (#6079) 2025-01-27 23:08:39 -08:00
saminaandGitHub aa548a359f Nebula docs (#6080) 2025-01-27 19:22:38 -08:00
Jonas DanielsandGitHub eef52dfa7e update plan selection copy and tracking (#6078) 2025-01-27 16:56:30 -08:00
Joaquim VergesandGitHub 19ec571500 chore: Update PR template format (#6077) 2025-01-28 13:13:07 +13:00
Jonas DanielsandGitHub 47dce2e05c remove growth trial eligibility (#6070) 2025-01-27 15:13:51 -08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
8b4695224c Version Packages (#6075)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-28 12:00:53 +13:00
Joaquim VergesandGitHub 94e2d5b8dd [SDK] fix: Account linking for ecosystem smart wallets (#6074) 2025-01-28 11:44:05 +13:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
40fa323294 Version Packages (#6073)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-28 11:02:56 +13:00
Joaquim VergesandGitHub 2cebb420e6 [SDK] fix: Show link profiles for ecosystem smart wallets (#6072) 2025-01-28 10:59:05 +13:00
Joaquim VergesandGitHub e89ddb24f2 [SDK] fix: Update ecosystem name and admin wallet handling (#6071) 2025-01-28 10:54:27 +13:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
86f205e2aa Version Packages (#6069)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-28 10:14:31 +13:00
Joaquim VergesandGitHub 6c277ae676 [SDK] fix: Respect chain param in in-app wallet connection (#6068) 2025-01-28 09:57:07 +13:00
MananTank 153651df35 [TOOL-3241] Fix previous account shown after logging out in onboarding flow (#6061) 2025-01-27 19:49:51 +00:00
MananTank 8ba460b6fc [NEB-77] Nebula: Various UI updates (#6060) 2025-01-27 18:18:13 +00:00
MananTank 1e61a9edfc [TOOL-3238] Fix Engine backend wallet balances not updating on chain change (#6059) 2025-01-27 17:00:48 +00:00
MananTank d03e687aa9 [TOOL-3231] Show View Project Link after creating a project in Modal (#6055)
<!-- start pr-codex -->

## PR-Codex overview
This PR introduces the `teamSlug` prop across multiple components to enhance API key management by associating it with specific teams. This allows for better project handling and user navigation within the dashboard.

### Detailed summary
- Added `teamSlug` prop to `CreateAPIKeyModal` and related components.
- Updated `embed-setup.tsx` to handle `teamSlug` as `undefined`.
- Integrated `teamSlug` in `TeamProjectsPage` and `AccountHeader`.
- Modified `CreateAPIKeyDialog` to include `teamSlug` in props.
- Enhanced `APIKeyDetails` to fetch projects based on `teamSlug`.
- Implemented conditional rendering for "View Project" button based on `teamSlug`.

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

<!-- end pr-codex -->
2025-01-27 09:52:19 +00:00
Joaquim VergesandGitHub e1c0dbe19e [SDK] chore: Increase CJS bundle size limit to 130kb (#6058) 2025-01-27 20:42:56 +13:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
3e7df081d7 Version Packages (#6048)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-27 20:41:53 +13:00
Joaquim VergesandGitHub b6f189c220 [SDK] update dependencies (#6057) 2025-01-27 16:48:24 +13:00
Phillip HoandGitHub fb4d0a26f1 [service-utils] Replace client_id with project_id in usage_v2 (#6050) 2025-01-24 17:02:15 +08:00
Phillip HoandGitHub bc345371ba [service-utils] usage_v2 accepts a project_id field (#6049) 2025-01-24 16:44:26 +08:00
Phillip HoandGitHub 7a8e4e5f86 [service-utils] Export UsageV2 helpers (#6047) 2025-01-24 15:55:41 +08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
6ba3b30b60 Version Packages (#6030)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-24 15:08:14 +13:00
Joaquim VergesandGitHub 5ad442cf0c [SDK] fix: Simplify in-app wallet logout flow (#6046) 2025-01-24 14:49:15 +13:00
Phillip HoandGitHub 5756c74a71 [service-utils] feat: Add usageV2 support (#6021) 2025-01-24 08:52:35 +08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a1cff7a579 build(deps-dev): bump checkly from 4.15.0 to 4.18.0 (#6045)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-24 13:46:30 +13:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
eafaefec20 build(deps-dev): bump vite from 6.0.10 to 6.0.11 in the npm_and_yarn group (#6022)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-24 13:37:31 +13:00
Manan TankandGitHub b81d7a1854 [TOOL-3207] Portal: Delete V4 SDK pages, Setup knip lint, Fix lint config (#6039) 2025-01-24 13:37:03 +13:00
Winston YeoandGitHub 21cc45adc2 Winston/tool 3161 cannot create backend wallets using latest version (#6025) 2025-01-24 13:36:33 +13:00
Joaquim VergesandGitHub a67d342779 [SDK] fix: Improve smart wallet signature verification (#6040) 2025-01-24 13:35:58 +13:00
Joaquim VergesandGitHub 60edce49c0 [SDK] fix: Improve WalletConnect mobile connection handling (#6042) 2025-01-24 13:35:31 +13:00
Joaquim VergesandGitHub 20c0dca958 [Portal] fix: Update useAutoConnect hook reference link (#6043) 2025-01-24 13:35:11 +13:00
Joaquim VergesandGitHub 4b30b847e2 [Dashboard] fix: Use formatted ABI for contract function calls (#6044) 2025-01-24 13:34:47 +13:00
MananTank b2cc17fd1e [TOOL-3220] Dashboard: Reset Project pagination on filter change, SDK: Fix WalletProvider JSDoc (#6041) 2025-01-23 23:58:47 +00:00
MananTank ace1dc5b09 [TOOL-3124] Dashboard: paginate projects, sort by Montly active users (#6027)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the project analytics in the dashboard by changing how project data is retrieved and displayed. It updates the calculation of user connections to reflect monthly active users and introduces pagination for project listings.

### Detailed summary
- Replaced `getProjectsWithTotalWallets` with `getProjectsWithAnalytics`.
- Updated project data structure to include `monthlyActiveUsers` instead of `totalConnections`.
- Introduced pagination in the `TeamProjectsPage` component.
- Changed sorting criteria from `totalConnections` to `monthlyActiveUsers`.
- Updated UI elements to reflect the new data fields.

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

<!-- end pr-codex -->
2025-01-23 12:09:59 +00:00
Joaquim VergesandGitHub 9f79277344 [Dashboard] perf: Optimize chain and contract data processing (#6035) 2025-01-23 13:47:14 +13:00
MananTank 6e0fd205ff [TOOL-2964] Dashboard: Fix crash on unix time converter page for invalid unix time, UI improvements (#6036)
=
2025-01-22 23:58:04 +00:00
MananTank 69b1ee9176 [TOOL-3082] Dashboard: Replace NetworkSelectorButton with SingleNetworkSelector where switching chain in wallet is not required (#6031)
<!-- start pr-codex -->

## PR-Codex overview
This PR introduces enhancements to the `SingleNetworkSelector` component and its usage across various files. It adds new props for alignment and positioning, replaces existing network selector buttons, and integrates chain ID handling in several components.

### Detailed summary
- Added `side`, `disableChainId`, and `align` props to `SingleNetworkSelector`.
- Updated `SingleNetworkSelector` to conditionally render the chain ID badge.
- Replaced `NetworkSelectorButton` with `SingleNetworkSelector` in multiple components.
- Integrated `chainId` state management using `useState` in `BackendWalletsSection` and `ImportForm`.
- Updated `BackendWalletsTable` and `BackendWalletBalanceCell` to accept `chainId` as a prop.
- Changed the way active chains are retrieved with `useActiveChainAsDashboardChain` to `useV5DashboardChain`.

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

<!-- end pr-codex -->
2025-01-22 23:48:14 +00:00
MananTank 0a19142a24 [TOOL-3120] Redirect to last visited team page if authenticated (#6028) 2025-01-22 23:39:05 +00:00
MananTank feb4fd0908 [TOOL-3123] Dashboard: Order projects by total wallets (#6026)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the `CopyTextButton` component, optimizing the project fetching logic, and improving project display features by integrating total wallet connections in the dashboard.

### Detailed summary
- Changed `tooltip` type in `CopyTextButton` to `string | undefined`.
- Updated the API call in `getChangelog` to limit results to 7.
- Replaced cookie-based token retrieval with `getAuthToken` in `getProjects`.
- Added `getProjectsWithTotalWallets` to calculate total wallet connections.
- Updated `TeamProjectsPage` to display projects with total connections.
- Modified sorting logic in `TeamProjectsPage` to include total connections.
- Replaced `CopyButton` with `CopyTextButton` in `ProjectCard`.
- Enhanced project display to show total users.

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

<!-- end pr-codex -->
2025-01-22 23:30:11 +00:00
gregfromstl 1d8695eb42 [SDK] Fix: Opt out of 1559 on lumia mainnet (#6032)
<!-- start pr-codex -->

## PR-Codex overview
This PR adds support for the `Lumia Mainnet` in the fee data configuration.

### Detailed summary
- Added a new entry for `Lumia Mainnet` in the fee data array.

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

<!-- end pr-codex -->
2025-01-22 22:34:34 +00:00
Joaquim VergesandGitHub 858dc53a25 [Dashboard] fix: Add error reason to analytics API error logs (#6034) 2025-01-23 11:34:16 +13:00
Joaquim VergesandGitHub 8997fac1b0 [Dashboard] fix: Improve error logging for analytics API failures (#6033) 2025-01-23 11:25:35 +13:00
gregfromstl 403e4224e4 Chore: Remove Jakub from CODEOWNERS (#6029)
<!-- start pr-codex -->

## PR-Codex overview
This PR modifies the `.github/CODEOWNERS` file to remove a specific code owner from the `apps/` directory.

### Detailed summary
- Removed `@jakubkrehel` as a code owner for the `apps/` directory.
- Retained `@thirdweb-dev/developer-tools` and `@thirdweb-dev/core-platform` as code owners for the `apps/` directory.
- `scripts/` code ownership remains unchanged with `@jnsdls` and `@joaquim-verges`.

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

<!-- end pr-codex -->
2025-01-22 20:17:33 +00:00
Phillip HoandGitHub de039ef462 [service-utils] fix: Don't hash cache key (#6023) 2025-01-23 04:16:16 +08:00
Joaquim VergesandGitHub 35fd3bcafa [Dashboard] feat: Add transaction analytics and tracking (#6019) 2025-01-23 08:36:04 +13:00
MananTank 80a22ff45e [TOOL-3173] Portal: Update hero iamge, OG image on landing page (#6017) 2025-01-22 21:46:05 +05:30
755 changed files with 16577 additions and 746520 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ packages/eslint-config-thirdweb/ @thirdweb-dev/core-platform @edwardysun
packages/tw-tsconfig/ @thirdweb-dev/core-platform @edwardysun
# apps
apps/ @thirdweb-dev/developer-tools @thirdweb-dev/core-platform @jakubkrehel
apps/ @thirdweb-dev/developer-tools @thirdweb-dev/core-platform
# .github folder + .changeset + turbo config is owned by jonas for now
scripts/ @jnsdls @joaquim-verges
+6 -3
View File
@@ -1,12 +1,15 @@
---
title: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"
---
<!--
## 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.
-->
+5 -1
View File
@@ -27,5 +27,9 @@
},
"[css]": {
"editor.defaultFormatter": "biomejs.biome"
}
},
"eslint.workingDirectories": [
{ "pattern": "./packages/*/" },
{ "pattern": "./apps/*/" }
]
}
+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
+42 -42
View File
@@ -31,29 +31,29 @@
"@marsidev/react-turnstile": "^1.1.0",
"@n8tb1t/use-scroll-position": "^2.0.3",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-alert-dialog": "^1.1.5",
"@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dialog": "1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.3",
"@radix-ui/react-hover-card": "^1.1.3",
"@radix-ui/react-dialog": "1.1.5",
"@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-hover-card": "^1.1.5",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.3",
"@radix-ui/react-popover": "^1.1.5",
"@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-radio-group": "^1.2.2",
"@radix-ui/react-select": "^2.1.3",
"@radix-ui/react-select": "^2.1.5",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tooltip": "1.1.5",
"@sentry/nextjs": "8.45.1",
"@shazow/whatsabi": "^0.18.0",
"@tanstack/react-query": "5.62.16",
"@radix-ui/react-tooltip": "1.1.7",
"@sentry/nextjs": "8.53.0",
"@shazow/whatsabi": "^0.19.0",
"@tanstack/react-query": "5.66.0",
"@tanstack/react-table": "^8.20.6",
"@thirdweb-dev/service-utils": "workspace:*",
"@vercel/functions": "^1.5.2",
"@vercel/og": "^0.6.4",
"abitype": "1.0.7",
"@vercel/functions": "^1.6.0",
"@vercel/og": "^0.6.5",
"abitype": "1.0.8",
"chakra-react-select": "^4.7.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -62,19 +62,19 @@
"date-fns": "4.1.0",
"fetch-event-stream": "0.1.5",
"flat": "^6.0.1",
"framer-motion": "11.15.0",
"framer-motion": "12.0.6",
"fuse.js": "7.0.0",
"input-otp": "^1.4.1",
"ioredis": "^5.4.1",
"ipaddr.js": "^2.2.0",
"lucide-react": "0.468.0",
"next": "15.1.3",
"lucide-react": "0.474.0",
"next": "15.1.6",
"next-plausible": "^3.12.4",
"next-themes": "^0.4.4",
"nextjs-toploader": "^1.6.12",
"openapi-types": "^12.1.3",
"p-limit": "^6.2.0",
"papaparse": "^5.4.1",
"papaparse": "^5.5.2",
"pluralize": "^8.0.0",
"posthog-js": "1.67.1",
"qrcode": "^1.5.3",
@@ -83,17 +83,17 @@
"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.14.1",
"recharts": "2.15.1",
"remark-gfm": "^4.0.0",
"server-only": "^0.0.1",
"shiki": "1.27.0",
"sonner": "^1.7.1",
"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:*",
@@ -103,42 +103,42 @@
},
"devDependencies": {
"@chakra-ui/cli": "^2.4.1",
"@chromatic-com/storybook": "3.2.2",
"@next/bundle-analyzer": "15.1.0",
"@next/eslint-plugin-next": "15.1.3",
"@playwright/test": "1.49.1",
"@storybook/addon-essentials": "8.4.7",
"@storybook/addon-interactions": "8.4.7",
"@storybook/addon-links": "8.4.7",
"@storybook/addon-onboarding": "8.4.7",
"@storybook/addon-viewport": "8.4.7",
"@storybook/blocks": "8.4.7",
"@storybook/nextjs": "8.4.7",
"@storybook/react": "8.5.0",
"@storybook/test": "8.4.7",
"@chromatic-com/storybook": "3.2.4",
"@next/bundle-analyzer": "15.1.6",
"@next/eslint-plugin-next": "15.1.6",
"@playwright/test": "1.50.1",
"@storybook/addon-essentials": "8.5.2",
"@storybook/addon-interactions": "8.5.2",
"@storybook/addon-links": "8.5.2",
"@storybook/addon-onboarding": "8.5.2",
"@storybook/addon-viewport": "8.5.2",
"@storybook/blocks": "8.5.2",
"@storybook/nextjs": "8.5.2",
"@storybook/react": "8.5.2",
"@storybook/test": "8.5.2",
"@types/color": "4.2.0",
"@types/node": "22.10.2",
"@types/node": "22.13.0",
"@types/papaparse": "^5.3.15",
"@types/pluralize": "^0.0.33",
"@types/qrcode": "^1.5.5",
"@types/react": "19.0.2",
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"@types/react-table": "^7.7.20",
"@types/spdx-correct": "^3.1.3",
"@types/swagger-ui-react": "^4.18.3",
"@types/swagger-ui-react": "^4.19.0",
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
"autoprefixer": "^10.4.19",
"checkly": "^4.15.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.41.0",
"knip": "5.43.6",
"next-sitemap": "^4.2.3",
"postcss": "8.4.49",
"storybook": "8.4.7",
"postcss": "8.5.1",
"storybook": "8.5.2",
"tailwindcss": "3.4.17",
"typescript": "5.7.2"
"typescript": "5.7.3"
}
}
+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();
+29 -1
View File
@@ -3,6 +3,7 @@ import type {
AnalyticsQueryParams,
InAppWalletStats,
RpcMethodStats,
TransactionStats,
UserOpStats,
WalletStats,
WalletUserStats,
@@ -77,7 +78,10 @@ export async function getUserOpUsage(
});
if (res?.status !== 200) {
console.error("Failed to fetch user ops usage");
const reason = await res?.text();
console.error(
`Failed to fetch user ops usage: ${res?.status} - ${res.statusText} - ${reason}`,
);
return [];
}
@@ -85,6 +89,30 @@ export async function getUserOpUsage(
return json.data as UserOpStats[];
}
export async function getClientTransactions(
params: AnalyticsQueryParams,
): Promise<TransactionStats[]> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v1/transactions/client?${searchParams.toString()}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
if (res?.status !== 200) {
const reason = await res?.text();
console.error(
`Failed to fetch client transactions stats: ${res?.status} - ${res.statusText} - ${reason}`,
);
return [];
}
const json = await res.json();
return json.data as TransactionStats[];
}
export async function getRpcMethodUsage(
params: AnalyticsQueryParams,
): Promise<RpcMethodStats[]> {
+3 -12
View File
@@ -1,7 +1,6 @@
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 { getAuthToken } from "../../app/api/lib/getAuthToken";
export type Project = {
id: string;
@@ -21,11 +20,7 @@ export type Project = {
};
export async function getProjects(teamSlug: string) {
const cookiesManager = await cookies();
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
const token = activeAccount
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
: null;
const token = await getAuthToken();
if (!token) {
return [];
@@ -46,11 +41,7 @@ export async function getProjects(teamSlug: string) {
}
export async function getProject(teamSlug: string, projectSlug: string) {
const cookiesManager = await cookies();
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
const token = activeAccount
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
: null;
const token = await getAuthToken();
if (!token) {
return null;
+1 -1
View File
@@ -25,7 +25,7 @@ export type Team = {
billingPlan: "pro" | "growth" | "free" | "starter";
billingStatus: "validPayment" | (string & {}) | null;
billingEmail: string | null;
growthTrialEligible: boolean | null;
growthTrialEligible: false;
enabledScopes: EnabledTeamScope[];
};
@@ -16,6 +16,8 @@ type Option = { label: string; value: string };
export function MultiNetworkSelector(props: {
selectedChainIds: number[];
onChange: (chainIds: number[]) => void;
disableChainId?: boolean;
className?: string;
}) {
const { allChains, idToChain } = useAllChainsData();
@@ -53,16 +55,24 @@ export function MultiNetworkSelector(props: {
return (
<div className="flex justify-between gap-4">
<span className="flex grow gap-2 truncate text-left">
<ChainIcon
className="size-5"
ipfsSrc={chain.icon?.url}
loading="lazy"
/>
{cleanChainName(chain.name)}
</span>
<Badge variant="outline" className="gap-2">
<span className="text-muted-foreground">Chain ID</span>
{chain.chainId}
</Badge>
{!props.disableChainId && (
<Badge variant="outline" className="gap-2">
<span className="text-muted-foreground">Chain ID</span>
{chain.chainId}
</Badge>
)}
</div>
);
},
[idToChain],
[idToChain, props.disableChainId],
);
return (
@@ -79,6 +89,7 @@ export function MultiNetworkSelector(props: {
disabled={allChains.length === 0}
overrideSearchFn={searchFn}
renderOption={renderOption}
className={props.className}
/>
);
}
@@ -90,6 +101,9 @@ export function SingleNetworkSelector(props: {
popoverContentClassName?: string;
// if specified - only these chains will be shown
chainIds?: number[];
side?: "left" | "right" | "top" | "bottom";
disableChainId?: boolean;
align?: "center" | "start" | "end";
}) {
const { allChains, idToChain } = useAllChainsData();
@@ -140,16 +154,19 @@ export function SingleNetworkSelector(props: {
ipfsSrc={chain.icon?.url}
loading="lazy"
/>
{chain.name}
{cleanChainName(chain.name)}
</span>
<Badge variant="outline" className="gap-2 max-sm:hidden">
<span className="text-muted-foreground">Chain ID</span>
{chain.chainId}
</Badge>
{!props.disableChainId && (
<Badge variant="outline" className="gap-2 max-sm:hidden">
<span className="text-muted-foreground">Chain ID</span>
{chain.chainId}
</Badge>
)}
</div>
);
},
[idToChain],
[idToChain, props.disableChainId],
);
const isLoadingChains = allChains.length === 0;
@@ -162,12 +179,15 @@ export function SingleNetworkSelector(props: {
onValueChange={(chainId) => {
props.onChange(Number(chainId));
}}
closeOnSelect={true}
placeholder={isLoadingChains ? "Loading Chains..." : "Select Chain"}
overrideSearchFn={searchFn}
renderOption={renderOption}
className={props.className}
popoverContentClassName={props.popoverContentClassName}
disabled={isLoadingChains}
side={props.side}
align={props.align}
/>
);
}
@@ -31,6 +31,9 @@ interface SelectWithSearchProps
) => boolean;
renderOption?: (option: { value: string; label: string }) => React.ReactNode;
popoverContentClassName?: string;
side?: "left" | "right" | "top" | "bottom";
align?: "center" | "start" | "end";
closeOnSelect?: boolean;
}
export const SelectWithSearch = React.forwardRef<
@@ -48,6 +51,7 @@ export const SelectWithSearch = React.forwardRef<
overrideSearchFn,
popoverContentClassName,
searchPlaceholder,
closeOnSelect,
...props
},
ref,
@@ -107,7 +111,7 @@ export const SelectWithSearch = React.forwardRef<
}, [searchValue]);
return (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal>
<PopoverTrigger asChild>
<Button
ref={ref}
@@ -118,23 +122,24 @@ export const SelectWithSearch = React.forwardRef<
className,
)}
>
<div className="flex w-full items-center justify-between">
<div className="flex w-full items-center justify-between gap-2">
<span
className={cn(
"text-muted-foreground text-sm",
"truncate text-muted-foreground text-sm",
selectedOption && "text-foreground",
)}
>
{selectedOption?.label || placeholder}
</span>
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
<ChevronDown className="size-4 cursor-pointer text-muted-foreground" />
</div>
</Button>
</PopoverTrigger>
<PopoverContent
className={cn("z-[10001] p-0", popoverContentClassName)}
align="center"
align={props.align || "center"}
side={props.side}
sideOffset={10}
onEscapeKeyDown={() => setIsPopoverOpen(false)}
style={{
@@ -174,7 +179,12 @@ export const SelectWithSearch = React.forwardRef<
key={option.value}
role="option"
aria-selected={isSelected}
onClick={() => onValueChange(option.value)}
onClick={() => {
onValueChange(option.value);
if (closeOnSelect) {
setIsPopoverOpen(false);
}
}}
variant="ghost"
className="flex w-full cursor-pointer justify-start gap-3 rounded-sm px-3 py-2 text-left"
ref={
@@ -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>
@@ -9,7 +9,7 @@ import { ToolTipLabel } from "./tooltip";
export function CopyTextButton(props: {
textToShow: string;
textToCopy: string;
tooltip: string;
tooltip: string | undefined;
className?: string;
iconClassName?: string;
variant?:
@@ -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) =>
@@ -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();
@@ -10,7 +10,7 @@ import {
import type { ResultItem } from "app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/metrics/components/StatusCodes";
import type { EngineBackendWalletType } from "lib/engine";
import { useState } from "react";
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import invariant from "tiny-invariant";
import type { EngineStatus } from "../../../app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table";
import { engineKeys } from "../cache-keys";
@@ -469,12 +469,12 @@ export function useEngineBackendWalletBalance(params: {
instanceUrl: string;
address: string;
authToken: string;
chainId: number;
}) {
const { instanceUrl, address, authToken } = params;
const chainId = useActiveWalletChain()?.id;
const { instanceUrl, address, authToken, chainId } = params;
return useQuery({
queryKey: engineKeys.backendWalletBalance(address, chainId || 1),
queryKey: engineKeys.backendWalletBalance(address, chainId),
queryFn: async () => {
invariant(chainId, "chainId is required");
@@ -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>
);
}
@@ -4,6 +4,7 @@ import {
toTokens,
toUnits,
} from "thirdweb";
import { getContract } from "thirdweb/contract";
import type { OverrideEntry } from "thirdweb/dist/types/utils/extensions/drops/types";
import type { Prettify } from "thirdweb/dist/types/utils/type-utils";
import { getContractMetadata } from "thirdweb/extensions/common";
@@ -71,7 +72,10 @@ export async function getClaimPhasesInLegacyFormat(
conditions.map(async (condition) => {
const [currencyMetadata, metadata, contractMetadata] = await Promise.all([
await ERC20Ext.getCurrencyMetadata({
contract: { ...options.contract, address: condition.currency },
contract: getContract({
...options.contract,
address: condition.currency,
}),
}).then((m) => ({
...m,
displayValue: toTokens(condition.pricePerToken, m.decimals),
@@ -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,612 +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}
/>
<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>
@@ -4,13 +4,13 @@ import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
import { Alert, AlertTitle } from "@/components/ui/alert";
import type { Account as TWAccount } from "@3rdweb-sdk/react/hooks/useApi";
import { CircleSlash } from "lucide-react";
import type { ContractOptions } from "thirdweb";
import type { ThirdwebContract } from "thirdweb/contract";
import type { Account } from "thirdweb/wallets";
import { ModuleCard } from "./module-card";
import { useAllModuleContractInfo } from "./moduleContractInfo";
export const InstalledModulesTable = (props: {
contract: ContractOptions;
contract: ThirdwebContract;
installedModules: {
data?: string[];
isPending: boolean;
@@ -15,8 +15,9 @@ import { FormProvider, type UseFormReturn, useForm } from "react-hook-form";
import { toast } from "sonner";
import {
type Chain,
type ContractOptions,
type ThirdwebClient,
type ThirdwebContract,
getContract,
sendTransaction,
waitForReceipt,
} from "thirdweb";
@@ -47,7 +48,7 @@ type FormData = {
};
type InstallModuleFormProps = {
contract: ContractOptions;
contract: ThirdwebContract;
refetchModules: () => void;
account: Account;
installedModules: {
@@ -180,11 +181,13 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
return Promise.all(
moduleAddress.map(async (address) => {
const result = await resolveImplementation({
client,
address,
chain: contract.chain,
});
const result = await resolveImplementation(
getContract({
client,
address,
chain: contract.chain,
}),
);
if (!result) {
throw new Error("Failed to fetch bytecode for module");
@@ -23,7 +23,7 @@ import { Suspense, useEffect, useState } from "react";
import { ErrorBoundary, type FallbackProps } from "react-error-boundary";
import { toast } from "sonner";
import {
type ContractOptions,
type ThirdwebContract,
getContract,
sendTransaction,
waitForReceipt,
@@ -35,7 +35,7 @@ import { useModuleContractInfo } from "./moduleContractInfo";
type ModuleCardProps = {
moduleAddress: string;
contract: ContractOptions;
contract: ThirdwebContract;
onRemoveModule: () => void;
ownerAccount: Account | undefined;
allModuleContractInfo: {
@@ -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"
@@ -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,29 +0,0 @@
"use client";
import { TabLinks } from "@/components/ui/tabs";
import { useSelectedLayoutSegment } from "next/navigation";
const links: {
name: string;
segment: string;
}[] = [
{ name: "Transaction Simulator", segment: "transaction-simulator" },
{ name: "Wei Converter", segment: "wei-converter" },
{ name: "Hex Converter", segment: "hex-converter" },
{ name: "Unix Time Converter", segment: "unixtime-converter" },
{ name: "Keccak-256 Converter", segment: "keccak256-converter" },
];
export function ToolsTabs() {
const layoutSegment = useSelectedLayoutSegment() || "";
return (
<TabLinks
links={links.map((tab) => ({
name: tab.name,
href: `/tools/${tab.segment}`,
isActive: layoutSegment === tab.segment,
}))}
/>
);
}
@@ -1,5 +1,5 @@
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
import type { Metadata } from "next";
import { ToolsTabs } from "./components/tabs";
export const metadata: Metadata = {
title: "thirdweb Blockchain Tools",
@@ -11,13 +11,31 @@ export default function ToolLayout({
children,
}: { children: React.ReactNode }) {
return (
<section className="flex h-full flex-col gap-8">
<main className="container flex-1 pb-20">
<div className="h-14" />
<ToolsTabs />
<div className="h-8" />
<div>{children}</div>
</main>
</section>
<SidebarLayout
sidebarLinks={[
{
label: "Transaction Simulator",
href: "/tools/transaction-simulator",
},
{
label: "Wei Converter",
href: "/tools/wei-converter",
},
{
label: "Hex Converter",
href: "/tools/hex-converter",
},
{
label: "Unix Time Converter",
href: "/tools/unixtime-converter",
},
{
label: "Keccak-256 Converter",
href: "/tools/keccak256-converter",
},
]}
>
{children}
</SidebarLayout>
);
}
@@ -1,118 +1,150 @@
"use client";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { formatDistanceToNow } from "date-fns";
import { useState } from "react";
import { ShareButton } from "../../components/share";
import {
formatDistanceToNow,
formatISO9075,
formatRFC3339,
formatRFC7231,
fromUnixTime,
} from "date-fns";
import { useMemo, useState } from "react";
export const UnixTimeConverter = () => {
const getCurrentUnixMilliseconds = () => Math.floor(new Date().getTime());
const [unix, setUnix] = useState<number>(getCurrentUnixMilliseconds());
export function UnixTimeConverter() {
const [unixTime, setUnixTime] = useState(() => {
return Math.floor(Date.now() / 1000);
});
// `unix` may be in milliseconds or seconds.
// Treat any value < 100000000000 as seconds, otherwise milliseconds.
const isSeconds = unix < 100000000000;
const date = new Date(isSeconds ? unix * 1000 : unix);
const dateLocal = date.toLocaleString(undefined, {
timeZoneName: "short",
});
const dateUTC = date.toLocaleString(undefined, {
timeZoneName: "short",
timeZone: "UTC",
});
const { date, error } = useMemo(() => {
if (Number.isNaN(unixTime) || !Number.isInteger(unixTime)) {
return {
error: "Invalid Unix Timestamp",
date: null,
};
}
const date = fromUnixTime(unixTime);
if (!isValidDate(date)) {
return {
date: null,
error: "Invalid Unix Timestamp",
};
}
return {
date,
error: null,
};
}, [unixTime]);
return (
<div className="space-y-24">
<div className="space-y-4">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Label htmlFor="unix-input" className="min-w-64 text-xl">
Unix Time
</Label>
<Input
id="unix-input"
type="number"
value={unix}
onChange={(e) => {
if (!Number.isNaN(e.target.valueAsNumber)) {
setUnix(e.target.valueAsNumber);
}
}}
placeholder="Enter a Unix time in milliseconds or seconds"
className="p-6 text-xl"
/>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Label className="min-w-64 text-xl">Time (local)</Label>
<div>
<CopyTextButton
textToCopy={dateLocal}
textToShow={dateLocal}
tooltip="Copy"
copyIconPosition="right"
className="font-mono text-xl"
/>
</div>
<p className="text-sm italic">
&larr; {formatDistanceToNow(date, { addSuffix: true })}
</p>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Label className="min-w-64 text-xl">Time (UTC)</Label>
<div>
<CopyTextButton
textToCopy={dateUTC}
textToShow={dateUTC}
tooltip="Copy"
copyIconPosition="right"
className="font-mono text-xl"
/>
</div>
</div>
<div className="mt-4 flex justify-end">
<ShareButton
cta="Share on X"
href="https://twitter.com/intent/tweet?text=Easy-to-use%20Unix%20time%20converter%20by%20thirdweb%20%F0%9F%98%8D&url=https%3A%2F%2Fthirdweb.com%2Ftools%2Funixtime-converter"
/>
</div>
</div>
<div className="mx-auto max-w-2xl">
<Card>
<CardHeader className="mb-6 border-b">
<CardTitle className="font-bold text-2xl">
Unix Time Converter
</CardTitle>
</CardHeader>
<div className="space-y-4">
<h1 className="font-bold text-2xl">
Convert Unix time in milliseconds or seconds
</h1>
<CardContent>
<div className="mb-4">
<Label htmlFor="unixTime">Enter Unix Timestamp</Label>
<Input
id="unixTime"
type="number"
value={unixTime}
onChange={(e) => {
setUnixTime(Number(e.target.value));
}}
placeholder="e.g., 1609459200"
className="mt-1"
/>
{error && (
<p className="mt-2 text-destructive-text text-sm">{error}</p>
)}
</div>
<p>
Timestamps are often represented in Unix time for several key reasons:
</p>
<h2 className="font-bold text-md">Simplicity</h2>
<p>
Counts seconds since January 1, 1970, making it easy to understand and
manipulate.
</p>
<h2 className="font-bold text-md">Consistency</h2>
<p>
Provides a uniform time representation across systems, unaffected by
time zones or calendar changes.
</p>
<h2 className="font-bold text-md">Standardization</h2>
<p>
Widely supported in computing, ensuring compatibility across
platforms.
</p>
<h2 className="font-bold text-md">Calculation</h2>
<p>
Allows straightforward arithmetic operations for time differences.
</p>
<h2 className="font-bold text-md">Efficiency</h2>
<p>
Stores time as a single integer, conserving memory and computational
resources.
</p>
<h2 className="font-bold text-md">Interoperability</h2>
<p>Facilitates seamless data exchange between systems and languages.</p>
</div>
<div className="flex flex-col gap-4">
<div>
<Label>Relative Time</Label>
<p className="font-mono text-muted-foreground">
{date
? formatDistanceToNow(date, {
addSuffix: true,
})
: "---"}
</p>
</div>
<div>
<Label>Local Time</Label>
<p className="font-mono text-muted-foreground">
{date ? formatLocal(date) : "---"}
</p>
</div>
<div>
<Label>UTC Time</Label>
<p className="font-mono text-muted-foreground">
{date ? formatUTC(date) : "---"}
</p>
</div>
<div>
<Label>ISO 8601</Label>
<p className="font-mono text-muted-foreground">
{date ? formatISO8601(date) : "---"}
</p>
</div>
<div>
<Label>ISO 9075</Label>
<p className="font-mono text-muted-foreground">
{date ? formatISO9075(date) : "---"}
</p>
</div>
<div>
<Label>RFC 7231</Label>
<p className="font-mono text-muted-foreground">
{date ? formatRFC7231(date) : "---"}
</p>
</div>
<div>
<Label>RFC 3339</Label>
<p className="font-mono text-muted-foreground">
{date ? formatRFC3339(date) : "---"}
</p>
</div>
<div>
<Label>SQL Timestamp</Label>
<p className="font-mono text-muted-foreground">
{date ? formatSQL(date) : "---"}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
};
}
function formatLocal(date: Date): string {
return date.toLocaleString();
}
function formatUTC(date: Date): string {
return date.toUTCString();
}
function isValidDate(date: Date): boolean {
return !Number.isNaN(date.getTime());
}
function formatISO8601(date: Date): string {
return date.toISOString();
}
function formatSQL(date: Date): string {
return date.toISOString().slice(0, 19).replace("T", " ");
}
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import { ClientOnly } from "../../../../components/ClientOnly/ClientOnly";
import { UnixTimeConverter } from "./components/UnixTimeConverter";
export const metadata: Metadata = {
@@ -7,5 +8,9 @@ export const metadata: Metadata = {
};
export default async function Page() {
return <UnixTimeConverter />;
return (
<ClientOnly ssr={null}>
<UnixTimeConverter />
</ClientOnly>
);
}
@@ -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>
);
}
@@ -70,6 +70,11 @@ export function AccountHeader(props: {
// refresh projects
router.refresh();
}}
teamSlug={
createProjectDialogState.isOpen
? createProjectDialogState.team.slug
: undefined
}
enableNebulaServiceByDefault={
createProjectDialogState.isOpen &&
createProjectDialogState.team.enabledScopes.includes("nebula")
@@ -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))"
+45 -26
View File
@@ -9,7 +9,7 @@ import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { useTheme } from "next-themes";
import Link from "next/link";
import { Suspense, lazy, useState } from "react";
import { Suspense, lazy, useEffect, useState } from "react";
import { ConnectEmbed, useActiveWalletConnectionStatus } from "thirdweb/react";
import { createWallet, inAppWallet } from "thirdweb/wallets";
import { ClientOnly } from "../../components/ClientOnly/ClientOnly";
@@ -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 />
@@ -154,27 +162,6 @@ function PageContent(props: {
router.replace(props.redirectPath);
}
if (connectionStatus === "connecting") {
return <LoadingCard />;
}
if (connectionStatus !== "connected" || screen.id === "login") {
return <CustomConnectEmbed onLogin={onLogin} />;
}
if (screen.id === "onboarding") {
return (
<Suspense fallback={<LoadingCard />}>
<LazyOnboardingUI
account={screen.account}
onComplete={onComplete}
redirectPath={props.redirectPath}
redirectToCheckout={redirectToCheckout}
/>
</Suspense>
);
}
async function onLogin() {
const account = await getRawAccountAction();
@@ -193,6 +180,38 @@ function PageContent(props: {
}
}
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
// if suddenly disconnected
if (connectionStatus !== "connected" && screen.id !== "login") {
setScreen({ id: "login" });
}
}, [connectionStatus, screen.id]);
if (connectionStatus === "connecting") {
return <LoadingCard />;
}
if (connectionStatus !== "connected" || screen.id === "login") {
return <CustomConnectEmbed onLogin={onLogin} />;
}
if (screen.id === "onboarding") {
return (
<Suspense fallback={<LoadingCard />}>
<LazyOnboardingUI
account={screen.account}
onComplete={onComplete}
redirectPath={props.redirectPath}
redirectToCheckout={redirectToCheckout}
onLogout={() => {
setScreen({ id: "login" });
}}
/>
</Suspense>
);
}
return <LoadingCard />;
}
@@ -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",
@@ -27,9 +27,10 @@ export function OnboardingChoosePlan(props: {
billingPlan="starter"
teamSlug={props.teamSlug}
cta={{
title: "Get started for free",
title: "Get Started for free",
tracking: {
category: "account",
label: "starterPlan",
},
}}
redirectPath={props.redirectPath}
@@ -40,15 +41,14 @@ export function OnboardingChoosePlan(props: {
billingPlan="growth"
teamSlug={props.teamSlug}
cta={{
title: "Claim your 1-month free",
hint: "Your free trial will end after 30 days.",
title: "Get Started with Growth",
tracking: {
category: "account",
label: "growthPlan",
},
variant: "default",
}}
canTrialGrowth={props.canTrialGrowth}
canTrialGrowth={false}
highlighted
redirectPath={props.redirectPath}
redirectToCheckout={props.redirectToCheckout}
@@ -14,12 +14,14 @@ type OnboardingGeneralProps = {
account: Account;
onSave: (email: string) => void;
onDuplicate: (email: string) => void;
onLogout: () => void;
};
export const OnboardingGeneral: React.FC<OnboardingGeneralProps> = ({
account,
onSave,
onDuplicate,
onLogout,
}) => {
const [existing, setExisting] = useState(false);
const activeWallet = useActiveWallet();
@@ -27,6 +29,7 @@ export const OnboardingGeneral: React.FC<OnboardingGeneralProps> = ({
async function handleLogout() {
await doLogout();
onLogout();
if (activeWallet) {
disconnect(activeWallet);
}
@@ -21,6 +21,7 @@ type OnboardingScreen =
function OnboardingUI(props: {
account: Account;
onComplete: () => void;
onLogout: () => void;
// path to redirect from stripe
redirectPath: string;
redirectToCheckout: RedirectBillingCheckoutAction;
@@ -68,6 +69,7 @@ function OnboardingUI(props: {
{screen.id === "onboarding" && (
<OnboardingGeneral
account={account}
onLogout={props.onLogout}
onSave={(email) => {
setUpdatedEmail(email);
setScreen({
@@ -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 = {
@@ -181,6 +181,7 @@ function SessionCard(props: {
<Link
className="before:absolute before:inset-0"
href={`/chat/${props.session.id}`}
prefetch={false}
>
{props.session.title || "Untitled"}
</Link>
@@ -12,15 +12,15 @@ 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";
import ContextFiltersButton from "./ContextFilters";
import ContextFiltersButton, { ContextFiltersForm } from "./ContextFilters";
import { EmptyStateChatPageContent } from "./EmptyStateChatPageContent";
export function ChatPageContent(props: {
@@ -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,
@@ -362,69 +354,91 @@ export function ChatPageContent(props: {
const showEmptyState = !userHasSubmittedMessage && messages.length === 0;
const handleUpdateContextFilters = async (
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
if (sessionId) {
await updateSession({
authToken: props.authToken,
sessionId,
contextFilters: values,
});
}
};
return (
<div className="flex grow flex-col overflow-hidden">
<WalletDisconnectedDialog
open={showConnectModal}
onOpenChange={setShowConnectModal}
/>
<header className="flex justify-start border-b bg-background p-4">
<header className="flex justify-between border-b bg-background p-4 xl:hidden">
<ContextFiltersButton
contextFilters={contextFilters}
setContextFilters={setContextFilters}
updateContextFilters={async (values) => {
// if session is not yet created, don't need to update sessions - starting a chat will create a session with the context filters
if (sessionId) {
await updateSession({
authToken: props.authToken,
config,
sessionId,
contextFilters: values,
});
}
}}
updateContextFilters={handleUpdateContextFilters}
/>
</header>
<div className="relative flex grow flex-col overflow-hidden rounded-lg pb-6">
{showEmptyState ? (
<div className="fade-in-0 container flex max-w-[800px] grow animate-in flex-col justify-center">
<EmptyStateChatPageContent sendMessage={handleSendMessage} />
</div>
) : (
<div className="fade-in-0 relative z-[0] flex max-h-full flex-1 animate-in flex-col overflow-hidden">
<Chats
messages={messages}
isChatStreaming={isChatStreaming}
authToken={props.authToken}
sessionId={sessionId}
className="min-w-0 pt-6 pb-32"
twAccount={props.account}
client={client}
enableAutoScroll={enableAutoScroll}
setEnableAutoScroll={setEnableAutoScroll}
/>
<div className="container max-w-[800px]">
<ChatBar
sendMessage={handleSendMessage}
isChatStreaming={isChatStreaming}
abortChatStream={() => {
chatAbortController?.abort();
setChatAbortController(undefined);
setIsChatStreaming(false);
// if last message is presence, remove it
if (messages[messages.length - 1]?.type === "presence") {
setMessages((prev) => prev.slice(0, -1));
}
}}
/>
<div className="flex grow overflow-hidden">
<div className="relative flex grow flex-col overflow-hidden rounded-lg pb-6">
{showEmptyState ? (
<div className="fade-in-0 container flex max-w-[800px] grow animate-in flex-col justify-center">
<EmptyStateChatPageContent sendMessage={handleSendMessage} />
</div>
</div>
)}
) : (
<div className="fade-in-0 relative z-[0] flex max-h-full flex-1 animate-in flex-col overflow-hidden">
<Chats
messages={messages}
isChatStreaming={isChatStreaming}
authToken={props.authToken}
sessionId={sessionId}
className="min-w-0 pt-6 pb-32"
twAccount={props.account}
client={client}
enableAutoScroll={enableAutoScroll}
setEnableAutoScroll={setEnableAutoScroll}
/>
<p className="mt-4 text-center text-muted-foreground text-xs opacity-75 lg:text-sm">
Nebula may make mistakes. Please use with discretion
</p>
<div className="container max-w-[800px]">
<ChatBar
sendMessage={handleSendMessage}
isChatStreaming={isChatStreaming}
abortChatStream={() => {
chatAbortController?.abort();
setChatAbortController(undefined);
setIsChatStreaming(false);
// if last message is presence, remove it
if (messages[messages.length - 1]?.type === "presence") {
setMessages((prev) => prev.slice(0, -1));
}
}}
/>
</div>
</div>
)}
<p className="mt-4 text-center text-muted-foreground text-xs opacity-75 lg:text-sm">
Nebula may make mistakes. Please use with discretion
</p>
</div>
<aside className="hidden w-[360px] flex-col border-l bg-card pt-4 xl:flex">
<div className="px-4">
<h3 className="font-semibold text-lg tracking-tight">Context</h3>
<p className="mb-5 text-muted-foreground text-sm">
Provide context to Nebula for your prompts
</p>
</div>
<ContextFiltersForm
contextFilters={contextFilters}
setContextFilters={setContextFilters}
modal={undefined}
updateContextFilters={handleUpdateContextFilters}
formBodyClassName="px-4"
formActionContainerClassName="px-4 border-t-0 pt-0 bg-transparent"
/>
</aside>
</div>
</div>
);
@@ -4,6 +4,8 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import {
FileCode2Icon,
MessageSquareShareIcon,
MessagesSquareIcon,
SquareDashedBottomCodeIcon,
TextIcon,
@@ -29,8 +31,11 @@ export function ChatSidebar(props: {
return (
<div className="flex h-full flex-col p-2">
<div className="flex items-center justify-start gap-3 p-2 lg:justify-between">
<Link href="/">
<NebulaIcon className="size-8 text-foreground" />
<Link href="/" className="flex items-center gap-2">
<NebulaIcon className="size-8 text-foreground" aria-label="Nebula" />
<span className="font-semibold text-lg tracking-tight">
Playground
</span>
</Link>
<Badge variant="secondary" className="gap-1 py-1">
@@ -48,6 +53,13 @@ export function ChatSidebar(props: {
<div className="h-3" />
<SidebarIconLink
href="https://portal.thirdweb.com/nebula/api-reference"
icon={FileCode2Icon}
label="API Reference"
target="_blank"
/>
<SidebarIconLink
href="/chat/history"
icon={MessagesSquareIcon}
@@ -79,7 +91,14 @@ export function ChatSidebar(props: {
</ScrollShadow>
)}
<div className="mb-3 border-b border-dashed pt-2 pb-3">
<div className="mb-3 border-y border-dashed py-3">
<SidebarIconLink
href="https://docs.google.com/forms/d/e/1FAIpQLSeM3fJRyywihRZUF1fiTNKEpJ_AzAcohRwXPpLr_3zxQ6W-tg/viewform?usp=sharing"
icon={MessageSquareShareIcon}
label="Take our quick survey!"
target="_blank"
/>
<SidebarIconLink
href="https://portal.thirdweb.com/changelog"
icon={TextIcon}
@@ -116,6 +135,7 @@ function SidebarIconLink(props: {
href={props.href}
target={props.target}
className="!justify-start !px-3 w-full gap-2.5 rounded-lg text-left"
prefetch={false}
>
<props.icon className="size-4" />
{props.label}
@@ -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));
}}
/>
@@ -8,6 +8,7 @@ import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
@@ -15,15 +16,14 @@ import {
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
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 { DialogDescription } from "@radix-ui/react-dialog";
import { useMutation } from "@tanstack/react-query";
import { SlidersHorizontalIcon } from "lucide-react";
import { useState } from "react";
@@ -31,75 +31,53 @@ 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 updateMutation = useMutation({
mutationFn: props.updateContextFilters,
});
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>
</Button>
</DialogTrigger>
<DialogContent className="p-0">
<ContextFilterDialogContent
contextFilters={props.contextFilters}
isPending={updateMutation.isPending}
updateFilters={async (data) => {
const promise = updateMutation.mutateAsync(data);
toast.promise(promise, {
success: "Context filters updated",
error: "Failed to update context filters",
});
<DialogContent className="overflow-hidden p-0">
<DialogHeader className="px-6 pt-6 pb-1">
<DialogTitle className="font-semibold text-xl">Context</DialogTitle>
<DialogDescription className="text-muted-foreground">
Provide context to Nebula for your prompts
</DialogDescription>
</DialogHeader>
promise.then(() => {
props.setContextFilters(data);
setIsOpen(false);
});
<ContextFiltersForm
contextFilters={props.contextFilters}
updateContextFilters={props.updateContextFilters}
setContextFilters={props.setContextFilters}
modal={{
close: () => setIsOpen(false),
}}
/>
</DialogContent>
@@ -107,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(
@@ -134,64 +116,71 @@ const formSchema = z.object({
message: "Chain IDs must be a comma-separated list of integers",
},
),
contractAddresses: commaSeparateListOfAddresses,
walletAddresses: commaSeparateListOfAddresses,
walletAddress: optionalAddressSchema,
});
function ContextFilterDialogContent(props: {
contextFilters: ContextFilters | undefined;
updateFilters: (filters: ContextFilters | undefined) => void;
isPending: boolean;
export function ContextFiltersForm(props: {
contextFilters: NebulaContext | undefined;
updateContextFilters: (filters: NebulaContext | undefined) => Promise<void>;
setContextFilters: (filters: NebulaContext | undefined) => void;
formBodyClassName?: string;
formActionContainerClassName?: string;
modal:
| {
close: () => void;
}
| undefined;
}) {
const updateMutation = useMutation({
mutationFn: props.updateContextFilters,
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
values: {
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());
props.updateFilters({
const data = {
chainIds: chainIdsArray,
contractAddresses: contractAddressesArray,
walletAddresses: walletAddressesArray,
walletAddress: walletAddress || null,
};
const promise = updateMutation.mutateAsync(data);
toast.promise(promise, {
success: "Context updated",
error: "Failed to update context",
});
promise.then(() => {
props.setContextFilters(data);
props.modal?.close?.();
});
}
return (
<Form {...form}>
<DialogHeader className="px-6 pt-6 pb-1">
<DialogTitle className="font-semibold text-xl">
Context Filters
</DialogTitle>
<DialogDescription className="text-muted-foreground">
Provide context information to Nebula for your prompts
</DialogDescription>
</DialogHeader>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="flex flex-col gap-5 px-6">
<form
onSubmit={form.handleSubmit(onSubmit)}
className={cn("flex w-full grow flex-col")}
>
<div
className={cn(
"flex flex-col gap-5 px-6 pb-6",
props.formBodyClassName,
)}
>
<FormField
control={form.control}
name="chainIds"
@@ -200,13 +189,14 @@ function ContextFilterDialogContent(props: {
<FormLabel>Chain IDs</FormLabel>
<FormControl>
<MultiNetworkSelector
disableChainId
className="bg-background"
selectedChainIds={form
.watch()
.chainIds.split(",")
.filter(Boolean)
.map(Number)}
onChange={(values) => {
console.log("values", values);
form.setValue("chainIds", values.join(","));
}}
/>
@@ -218,54 +208,44 @@ function ContextFilterDialogContent(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="min-h-[32px] resize-none"
placeholder="0x123.."
className="md:text-xs md:placeholder:text-xs"
/>
</FormControl>
<FormDescription>
Comma separated list of contract addresses
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="walletAddresses"
render={({ field }) => (
<FormItem>
<FormLabel>Wallet Addresses</FormLabel>
<FormControl>
<AutoResizeTextarea
{...field}
placeholder="0x123..., 0x456..."
className="min-h-[32px] resize-none"
/>
</FormControl>
<FormDescription>
Comma separated list of wallet addresses
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="mt-10 flex justify-end gap-3 border-t bg-card p-6">
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button className="min-w-28 gap-2" type="submit">
{props.isPending && <Spinner className="size-4" />}
Update
<div
className={cn(
"flex justify-end gap-3 border-t bg-card p-6",
props.formActionContainerClassName,
)}
>
{props.modal && (
<DialogClose asChild>
<Button variant="outline" size="sm">
Cancel
</Button>
</DialogClose>
)}
<Button
className="min-w-24 gap-2"
type="submit"
size="sm"
disabled={updateMutation.isPending}
>
{updateMutation.isPending && <Spinner className="size-4" />}
Update Context
</Button>
</div>
</form>
@@ -10,7 +10,7 @@ export function EmptyStateChatPageContent(props: {
sendMessage: (message: string) => void;
}) {
return (
<div className="py-16">
<div className="py-10 lg:py-16">
<div className="relative py-10">
<FancyBorders />
<div className="flex justify-center">
@@ -1,6 +1,5 @@
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import {} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { CodeClient } from "@/components/ui/code/code.client";
import type { Account as TWAccount } from "@3rdweb-sdk/react/hooks/useApi";
@@ -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();
@@ -106,7 +106,7 @@ export async function TotalSponsoredChartCardUI({
return (
<CombinedBarChartCard
isCurrency
title={title || "Total Sponsored"}
title={title || "Gas Sponsored"}
chartConfig={chartConfig}
data={timeSeriesData}
activeChart={
@@ -0,0 +1,130 @@
import { cn } from "@/lib/utils";
import { defineChain } from "thirdweb";
import { type ChainMetadata, getChainMetadata } from "thirdweb/chains";
import type { TransactionStats } from "types/analytics";
import { EmptyAccountAbstractionChartContent } from "../../../../../components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard";
import { BarChart } from "../../../components/Analytics/BarChart";
import { CombinedBarChartCard } from "../../../components/Analytics/CombinedBarChartCard";
export async function TransactionsChartCardUI({
data,
aggregatedData,
searchParams,
className,
onlyMainnet,
title,
description,
}: {
data: TransactionStats[];
aggregatedData: TransactionStats[];
searchParams?: { [key: string]: string | string[] | undefined };
className?: string;
onlyMainnet?: boolean;
title?: string;
description?: string;
}) {
const uniqueChainIds = [
...new Set(data.map((item) => item.chainId).filter(Boolean)),
];
const chains = await Promise.all(
uniqueChainIds.map((chainId) =>
// eslint-disable-next-line no-restricted-syntax
getChainMetadata(defineChain(Number(chainId))).catch(() => undefined),
),
).then((chains) => chains.filter((c) => c) as ChainMetadata[]);
// Process data to combine by date and chain type
const dateMap = new Map<string, { mainnet: number; testnet: number }>();
for (const item of data) {
const chain = chains.find((c) => c.chainId === Number(item.chainId));
const existing = dateMap.get(item.date) || { mainnet: 0, testnet: 0 };
if (chain?.testnet) {
existing.testnet += item.count;
} else {
existing.mainnet += item.count;
}
dateMap.set(item.date, existing);
}
// Convert to array and sort by date
const timeSeriesData = Array.from(dateMap.entries())
.map(([date, values]) => ({
date,
mainnet: values.mainnet,
testnet: values.testnet,
total: values.mainnet + values.testnet,
}))
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
const processedAggregatedData = {
mainnet: aggregatedData
.filter(
(d) => !chains.find((c) => c.chainId === Number(d.chainId))?.testnet,
)
.reduce((acc, curr) => acc + curr.count, 0),
testnet: aggregatedData
.filter(
(d) => chains.find((c) => c.chainId === Number(d.chainId))?.testnet,
)
.reduce((acc, curr) => acc + curr.count, 0),
total: aggregatedData.reduce((acc, curr) => acc + curr.count, 0),
};
const chartConfig = {
mainnet: {
label: "Mainnet Chains",
color: "hsl(var(--chart-1))",
},
testnet: {
label: "Testnet Chains",
color: "hsl(var(--chart-2))",
},
total: {
label: "All Chains",
color: "hsl(var(--chart-3))",
},
};
if (onlyMainnet) {
const filteredData = timeSeriesData.filter((d) => d.mainnet > 0);
return (
<div className={cn("rounded-lg border p-4 lg:p-6", className)}>
<h3 className="mb-1 font-semibold text-xl tracking-tight">
{title || "Transactions"}
</h3>
<p className="text-muted-foreground"> {description}</p>
<BarChart
chartConfig={chartConfig}
data={filteredData}
activeKey="mainnet"
emptyChartContent={<EmptyAccountAbstractionChartContent />}
/>
</div>
);
}
return (
<CombinedBarChartCard
title={title || "Transactions"}
chartConfig={chartConfig}
data={timeSeriesData}
activeChart={
(searchParams?.client_transactions as keyof typeof chartConfig) ??
"mainnet"
}
queryKey="client_transactions"
existingQueryParams={searchParams}
aggregateFn={(_data, key) => processedAggregatedData[key]}
className={className}
// Get the trend from the last two COMPLETE periods
trendFn={(data, key) =>
data.filter((d) => (d[key] as number) > 0).length >= 3
? ((data[data.length - 2]?.[key] as number) ?? 0) /
((data[data.length - 3]?.[key] as number) ?? 0) -
1
: undefined
}
/>
);
}
@@ -1,8 +1,13 @@
import { getProjects } from "@/api/projects";
import { getWalletConnections } from "@/api/analytics";
import { type Project, getProjects } from "@/api/projects";
import { getTeamBySlug } from "@/api/team";
import { Changelog } from "components/dashboard/Changelog";
import { subDays } from "date-fns";
import { redirect } from "next/navigation";
import { TeamProjectsPage } from "./~/projects/TeamProjectsPage";
import {
type ProjectWithAnalytics,
TeamProjectsPage,
} from "./~/projects/TeamProjectsPage";
export default async function Page(props: {
params: Promise<{ team_slug: string }>;
@@ -15,12 +20,13 @@ export default async function Page(props: {
}
const projects = await getProjects(params.team_slug);
const projectsWithTotalWallets = await getProjectsWithAnalytics(projects);
return (
<div className="container flex grow flex-col gap-12 py-8 lg:flex-row">
<div className="flex grow flex-col">
<h1 className="mb-4 font-semibold text-2xl tracking-tight">Projects</h1>
<TeamProjectsPage projects={projects} team={team} />
<TeamProjectsPage projects={projectsWithTotalWallets} team={team} />
</div>
<div className="shrink-0 lg:w-[320px]">
<h2 className="mb-4 font-semibold text-2xl tracking-tight">
@@ -31,3 +37,38 @@ export default async function Page(props: {
</div>
);
}
async function getProjectsWithAnalytics(
projects: Project[],
): Promise<Array<ProjectWithAnalytics>> {
return Promise.all(
projects.map(async (p) => {
try {
const today = new Date();
const thirtyDaysAgo = subDays(today, 30);
const data = await getWalletConnections({
clientId: p.publishableKey,
period: "all",
from: thirtyDaysAgo,
to: today,
});
let uniqueWalletsConnected = 0;
for (const d of data) {
uniqueWalletsConnected += d.uniqueWalletsConnected;
}
return {
...p,
monthlyActiveUsers: uniqueWalletsConnected,
};
} catch {
return {
...p,
monthlyActiveUsers: 0,
};
}
}),
);
}
@@ -1,4 +1,5 @@
import {
getClientTransactions,
getInAppWalletUsage,
getUserOpUsage,
getWalletConnections,
@@ -31,9 +32,10 @@ import { getValidAccount } from "app/account/settings/getAccount";
import { EmptyStateCard } from "app/team/components/Analytics/EmptyStateCard";
import { Suspense } from "react";
import { TotalSponsoredChartCardUI } from "../../_components/TotalSponsoredCard";
import { TransactionsChartCardUI } from "../../_components/TransactionsCard";
// revalidate every 5 minutes
export const revalidate = 300;
export const maxDuration = 300;
type SearchParams = {
usersChart?: string;
@@ -100,6 +102,8 @@ async function OverviewPageContent(props: {
inAppWalletUsage,
userOpUsageTimeSeries,
userOpUsage,
clientTransactionsTimeSeries,
clientTransactions,
] = await Promise.all([
// Aggregated wallet connections
getWalletConnections({
@@ -135,6 +139,19 @@ async function OverviewPageContent(props: {
to: range.to,
period: "all",
}),
// Client transactions
getClientTransactions({
accountId: account.id,
from: range.from,
to: range.to,
period: interval,
}),
getClientTransactions({
accountId: account.id,
from: range.from,
to: range.to,
period: "all",
}),
]);
const isEmpty =
@@ -180,6 +197,14 @@ async function OverviewPageContent(props: {
/>
)}
</div>
{clientTransactions.length > 0 && (
<TransactionsChartCardUI
searchParams={searchParams}
data={clientTransactionsTimeSeries}
aggregatedData={clientTransactions}
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
/>
)}
{userOpUsage.length > 0 ? (
<TotalSponsoredChartCardUI
searchParams={searchParams}
@@ -189,7 +214,7 @@ async function OverviewPageContent(props: {
/>
) : (
<EmptyStateCard
metric="Sponsored Transactions"
metric="Gas Sponsored"
link="https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started"
/>
)}
@@ -5,6 +5,9 @@ import { Badge } from "@/components/ui/badge";
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,
@@ -29,28 +32,31 @@ 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";
import { useTrack } from "hooks/analytics/useTrack";
import { useAllChainsData } from "hooks/chains/allChains";
import { EngineBackendWalletOptions } from "lib/engine";
import { useActiveChainAsDashboardChain } from "lib/v5-adapter";
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 invariant from "tiny-invariant";
import { FormHelperText, FormLabel, LinkButton, Text } from "tw-components";
import { FormHelperText, FormLabel, Text } from "tw-components";
import { prettyPrintCurrency } from "./utils";
interface BackendWalletsTableProps {
@@ -59,6 +65,7 @@ interface BackendWalletsTableProps {
isPending: boolean;
isFetched: boolean;
authToken: string;
chainId: number;
}
interface BackendWalletDashboard extends BackendWallet {
@@ -72,48 +79,49 @@ interface BackendWalletBalanceCellProps {
instanceUrl: string;
address: string;
authToken: string;
chainId: number;
}
const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
instanceUrl,
address,
authToken,
chainId,
}) => {
const { data: backendWalletBalance } = useEngineBackendWalletBalance({
instanceUrl: instanceUrl,
address,
authToken,
});
const chain = useActiveChainAsDashboardChain();
if (!chain || !backendWalletBalance) {
return;
const { data: backendWalletBalance, isFetching } =
useEngineBackendWalletBalance({
instanceUrl: instanceUrl,
address,
authToken,
chainId,
});
const chain = useV5DashboardChain(chainId);
if (!backendWalletBalance || isFetching) {
return <Skeleton className="h-6 w-36 rounded-lg" />;
}
const balanceDisplay = prettyPrintCurrency({
amount: backendWalletBalance.displayValue,
symbol: backendWalletBalance.symbol,
symbol: backendWalletBalance.symbol || chain.nativeCurrency?.symbol,
});
const balanceComponent = (
<Text fontWeight={backendWalletBalance.value === "0" ? "light" : "bold"}>
{balanceDisplay}
</Text>
);
const balanceComponent = <span>{balanceDisplay}</span>;
const explorer = chain?.explorers?.[0];
const explorer = chain.blockExplorers?.[0];
if (!explorer) {
return balanceComponent;
}
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>
);
};
@@ -123,11 +131,13 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
isPending,
isFetched,
authToken,
chainId,
}) => {
const editDisclosure = useDisclosure();
const receiveDisclosure = useDisclosure();
const sendDisclosure = useDisclosure();
const deleteDisclosure = useDisclosure();
const queryClient = useQueryClient();
const columns = useMemo(() => {
return [
@@ -155,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 (
@@ -163,13 +197,14 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
instanceUrl={instanceUrl}
address={address}
authToken={authToken}
chainId={chainId}
/>
);
},
id: "balance",
}),
];
}, [instanceUrl, authToken]);
}, [instanceUrl, authToken, chainId, queryClient]);
const [selectedBackendWallet, setSelectedBackendWallet] =
useState<BackendWallet>();
@@ -242,6 +277,7 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
disclosure={sendDisclosure}
instanceUrl={instanceUrl}
authToken={authToken}
chainId={chainId}
/>
)}
{selectedBackendWallet && deleteDisclosure.isOpen && (
@@ -417,24 +453,28 @@ const SendFundsModal = ({
disclosure,
instanceUrl,
authToken,
chainId,
}: {
fromWallet: BackendWallet;
backendWallets: BackendWallet[];
disclosure: UseDisclosureReturn;
instanceUrl: string;
authToken: string;
chainId: number;
}) => {
const chain = useActiveChainAsDashboardChain();
const form = useForm<SendFundsInput>();
const sendTokens = useEngineSendTokens({
instanceUrl,
authToken,
});
const { idToChain } = useAllChainsData();
const { data: backendWalletBalance } = useEngineBackendWalletBalance({
instanceUrl,
address: fromWallet.address,
authToken,
chainId: chainId,
});
const chain = idToChain.get(chainId);
const toWalletDisclosure = useDisclosure();
if (!backendWalletBalance) {
@@ -442,11 +482,9 @@ const SendFundsModal = ({
}
const onSubmit = async (data: SendFundsInput) => {
invariant(chain, "chain is required");
const promise = sendTokens.mutateAsync(
{
chainId: chain.chainId,
chainId: chainId,
fromAddress: fromWallet.address,
toAddress: data.toAddress,
amount: data.amount,
@@ -1,11 +1,13 @@
"use client";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
import {
type EngineInstance,
useEngineBackendWallets,
useEngineWalletConfig,
} from "@3rdweb-sdk/react/hooks/useEngine";
import { NetworkSelectorButton } from "components/selects/NetworkSelectorButton";
import Link from "next/link";
import { useState } from "react";
import { useActiveWalletChain } from "thirdweb/react";
import { BackendWalletsTable } from "./backend-wallets-table";
import { CreateBackendWalletButton } from "./create-backend-wallet-button";
import { ImportBackendWalletButton } from "./import-backend-wallet-button";
@@ -43,6 +45,10 @@ function BackendWalletsSection(props: {
authToken: string;
}) {
const { instance, teamSlug, authToken } = props;
const activeWalletChain = useActiveWalletChain();
const [_chainId, setChainId] = useState<number>();
const chainId = _chainId || activeWalletChain?.id || 1;
const backendWallets = useEngineBackendWallets({
instanceUrl: instance.url,
authToken,
@@ -105,9 +111,14 @@ function BackendWalletsSection(props: {
<div className="flex justify-end">
<div className="flex items-center gap-2">
<span className="text-sm">Show balance for</span>
{/* TODO - Replace with simple network selector - there's no need for user to switch chain */}
<div className="flex flex-row">
<NetworkSelectorButton />
<SingleNetworkSelector
chainId={chainId}
onChange={setChainId}
className="min-w-40 max-w-52 lg:max-w-60"
popoverContentClassName="!w-[80vw] md:!w-[500px]"
align="end"
/>
</div>
</div>
</div>
@@ -120,6 +131,7 @@ function BackendWalletsSection(props: {
isPending={backendWallets.isPending}
isFetched={backendWallets.isFetched}
authToken={authToken}
chainId={chainId}
/>
</section>
);
@@ -3,9 +3,9 @@ export const prettyPrintCurrency = ({
symbol,
}: {
amount?: string | number;
symbol: string;
symbol?: string;
}): string => {
const amountNumber =
typeof amount === "string" ? Number.parseFloat(amount) : (amount ?? 0);
return `${amountNumber.toFixed(6)} ${symbol}`;
return `${amountNumber.toFixed(6)} ${symbol || ""}`;
};
@@ -3,7 +3,8 @@
import type { Project } from "@/api/projects";
import type { Team } from "@/api/team";
import { ProjectAvatar } from "@/components/blocks/Avatars/ProjectAvatar";
import { CopyButton } from "@/components/ui/CopyButton";
import { PaginationButtons } from "@/components/pagination-buttons";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@@ -11,6 +12,11 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
@@ -19,51 +25,90 @@ import {
} from "@/components/ui/select";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
import { ChevronDownIcon, PlusIcon, SearchIcon } from "lucide-react";
import {
ChevronDownIcon,
EllipsisVerticalIcon,
PlusIcon,
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { useMemo, useState } from "react";
type SortById = "name" | "createdAt";
type SortById = "name" | "createdAt" | "monthlyActiveUsers";
export type ProjectWithAnalytics = Project & {
monthlyActiveUsers: number;
};
export function TeamProjectsPage(props: {
projects: Project[];
projects: ProjectWithAnalytics[];
team: Team;
}) {
const { projects } = props;
const [searchTerm, setSearchTerm] = useState("");
const [sortBy, setSortBy] = useState<SortById>("createdAt");
const [sortBy, setSortBy] = useState<SortById>("monthlyActiveUsers");
const [isCreateProjectDialogOpen, setIsCreateProjectDialogOpen] =
useState(false);
const router = useDashboardRouter();
let projectsToShow = !searchTerm
? projects
: projects.filter(
(project) =>
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
project.publishableKey
.toLowerCase()
.includes(searchTerm.toLowerCase()),
);
const sortedProjects = useMemo(() => {
let _projectsToShow = !searchTerm
? projects
: projects.filter(
(project) =>
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
project.publishableKey
.toLowerCase()
.includes(searchTerm.toLowerCase()),
);
if (sortBy === "name") {
projectsToShow = projectsToShow.sort((a, b) =>
a.name.localeCompare(b.name),
);
} else if (sortBy === "createdAt") {
projectsToShow = projectsToShow.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
}
if (sortBy === "name") {
_projectsToShow = _projectsToShow.sort((a, b) =>
a.name.localeCompare(b.name),
);
} else if (sortBy === "createdAt") {
_projectsToShow = _projectsToShow.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
} else if (sortBy === "monthlyActiveUsers") {
_projectsToShow = _projectsToShow.sort(
(a, b) => b.monthlyActiveUsers - a.monthlyActiveUsers,
);
}
return _projectsToShow;
}, [searchTerm, sortBy, projects]);
const pageSize = 8;
const [page, setPage] = useState(1);
const paginatedProjects = sortedProjects.slice(
(page - 1) * pageSize,
page * pageSize,
);
const showPagination = sortedProjects.length > pageSize;
const totalPages = Math.ceil(sortedProjects.length / pageSize);
return (
<div className="flex grow flex-col">
{/* Filters + Add New */}
<div className="flex flex-col gap-4 md:flex-row md:items-center">
<SearchInput value={searchTerm} onValueChange={setSearchTerm} />
<SearchInput
value={searchTerm}
onValueChange={(v) => {
setSearchTerm(v);
setPage(1);
}}
/>
<div className="flex gap-4">
<SelectBy value={sortBy} onChange={setSortBy} />
<SelectBy
value={sortBy}
onChange={(v) => {
setSortBy(v);
setPage(1);
}}
/>
<AddNewButton
createProject={() => setIsCreateProjectDialogOpen(true)}
teamMembersSettingsPath={`/team/${props.team.slug}/~/settings/members`}
@@ -74,23 +119,33 @@ export function TeamProjectsPage(props: {
<div className="h-6" />
{/* Projects */}
{projectsToShow.length === 0 ? (
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
<div className="flex flex-col items-center">
<p className="mb-5 text-center">No projects created</p>
<Button
className="gap-2"
onClick={() => setIsCreateProjectDialogOpen(true)}
variant="outline"
>
<PlusIcon className="size-4" />
Create a Project
</Button>
</div>
</div>
{paginatedProjects.length === 0 ? (
<>
{searchTerm !== "" ? (
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
<div className="flex flex-col items-center">
<p className="mb-5 text-center">No projects found</p>
</div>
</div>
) : (
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
<div className="flex flex-col items-center">
<p className="mb-5 text-center">No projects created</p>
<Button
className="gap-2"
onClick={() => setIsCreateProjectDialogOpen(true)}
variant="outline"
>
<PlusIcon className="size-4" />
Create a Project
</Button>
</div>
</div>
)}
</>
) : (
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
{projectsToShow.map((project) => {
{paginatedProjects.map((project) => {
return (
<ProjectCard
key={project.id}
@@ -102,9 +157,20 @@ export function TeamProjectsPage(props: {
</div>
)}
{showPagination && (
<div className="py-6">
<PaginationButtons
activePage={page}
onPageClick={setPage}
totalPages={totalPages}
/>
</div>
)}
<LazyCreateAPIKeyDialog
open={isCreateProjectDialogOpen}
onOpenChange={setIsCreateProjectDialogOpen}
teamSlug={props.team.slug}
onCreateAndComplete={() => {
// refresh projects
router.refresh();
@@ -118,7 +184,7 @@ export function TeamProjectsPage(props: {
}
function ProjectCard(props: {
project: Project;
project: ProjectWithAnalytics;
team_slug: string;
}) {
const { project, team_slug } = props;
@@ -130,34 +196,51 @@ function ProjectCard(props: {
{/* TODO - set image */}
<ProjectAvatar className="size-10 rounded-full" src="" />
<div className="flex-grow flex-col gap-1">
<div className="flex items-center justify-between gap-2">
<Link
className="group static before:absolute before:top-0 before:right-0 before:bottom-0 before:left-0 before:z-0"
// remove /connect when we have overview page
href={`/team/${team_slug}/${project.slug}`}
>
<h2 className="font-medium text-base">{project.name}</h2>
</Link>
<CopyButton
text={project.publishableKey}
iconClassName="z-10 size-3"
className="!h-auto !w-auto -translate-x-1 p-2 hover:bg-secondary"
/>
</div>
<div className="flex flex-grow flex-col gap-1">
<Link
className="group static before:absolute before:top-0 before:right-0 before:bottom-0 before:left-0 before:z-0"
// remove /connect when we have overview page
href={`/team/${team_slug}/${project.slug}`}
>
<h2 className="font-medium text-base">{project.name}</h2>
</Link>
<p className="flex items-center text-muted-foreground text-sm">
{truncate(project.publishableKey, 32)}
<p className="flex items-center gap-1 text-muted-foreground text-sm">
<span>{project.monthlyActiveUsers}</span>
Monthly Active Users
</p>
</div>
<Popover>
<PopoverTrigger asChild>
<Button className="z-10 h-auto w-auto p-2" variant="ghost">
<EllipsisVerticalIcon className="size-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[180px] p-1">
<CopyTextButton
textToCopy={project.publishableKey}
textToShow="Copy Client ID"
copyIconPosition="right"
tooltip={undefined}
variant="ghost"
className="flex h-10 w-full justify-between gap-3 rounded-md px-4 py-2"
/>
<Button
variant="ghost"
className="w-full justify-start gap-3"
asChild
>
<Link href={`/team/${team_slug}/${project.slug}/settings`}>
Settings
</Link>
</Button>
</PopoverContent>
</Popover>
</div>
);
}
function truncate(str: string, stringLimit: number) {
return str.length > stringLimit ? `${str.slice(0, stringLimit)}...` : str;
}
function SearchInput(props: {
value: string;
onValueChange: (value: string) => void;
@@ -209,10 +292,11 @@ function SelectBy(props: {
value: SortById;
onChange: (value: SortById) => void;
}) {
const values: SortById[] = ["name", "createdAt"];
const values: SortById[] = ["name", "createdAt", "monthlyActiveUsers"];
const valueToLabel: Record<SortById, string> = {
name: "Name",
createdAt: "Creation Date",
monthlyActiveUsers: "Monthly Active Users",
};
return (
@@ -223,7 +307,12 @@ function SelectBy(props: {
}}
>
<SelectTrigger className="min-w-[200px] bg-card capitalize">
Sort by {valueToLabel[props.value]}
<div className="flex items-center gap-1.5">
<span className="!hidden lg:!inline text-muted-foreground">
Sort by
</span>
{valueToLabel[props.value]}
</div>
</SelectTrigger>
<SelectContent>
{values.map((value) => (
@@ -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)} />
);
}
@@ -0,0 +1,119 @@
import { defineChain, getContract } from "thirdweb";
import { getChainMetadata } from "thirdweb/chains";
import { TransactionsChartCardUI } from "../../../(team)/_components/TransactionsCard";
import { getThirdwebClient } from "../../../../../../@/constants/thirdweb.server";
import { fetchDashboardContractMetadata } from "../../../../../../@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
import type { TransactionStats } from "../../../../../../types/analytics";
import { PieChartCard } from "../../../../components/Analytics/PieChartCard";
export function TransactionsChartsUI({
data,
aggregatedData,
searchParams,
}: {
data: TransactionStats[];
aggregatedData: TransactionStats[];
searchParams: { [key: string]: string | string[] | undefined };
}) {
return (
<>
<TransactionsChartCardUI
searchParams={searchParams}
data={data}
aggregatedData={aggregatedData}
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
/>
<div className="grid gap-6 max-md:px-6 md:grid-cols-2">
<ChainDistributionCard data={aggregatedData} />
<ContractDistributionCard data={aggregatedData} />
</div>
</>
);
}
async function ChainDistributionCard({ data }: { data: TransactionStats[] }) {
const reducedData = await Promise.all(
Object.entries(
data.reduce(
(acc, curr) => {
acc[curr.chainId] = (acc[curr.chainId] || 0) + curr.count;
return acc;
},
{} as Record<string, number>,
),
)
.sort((a, b) => b[1] - a[1])
.slice(0, 10) // only top ten
.map(async ([key, value]) => {
// eslint-disable-next-line no-restricted-syntax
const chain = defineChain(Number(key));
const chainMeta = await getChainMetadata(chain).catch(() => undefined);
return {
label: chainMeta?.slug || chain.id.toString(),
value,
link: `/${chain.id}`,
};
}),
);
const aggregateFn = () => new Set(data.map((d) => `${d.chainId}`)).size;
return (
<PieChartCard title="Chains" data={reducedData} aggregateFn={aggregateFn} />
);
}
async function ContractDistributionCard({
data,
}: { data: TransactionStats[] }) {
const reducedData = await Promise.all(
Object.entries(
data
.filter((d) => d.contractAddress)
.reduce(
(acc, curr) => {
acc[`${curr.chainId}:${curr.contractAddress}`] =
(acc[`${curr.chainId}:${curr.contractAddress}`] || 0) +
curr.count;
return acc;
},
{} as Record<string, number>,
),
)
.sort((a, b) => b[1] - a[1])
.slice(0, 10) // only top ten
.map(async ([key, value]) => {
const [chainId, contractAddress] = key.split(":");
// eslint-disable-next-line no-restricted-syntax
const chain = defineChain(Number(chainId));
const chainMeta = await getChainMetadata(chain).catch(() => undefined);
const contractData = await fetchDashboardContractMetadata(
getContract({
chain,
address: contractAddress as string, // we filter above
client: getThirdwebClient(),
}),
).catch(() => undefined);
return {
label: `${contractData?.name} (${chainMeta?.slug || chainId})`,
link: `/${chainId}/${contractAddress}`,
value,
};
}),
);
const aggregateFn = () =>
new Set(
data
.filter((d) => d.contractAddress)
.map((d) => `${d.chainId}:${d.contractAddress}`),
).size;
return (
<PieChartCard
title="Contracts"
data={reducedData}
aggregateFn={aggregateFn}
/>
);
}
@@ -0,0 +1,50 @@
import { LoadingChartState } from "components/analytics/empty-chart-state";
import { Suspense } from "react";
import type { AnalyticsQueryParams } from "types/analytics";
import { getClientTransactions } from "../../../../../../@/api/analytics";
import { TransactionsChartsUI } from "./TransactionCharts";
export function TransactionsCharts(
props: AnalyticsQueryParams & {
searchParams: { [key: string]: string | string[] | undefined };
},
) {
return (
// TODO: Add better LoadingChartState
<Suspense
fallback={
<div className="h-[400px]">
<LoadingChartState />
</div>
}
>
<TransactionsChartCardAsync {...props} />
</Suspense>
);
}
async function TransactionsChartCardAsync(
props: AnalyticsQueryParams & {
searchParams: { [key: string]: string | string[] | undefined };
},
) {
const [data, aggregatedData] = await Promise.all([
getClientTransactions(props),
getClientTransactions({
...props,
period: "all",
}),
]);
if (!aggregatedData.length) {
return null;
}
return (
<TransactionsChartsUI
data={data}
aggregatedData={aggregatedData}
searchParams={props.searchParams}
/>
);
}
@@ -1,5 +1,8 @@
import type { Range } from "components/analytics/date-range-selector";
import { InAppWalletAnalytics } from "components/embedded-wallets/Analytics";
import { notFound } from "next/navigation";
import { getProject } from "../../../../../../@/api/projects";
import { getAPIKeyForProjectId } from "../../../../../api/lib/getAPIKeys";
export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;
@@ -27,9 +30,19 @@ export default async function Page(props: {
? (searchParams.interval as "day" | "week")
: "week";
const project = await getProject(params.team_slug, params.project_slug);
if (!project) {
notFound();
}
const apiKey = await getAPIKeyForProjectId(project.id);
if (!apiKey) {
notFound();
}
return (
<InAppWalletAnalytics
clientId={params.project_slug}
clientId={apiKey.key}
interval={interval}
range={range as Range}
/>
@@ -34,6 +34,7 @@ import { CombinedBarChartCard } from "../../components/Analytics/CombinedBarChar
import { EmptyState } from "../../components/Analytics/EmptyState";
import { PieChartCard } from "../../components/Analytics/PieChartCard";
import { RpcMethodBarChartCard } from "./components/RpcMethodBarChartCard";
import { TransactionsCharts } from "./components/Transactions";
interface PageParams {
team_slug: string;
@@ -49,6 +50,8 @@ interface PageProps {
searchParams: Promise<PageSearchParams>;
}
export const maxDuration = 300;
export default async function ProjectOverviewPage(props: PageProps) {
const [params, searchParams] = await Promise.all([
props.params,
@@ -174,12 +177,6 @@ async function ProjectAnalytics(props: {
link="https://portal.thirdweb.com/connect/quickstart"
/>
)}
<RpcMethodBarChartCard
from={range.from}
to={range.to}
period={interval}
clientId={project.publishableKey}
/>
<div className="grid gap-6 max-md:px-6 md:grid-cols-2">
{walletConnections.status === "fulfilled" &&
walletConnections.value.length > 0 ? (
@@ -200,6 +197,13 @@ async function ProjectAnalytics(props: {
/>
)}
</div>
<TransactionsCharts
searchParams={searchParams}
from={range.from}
to={range.to}
period={interval}
clientId={project.publishableKey}
/>
{userOpUsageTimeSeries.status === "fulfilled" &&
userOpUsage.status === "fulfilled" &&
userOpUsage.value.length > 0 ? (
@@ -216,6 +220,12 @@ async function ProjectAnalytics(props: {
link="https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started"
/>
)}
<RpcMethodBarChartCard
from={range.from}
to={range.to}
period={interval}
clientId={project.publishableKey}
/>
</div>
);
}
@@ -417,7 +427,7 @@ async function TotalSponsoredCard({
return (
<CombinedBarChartCard
isCurrency
title="Total Sponsored"
title="Gas Sponsored"
chartConfig={chartConfig}
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
data={timeSeriesData}
@@ -60,6 +60,16 @@ function Story() {
payConfig: "/payConfig",
}}
onKeyUpdated={undefined}
rotateSecretKey={async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
data: {
secret: new Array(86).fill("x").join(""),
secretHash: new Array(64).fill("x").join(""),
secretMasked: "123...4567",
},
};
}}
showNebulaSettings={false}
/>
@@ -1,12 +1,21 @@
"use client";
import { apiServerProxy } from "@/actions/proxies";
import { DangerSettingCard } from "@/components/blocks/DangerSettingCard";
import { SettingsCard } from "@/components/blocks/SettingsCard";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { DynamicHeight } from "@/components/ui/DynamicHeight";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -14,13 +23,14 @@ import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
import type { ApiKey, UpdateKeyInput } from "@3rdweb-sdk/react/hooks/useApi";
import {
useRevokeApiKey,
useUpdateApiKey,
} from "@3rdweb-sdk/react/hooks/useApi";
import { zodResolver } from "@hookform/resolvers/zod";
import type { UseMutationResult } from "@tanstack/react-query";
import { type UseMutationResult, useMutation } from "@tanstack/react-query";
import { SERVICES } from "@thirdweb-dev/service-utils";
import {
type ServiceName,
@@ -28,8 +38,13 @@ import {
} from "@thirdweb-dev/service-utils";
import { format } from "date-fns";
import { useTrack } from "hooks/analytics/useTrack";
import { ExternalLinkIcon } from "lucide-react";
import {
CircleAlertIcon,
ExternalLinkIcon,
RefreshCcwIcon,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { type UseFormReturn, useForm } from "react-hook-form";
import { type FieldArrayWithId, useFieldArray } from "react-hook-form";
import { toast } from "sonner";
@@ -47,11 +62,20 @@ type EditProjectUIPaths = {
afterDeleteRedirectTo: string;
};
type RotateSecretKeyAPIReturnType = {
data: {
secret: string;
secretMasked: string;
secretHash: string;
};
};
export function ProjectGeneralSettingsPage(props: {
apiKey: ApiKey;
paths: EditProjectUIPaths;
onKeyUpdated: (() => void) | undefined;
showNebulaSettings: boolean;
projectId: string;
}) {
const updateMutation = useUpdateApiKey();
const deleteMutation = useRevokeApiKey();
@@ -64,6 +88,24 @@ export function ProjectGeneralSettingsPage(props: {
paths={props.paths}
onKeyUpdated={props.onKeyUpdated}
showNebulaSettings={props.showNebulaSettings}
rotateSecretKey={async () => {
const res = await apiServerProxy<RotateSecretKeyAPIReturnType>({
pathname: "/v2/keys/rotate-secret-key",
method: "POST",
body: JSON.stringify({
projectId: props.projectId,
}),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
throw new Error(res.error);
}
return res.data;
}}
/>
);
}
@@ -84,6 +126,7 @@ interface EditApiKeyProps {
paths: EditProjectUIPaths;
onKeyUpdated: (() => void) | undefined;
showNebulaSettings: boolean;
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
}
type UpdateAPIForm = UseFormReturn<ProjectSettingsPageFormSchema>;
@@ -216,7 +259,10 @@ export const ProjectGeneralSettingsPageUI: React.FC<EditApiKeyProps> = (
handleSubmit={handleSubmit}
/>
<APIKeyDetails apiKey={apiKey} />
<APIKeyDetails
apiKey={apiKey}
rotateSecretKey={props.rotateSecretKey}
/>
<AllowedDomainsSetting
form={form}
@@ -609,10 +655,13 @@ function EnabledServicesSetting(props: {
function APIKeyDetails({
apiKey,
rotateSecretKey,
}: {
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
apiKey: ApiKey;
}) {
const { createdAt, updatedAt, lastAccessedAt } = apiKey;
const [secretKeyMasked, setSecretKeyMasked] = useState(apiKey.secretMasked);
return (
<div className="flex flex-col gap-6 rounded-lg border border-border bg-card px-4 py-6 lg:px-6">
@@ -632,7 +681,7 @@ function APIKeyDetails({
</div>
{/* NOTE: for very old api keys the secret might be `null`, if that's the case we skip it */}
{apiKey.secretMasked && (
{secretKeyMasked && (
<div>
<h3>Secret Key</h3>
<p className="mb-2 text-muted-foreground text-sm">
@@ -641,8 +690,17 @@ function APIKeyDetails({
the time of creation for the full secret key.
</p>
<div className="max-w-[350px] rounded-lg border border-border bg-background px-4 py-3 font-mono text-sm">
{apiKey.secretMasked}
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
<div className="rounded-lg border border-border bg-background px-4 py-3 font-mono text-sm lg:w-[350px]">
{secretKeyMasked}
</div>
<RotateSecretKeyButton
rotateSecretKey={rotateSecretKey}
onSuccess={(data) => {
setSecretKeyMasked(data.data.secretMasked);
}}
/>
</div>
</div>
)}
@@ -726,3 +784,214 @@ function DeleteProject(props: {
/>
);
}
function RotateSecretKeyButton(props: {
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
}) {
const [isOpen, setIsOpen] = useState(false);
const [isModalCloseAllowed, setIsModalCloseAllowed] = useState(true);
return (
<Dialog
open={isOpen}
onOpenChange={(v) => {
if (!isModalCloseAllowed) {
return;
}
setIsOpen(v);
}}
>
<DialogTrigger asChild>
<Button
variant="outline"
className="h-auto gap-2 rounded-lg bg-background px-4 py-3"
onClick={() => setIsOpen(true)}
>
<RefreshCcwIcon className="size-4" />
Rotate Secret Key
</Button>
</DialogTrigger>
<DialogContent
className="overflow-hidden p-0"
dialogCloseClassName={cn(!isModalCloseAllowed && "hidden")}
>
<RotateSecretKeyModalContent
rotateSecretKey={props.rotateSecretKey}
closeModal={() => {
setIsOpen(false);
setIsModalCloseAllowed(true);
}}
disableModalClose={() => setIsModalCloseAllowed(false)}
onSuccess={props.onSuccess}
/>
</DialogContent>
</Dialog>
);
}
type RotateSecretKeyScreen =
| { id: "initial" }
| { id: "save-newkey"; secretKey: string };
function RotateSecretKeyModalContent(props: {
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
closeModal: () => void;
disableModalClose: () => void;
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
}) {
const [screen, setScreen] = useState<RotateSecretKeyScreen>({
id: "initial",
});
if (screen.id === "save-newkey") {
return (
<SaveNewKeyScreen
secretKey={screen.secretKey}
closeModal={props.closeModal}
/>
);
}
if (screen.id === "initial") {
return (
<RotateSecretKeyInitialScreen
rotateSecretKey={props.rotateSecretKey}
onSuccess={(data) => {
props.disableModalClose();
props.onSuccess(data);
setScreen({ id: "save-newkey", secretKey: data.data.secret });
}}
closeModal={props.closeModal}
/>
);
}
return null;
}
function RotateSecretKeyInitialScreen(props: {
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
closeModal: () => void;
}) {
const [isConfirmed, setIsConfirmed] = useState(false);
const rotateKeyMutation = useMutation({
mutationFn: props.rotateSecretKey,
onSuccess: (data) => {
props.onSuccess(data);
},
onError: (err) => {
console.error(err);
toast.error("Failed to rotate secret key");
},
});
return (
<div>
<div className="flex flex-col p-6">
<DialogHeader>
<DialogTitle>Rotate Secret Key</DialogTitle>
</DialogHeader>
<div className="h-6" />
<Alert variant="destructive">
<CircleAlertIcon className="size-5" />
<AlertTitle>Current secret key will stop working</AlertTitle>
<AlertDescription>
Rotating the secret key will invalidate the current secret key and
generate a new one. This action is irreversible.
</AlertDescription>
</Alert>
<div className="h-4" />
<CheckboxWithLabel className="text-foreground">
<Checkbox
checked={isConfirmed}
onCheckedChange={(v) => setIsConfirmed(!!v)}
/>
I understand the consequences of rotating the secret key
</CheckboxWithLabel>
</div>
<div className="flex justify-end gap-3 border-t bg-card p-6">
<Button variant="outline" onClick={props.closeModal}>
Close
</Button>
<Button
variant="destructive"
className="gap-2"
disabled={!isConfirmed || rotateKeyMutation.isPending}
onClick={() => {
rotateKeyMutation.mutate();
}}
>
{rotateKeyMutation.isPending ? (
<Spinner className="size-4" />
) : (
<RefreshCcwIcon className="size-4" />
)}
Rotate Secret Key
</Button>
</div>
</div>
);
}
function SaveNewKeyScreen(props: {
secretKey: string;
closeModal: () => void;
}) {
const [isSecretStored, setIsSecretStored] = useState(false);
return (
<div className="flex min-w-0 flex-col">
<div className="flex flex-col p-6">
<DialogHeader>
<DialogTitle>Save New Secret Key</DialogTitle>
</DialogHeader>
<div className="h-6" />
<CopyTextButton
textToCopy={props.secretKey}
className="!h-auto w-full justify-between bg-card px-3 py-3 font-mono"
textToShow={props.secretKey}
copyIconPosition="right"
tooltip="Copy Secret Key"
/>
<div className="h-4" />
<Alert variant="destructive">
<AlertTitle>Do not share or expose your secret key</AlertTitle>
<AlertDescription>
<div className="mb-5">
Secret keys cannot be recovered. If you lose your secret key, you
will need to rotate the secret key or create a new Project.
</div>
<CheckboxWithLabel className="text-foreground">
<Checkbox
checked={isSecretStored}
onCheckedChange={(v) => {
setIsSecretStored(!!v);
}}
/>
I confirm that I've securely stored my secret key
</CheckboxWithLabel>
</AlertDescription>
</Alert>
</div>
<div className="flex justify-end gap-3 border-t bg-card p-6">
<Button
variant="outline"
className="gap-2"
disabled={!isSecretStored}
onClick={props.closeModal}
>
Close
</Button>
</div>
</div>
);
}
@@ -9,9 +9,10 @@ export function ProjectGeneralSettingsPageForTeams(props: {
team: Team;
project_slug: string;
apiKey: ApiKey;
projectId: string;
}) {
const router = useDashboardRouter();
const { team, project_slug, apiKey } = props;
const { team, project_slug, apiKey, projectId } = props;
const projectLayout = `/team/${team.slug}/${project_slug}`;
// TODO - add a Project Image form field on this page
@@ -19,6 +20,7 @@ export function ProjectGeneralSettingsPageForTeams(props: {
return (
<ProjectGeneralSettingsPage
apiKey={apiKey}
projectId={projectId}
paths={{
aaConfig: `${projectLayout}/connect/account-abstraction/settings`,
inAppConfig: `${projectLayout}/connect/in-app-wallets/settings`,
@@ -31,6 +31,7 @@ export default async function Page(props: {
<ProjectGeneralSettingsPageForTeams
apiKey={apiKey}
project_slug={project_slug}
projectId={project.id}
team={team}
/>
);
@@ -2,6 +2,7 @@ import { getTeamBySlug } from "@/api/team";
import { AppFooter } from "@/components/blocks/app-footer";
import { redirect } from "next/navigation";
import { TWAutoConnect } from "../../components/autoconnect";
import { SaveLastVisitedTeamPage } from "../components/last-visited-page/SaveLastVisitedPage";
export default async function RootTeamLayout(props: {
children: React.ReactNode;
@@ -19,6 +20,7 @@ export default async function RootTeamLayout(props: {
<div className="flex grow flex-col">{props.children}</div>
<TWAutoConnect />
<AppFooter />
<SaveLastVisitedTeamPage />
</div>
);
}
@@ -4,6 +4,7 @@ import {
CardFooter,
CardHeader,
} from "@/components/ui/card";
import Link from "next/link";
import { PieChart } from "./PieChart";
import { Stat } from "./Stat";
@@ -11,6 +12,7 @@ type ChartData = {
value: number;
label: string;
fill?: string;
link?: string;
};
export function PieChartCard({
title,
@@ -22,11 +24,11 @@ export function PieChartCard({
aggregateFn?: (data: ChartData[]) => number;
}) {
const processedData = (() => {
if (data.length <= 9) return data;
// Sort by value descending
const sorted = [...data].sort((a, b) => b.value - a.value);
if (sorted.length <= 9) return sorted;
// Take top 9
const top10 = sorted.slice(0, 9).map((item) => ({
...item,
@@ -62,13 +64,27 @@ export function PieChartCard({
<CardFooter className="no-scrollbar flex max-w-full justify-center p-6 pt-0 max-md:overflow-x-auto">
<div className="flex flex-wrap justify-center gap-x-4 gap-y-2.5">
{processedData.map(
({ label, fill }: { label: string; fill?: string }) => (
({
label,
fill,
link,
}: { label: string; fill?: string; link?: string }) => (
<div key={fill} className="flex items-center gap-2">
<div
className="size-2 rounded-full"
style={{ background: fill }}
/>
<span className="text-muted-foreground text-xs">{label}</span>
{link ? (
<Link
href={link}
target="_blank"
className="text-muted-foreground text-xs hover:text-link-foreground"
>
{label}
</Link>
) : (
<span className="text-muted-foreground text-xs">{label}</span>
)}
</div>
),
)}
@@ -19,7 +19,7 @@ export function TeamSelectionUI(props: {
currentTeam: Team | undefined;
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
upgradeTeamLink: string | undefined;
account: Pick<Account, "email" | "id"> | undefined;
account: Pick<Account, "email" | "id" | "image"> | undefined;
client: ThirdwebClient;
}) {
const { setHoveredTeam, currentTeam, teamsAndProjects } = props;
@@ -52,9 +52,8 @@ export function TeamSelectionUI(props: {
asChild
>
<Link href="/account">
{/* TODO set Image src */}
<GradientAvatar
src={""}
src={props.account?.image || ""}
className="size-4"
id={props.account?.id}
client={props.client}
@@ -65,6 +65,11 @@ export function TeamHeaderLoggedIn(props: {
<LazyCreateAPIKeyDialog
open={createProjectDialogState.isOpen}
teamSlug={
createProjectDialogState.isOpen
? createProjectDialogState.team.slug
: undefined
}
onOpenChange={() =>
setCreateProjectDialogState({
isOpen: false,
@@ -0,0 +1,17 @@
"use client";
import { usePathname } from "next/navigation";
import { useEffect } from "react";
import { setCookie } from "../../../../lib/cookie";
import { LAST_VISITED_TEAM_PAGE_PATH } from "./consts";
export function SaveLastVisitedTeamPage() {
const pathname = usePathname();
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
setCookie(LAST_VISITED_TEAM_PAGE_PATH, pathname);
}, [pathname]);
return null;
}
@@ -0,0 +1,3 @@
// Only keep constants here - this file is imported in middleware too
export const LAST_VISITED_TEAM_PAGE_PATH = "last-visited-team-page-path";
@@ -25,7 +25,7 @@ import { CircleAlertIcon, ExternalLinkIcon, InfoIcon } from "lucide-react";
import Link from "next/link";
import { useCallback, useMemo } from "react";
import { FormProvider, type UseFormReturn, useForm } from "react-hook-form";
import { ZERO_ADDRESS } from "thirdweb";
import { ZERO_ADDRESS, getContract } from "thirdweb";
import type { FetchDeployMetadataResult } from "thirdweb/contract";
import {
deployContractfromDeployMetadata,
@@ -567,11 +567,13 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
const contractAddr = await deployMutation.mutateAsync(formData);
// send verification request - no need to await
verifyContract({
address: contractAddr,
chain: walletChain,
client: thirdwebClient,
});
verifyContract(
getContract({
address: contractAddr,
chain: walletChain,
client: thirdwebClient,
}),
);
trackEvent({
category: "custom-contract",

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