Compare commits

..
Author SHA1 Message Date
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
5a4ef087ef Version Packages (#4742)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-23 19:17:14 +12:00
gregfromstl fc2ecdfd98 [React] Fix: hides guest accounts from linked profiles (#4687)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on hiding guest accounts from the linked profiles screen in React.

### Detailed summary
- Excludes guest profiles from being displayed on the linked profiles screen
- Updates the rendering logic to filter out guest profiles before displaying them

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

<!-- end pr-codex -->
2024-09-23 06:34:18 +00:00
joaquim-verges 33936b3a86 fix: handle BigInt serialization for ecosystem wallets (#4756)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to handle biging serialization for ecosystem wallets.

### Detailed summary
- Updated `signTypedData`, `signMessage`, and `signTransaction` functions to use `stringify` for serialization
- Improved handling of payload data in ecosystem wallets

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

<!-- end pr-codex -->
2024-09-23 05:29:44 +00:00
joaquim-verges 1133ccc239 fix: improve ABI resolution fallback (#4753)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on fixing ABI resolution fallback in `resolve-abi.ts`.

### Detailed summary
- Removed unnecessary imports
- Updated ABI resolution logic with new imports
- Improved handling of root ABI merging
- Enhanced efficiency in joining ABIs

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

<!-- end pr-codex -->
2024-09-23 02:14:14 +00:00
kien-ngo 3eef910a24 [Dashboard] Remove HStack (#4749)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces changes to replace `HStack` with `div` using flex classes in various components for better styling consistency.

### Detailed summary
- Replaced `HStack` with `div` using flex classes in multiple components for styling consistency.

> The following files were skipped due to too many changes: `apps/dashboard/src/components/smart-wallets/SponsorshipPolicies/index.tsx`

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

<!-- end pr-codex -->
2024-09-23 01:29:48 +00:00
kien-ngo 3d52dd4587 [Dashboard] Remove VStack (#4748)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to replace the `VStack` components with `div` elements styled using Tailwind CSS classes for layout consistency and improved accessibility.

### Detailed summary
- Replaced `VStack` with `div` in multiple components for layout consistency
- Used Tailwind CSS classes for styling and spacing
- Improved accessibility and readability of the codebase

> The following files were skipped due to too many changes: `apps/dashboard/src/components/settings/Account/AccountForm.tsx`, `apps/dashboard/src/components/settings/Account/Usage.tsx`

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

<!-- end pr-codex -->
2024-09-23 01:29:46 +00:00
jnsdls 24ff6f2ab6 knip it in the... (#4751)
1. make `knip` a part of `packages/thirdweb`
2. clean up all the things
3. ???
4. profit

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on removing unused files, updating types, and deprecating constants.

### Detailed summary
- Removed unused files related to benchmarks, wallets, and UI components
- Updated type definitions in various files
- Deprecated and replaced constant `ADDRESS_ZERO` with `ZERO_ADDRESS`

> The following files were skipped due to too many changes: `packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx`, `packages/thirdweb/src/pay/utils/definitions.ts`, `packages/thirdweb/src/utils/hashing/sha256.ts`, `packages/thirdweb/src/wallets/eip5792/show-calls-status.ts`, `packages/thirdweb/src/extensions/modules/ClaimableERC20/mint.ts`, `packages/thirdweb/src/react/web/wallets/shared/ConnectWalletSocialOptions.tsx`, `apps/portal/package.json`, `packages/thirdweb/src/extensions/modules/ClaimableERC721/mint.ts`, `packages/thirdweb/src/utils/hashing/hashMessage.ts`, `packages/thirdweb/src/react/native/ui/components/text.tsx`, `packages/thirdweb/src/utils/promise/resolve-promised-value.ts`, `packages/thirdweb/src/auth/core/verify-jwt.ts`, `packages/thirdweb/src/utils/hashing/keccak256.ts`, `packages/thirdweb/src/utils/uint8-array.ts`, `packages/thirdweb/src/react/core/design-system/CustomThemeProvider.tsx`, `packages/thirdweb/src/react/native/ui/connect/SendScreen.tsx`, `packages/thirdweb/src/react/web/ui/MediaRenderer/MediaRenderer.tsx`, `packages/thirdweb/src/gas/estimate-l1-fee.ts`, `packages/thirdweb/src/react/web/ui/ConnectWallet/WalletSelector.tsx`, `packages/thirdweb/src/extensions/modules/MintableERC721/mintWithSignature.ts`, `packages/thirdweb/src/react/native/ui/connect/ExternalWalletsList.tsx`, `packages/thirdweb/src/exports/extensions/erc1155.ts`, `packages/thirdweb/src/extensions/modules/ClaimableERC1155/mint.ts`, `packages/thirdweb/src/extensions/modules/MintableERC1155/mintWithSignature.ts`, `packages/thirdweb/src/utils/encoding/helpers/trim.ts`, `packages/thirdweb/src/wallets/utils/getTokenBalance.ts`, `apps/dashboard/knip.json`, `packages/thirdweb/src/react/native/ui/connect/TokenListScreen.tsx`, `packages/thirdweb/src/extensions/vote/write/executeProposal.ts`, `packages/thirdweb/src/react/native/ui/components/spinner.tsx`, `packages/thirdweb/src/react/web/ui/MediaRenderer/ModelViewer.tsx`, `packages/thirdweb/src/transaction/extract-error.ts`, `packages/thirdweb/src/react/native/ui/components/button.tsx`, `packages/thirdweb/src/extensions/erc1155/write/updateTokenURI.ts`, `packages/thirdweb/src/wallets/interfaces/wallet.ts`, `packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/useSwapSupportedChains.ts`, `packages/thirdweb/src/utils/storage/walletStorage.ts`, `packages/thirdweb/src/wallets/in-app/native/native-connector.ts`, `packages/thirdweb/src/extensions/erc20/write/sigMint.ts`, `packages/thirdweb/src/extensions/erc721/write/updateTokenURI.ts`, `packages/thirdweb/src/react/web/wallets/ecosystem/EcosystemWalletFormUI.tsx`, `packages/thirdweb/src/wallets/eip5792/wait-for-bundle.ts`, `packages/thirdweb/src/utils/jwt/jwt-header.ts`, `packages/thirdweb/src/extensions/modules/MintableERC20/mintWithSignature.ts`, `packages/thirdweb/src/exports/modules.ts`, `packages/thirdweb/src/react/web/wallets/in-app/InAppWalletFormUI.tsx`, `packages/thirdweb/src/transaction/actions/wait-for-tx-receipt.ts`, `packages/thirdweb/src/auth/core/generate-jwt.ts`, `packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts`, `packages/thirdweb/src/exports/extensions/erc1271.ts`, `packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/useBuyTxStates.ts`, `packages/thirdweb/src/contract/deployment/publisher.ts`, `packages/thirdweb/src/extensions/erc721/drops/write/updateMetadata.ts`, `packages/thirdweb/src/extensions/erc1155/drops/write/updateMetadata.ts`, `packages/thirdweb/src/extensions/prebuilts/deploy-marketplace.ts`, `packages/thirdweb/src/wallets/in-app/native/auth/index.ts`, `packages/thirdweb/src/wallets/in-app/native/helpers/auth/middleware.ts`, `packages/thirdweb/src/utils/platform.ts`, `.github/CODEOWNERS`, `packages/thirdweb/src/wallets/in-app/web/lib/auth/index.ts`, `packages/thirdweb/src/react/core/utils/structuralSharing.ts`, `packages/thirdweb/src/react/core/hooks/transaction/useSendAndConfirmTransaction.ts`, `packages/thirdweb/src/extensions/prebuilts/get-required-transactions.ts`, `packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/FiatDetailsScreen.tsx`, `packages/thirdweb/src/extensions/ens/resolve-l2-name.ts`, `packages/thirdweb/src/utils/any-evm/deploy-metadata.ts`, `packages/thirdweb/src/cli/commands/generate/utils.ts`, `packages/thirdweb/src/extensions/farcaster/eip712Signatures/keyRequestSignature.ts`, `packages/thirdweb/src/extensions/airdrop/write/airdropERC20WithSignature.ts`, `packages/thirdweb/src/extensions/airdrop/write/airdropERC721WithSignature.ts`, `packages/thirdweb/src/transaction/actions/zksync/send-eip712-transaction.ts`, `packages/thirdweb/src/wallets/in-app/web/utils/iFrameCommunication/InAppWalletIframeCommunicator.ts`, `packages/thirdweb/src/utils/any-evm/zksync/constants.ts`, `packages/thirdweb/src/extensions/marketplace/direct-listings/utils.ts`, `packages/thirdweb/src/transaction/actions/gasless/send-gasless-transaction.ts`, `packages/thirdweb/src/extensions/prebuilts/deploy-vote.ts`, `packages/thirdweb/src/react/web/ui/components/Drawer.tsx`, `packages/thirdweb/src/extensions/airdrop/write/airdropERC1155WithSignature.ts`, `packages/thirdweb/src/react/core/hooks/others/useWalletBalance.ts`, `packages/thirdweb/src/extensions/permissions/utils.ts`, `packages/thirdweb/src/wallets/in-app/native/helpers/auth/logout.ts`, `packages/thirdweb/src/wallets/smart/lib/utils.ts`, `packages/thirdweb/src/react/web/ui/components/buttons.tsx`, `packages/thirdweb/src/react/web/ui/prebuilt/thirdweb/ClaimButton/index.tsx`, `packages/thirdweb/src/wallets/wallet-connect/receiver/index.ts`, `packages/thirdweb/src/transaction/actions/gasless/providers/engine.ts`, `packages/thirdweb/src/transaction/actions/gasless/providers/openzeppelin.ts`, `packages/thirdweb/src/wallets/in-app/web/types.ts`, `packages/thirdweb/src/extensions/airdrop/write/claimERC20.ts`, `packages/thirdweb/test/globalSetup.ts`, `packages/thirdweb/src/extensions/erc721/read/getAllOwners.ts`, `packages/thirdweb/src/wallets/smart/lib/userop.ts`, `packages/thirdweb/src/wallets/in-app/native/helpers/wallet/retrieval.ts`, `packages/thirdweb/src/wallets/in-app/web/lib/iframe-wallet.ts`, `packages/thirdweb/src/react/web/ui/TransactionButton/TransactionModal.tsx`, `packages/thirdweb/src/react/native/ui/connect/ConnectedModal.tsx`, `packages/thirdweb/src/extensions/erc4337/account/common.ts`, `packages/thirdweb/test/vitest.config.ts`, `packages/thirdweb/src/exports/extensions/farcaster.ts`, `packages/thirdweb/src/exports/chains.ts`, `packages/thirdweb/src/extensions/erc4337/account/permissions.test.ts`, `packages/thirdweb/src/wallets/in-app/core/constants/settings.ts`, `packages/thirdweb/scripts/typedoc.mjs`, `packages/thirdweb/src/extensions/airdrop/write/merkleInfoERC20.ts`, `packages/thirdweb/src/utils/extensions/drops/hash-entry.ts`, `packages/thirdweb/src/extensions/farcaster/eip712Signatures/registerSignature.ts`, `packages/thirdweb/src/react/native/ui/icons/svgs.ts`, `packages/thirdweb/src/transaction/actions/gasless/providers/biconomy.ts`, `packages/thirdweb/src/extensions/farcaster/eip712Signatures/addSignature.ts`, `packages/thirdweb/src/react/web/ui/AutoConnect/AutoConnect.tsx`, `packages/thirdweb/src/react/native/ui/AutoConnect/AutoConnect.tsx`, `packages/service-utils/package.json`, `packages/thirdweb/src/stories/utils.tsx`, `packages/thirdweb/src/react/core/utils/defaultTokens.ts`, `package.json`, `packages/thirdweb/knip.json`, `packages/thirdweb/src/pay/buyWithCrypto/getStatus.ts`, `packages/thirdweb/src/exports/extensions/marketplace.ts`, `packages/thirdweb/src/utils/semver.ts`, `packages/thirdweb/src/extensions/uniswap/read/getUniswapV3Pools.ts`, `apps/dashboard/package.json`, `packages/thirdweb/src/utils/royalty-engine.ts`, `packages/react-native-adapter/package.json`, `packages/thirdweb/src/wallets/in-app/core/authentication/types.ts`, `packages/thirdweb/src/contract/deployment/deploy-deterministic.ts`, `packages/thirdweb/src/contract/deployment/utils/infra.ts`, `packages/thirdweb/src/wallets/injected/index.ts`, `packages/thirdweb/src/wallets/smart/lib/packUserOp.ts`, `packages/thirdweb/src/wallets/in-app/native/helpers/storage/local.ts`, `packages/thirdweb/src/react/web/ui/components/ChainIcon.tsx`, `packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/BuyTxHistory.tsx`, `packages/thirdweb/src/wallets/in-app/native/helpers/constants.ts`, `packages/thirdweb/src/wallets/in-app/native/helpers/api/fetchers.ts`, `packages/thirdweb/package.json`, `packages/thirdweb/src/react/core/utils/walletIcon.ts`, `pnpm-lock.yaml`

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

<!-- end pr-codex -->
2024-09-23 00:00:12 +00:00
Joaquim VergesandGitHub e9ae98084a feat(ReactNative): respect icon theme color in wallet UI (#4752) 2024-09-23 11:56:47 +12:00
MananTank d936d4e5b9 Remove permissions form field in ecosystem partners page + UI adjusments (#4747)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR removes unused code related to partner permissions and updates UI components in the dashboard ecosystem section.

### Detailed summary
- Removed unused `PartnerPermission` type and related code
- Updated UI components in the dashboard ecosystem section for better user experience

> The following files were skipped due to too many changes: `apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/add-partner-form.client.tsx`

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

<!-- end pr-codex -->
2024-09-21 22:51:27 +00:00
MananTank bc7288370c Migrate Network Config Modal from chakra to shadcn/tailwind (#4745)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR removes unused components, updates UI elements, and refactors form fields in the dashboard application.

### Detailed summary
- Deleted `TooltipBox.tsx` and `ConfirmationPopover.tsx`
- Updated UI elements in `IconUpload.tsx` and `ConfigureNetworkModal.tsx`
- Refactored form fields in `ChainIdInput.tsx`, `RpcInput.tsx`, and `NetworkIDInput.tsx`

> The following files were skipped due to too many changes: `apps/dashboard/src/@/components/ui/radio-group.tsx`, `apps/dashboard/src/components/configure-networks/ConfigureNetworkForm.tsx`

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

<!-- end pr-codex -->
2024-09-21 21:24:47 +00:00
MananTank 0a4e44575d Remove all chain override contexts and move to global store (#4743)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to refactor chain-related functionality and imports, update TypeScript types, and improve store management.

### Detailed summary
- Refactored chain-related functionality and imports
- Updated TypeScript types for chain mapping
- Improved store management for chains
- Updated API calls for chain data
- Removed unused hooks and contexts

> The following files were skipped due to too many changes: `apps/dashboard/src/components/configure-networks/ConfigureNetworkModal.tsx`, `apps/dashboard/src/contract-ui/tabs/account-permissions/components/account-signer.tsx`, `apps/dashboard/src/contract-ui/tabs/split/page.tsx`, `apps/dashboard/src/stores/SyncStoreToStorage.tsx`, `apps/dashboard/src/components/contract-components/contract-publish-form/NetworkDropdown.tsx`, `apps/dashboard/src/components/configure-networks/Form/NetworkIdInput.tsx`, `apps/dashboard/src/contract-ui/tabs/embed/components/embed-setup.tsx`, `apps/dashboard/src/contract-ui/tabs/code/components/code-overview.tsx`, `apps/dashboard/src/components/engine/contract-subscription/contract-subscriptions-table.tsx`, `apps/dashboard/src/components/buttons/MismatchButton.tsx`, `apps/dashboard/src/components/selects/NetworkSelectDropdown.tsx`, `apps/dashboard/src/components/selects/CustomChainRenderer.tsx`, `apps/dashboard/src/components/app-layouts/app.tsx`, `apps/dashboard/src/components/contract-components/published-contract/addresses-modal.tsx`, `apps/dashboard/src/components/contract-components/tables/deployed-contracts.tsx`, `apps/dashboard/src/lib/v5-adapter.ts`, `apps/dashboard/src/components/engine/overview/transactions-table.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useRegistry.ts`, `apps/dashboard/src/stores/chainStores.tsx`, `apps/dashboard/src/components/engine/relayer/relayers-table.tsx`, `apps/dashboard/src/components/configure-networks/ConfigureNetworkForm.tsx`, `apps/dashboard/src/hooks/chains/allChains.ts`, `apps/dashboard/src/pages/[chain_id]/[...paths].tsx`, `apps/dashboard/src/components/selects/NetworkSelectorButton.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/components/connect-wallet/index.tsx`

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

<!-- end pr-codex -->
2024-09-21 21:10:49 +00:00
arcoraven f2307fde02 chore: show min version number (#4744)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates the alert management feature message for cloud-hosted Engines to specify version requirement.

### Detailed summary
- Updated alert management feature message for cloud-hosted Engines to require v2.0.10 or later.

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

<!-- end pr-codex -->
2024-09-21 16:30:43 +00:00
kien-ngo a4dac95630 [SDK] Fix jsdoc for getCLaimParams (#4738)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to fix the JSDoc for the `getClaimParams` function in the `thirdweb` package.

### Detailed summary
- Updated JSDoc for `getClaimParams` in `thirdweb/utils` directory.
- Changed import path from `thirdweb/extensions/erc1155` to `thirdweb/utils`.

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

<!-- end pr-codex -->
2024-09-21 11:08:35 +00:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
f424d0ab2d Version Packages (#4741)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-21 22:46:48 +12:00
Joaquim VergesandGitHub 915442e1c4 feat: enable wallet linking and guest mode for ecosystem wallets (#4740) 2024-09-21 22:31:25 +12:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
89728c6578 Version Packages (#4739)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-21 19:43:51 +12:00
Joaquim VergesandGitHub 2707db136e feat: allow linking identities when using in-app + smart wallets (#4737) 2024-09-21 19:34:23 +12:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
ca23a95bfd Version Packages (#4735)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-21 14:27:50 +12:00
Joaquim VergesandGitHub 997bfa84cc Update pregenerate docs (#4736)
Signed-off-by: Joaquim Verges <[email protected]>
2024-09-21 13:14:48 +12:00
gregfromstl 824b1bdb69 [SDK] Feature: Ecosystem account linking (#4733)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enables account linking for ecosystem wallets.

### Detailed summary
- Added support for ecosystem wallets in account linking functionality
- Updated conditions for multi-auth support in various components
- Imported and utilized `isEcosystemWallet` function in multiple files
- Modified functions to handle both in-app and ecosystem wallets in account linking operations

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

<!-- end pr-codex -->
2024-09-21 01:01:07 +00:00
Joaquim VergesandGitHub 6e3669d384 chore(docs): update embedded wallet API endpoint (#4734) 2024-09-21 11:54:20 +12:00
gregfromstl 09eb0e6a3f Fix/pregen docs (#4732)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the structure of the `wallet` object in the `page.mdx` file to include `address` and `createdAt` properties.

### Detailed summary
- Updated the `wallet` object in `page.mdx` to include `address` and `createdAt` properties.

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

<!-- end pr-codex -->
2024-09-20 23:37:40 +00:00
gregfromstl 786ef9d777 Docs: Get User Page (#4730)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to add guides for retrieving user details from the server using In-App Wallets and pregenerating user wallets.

### Detailed summary
- Added guide for retrieving In-App Wallet user details on the server
- Added guide for pregenerating user wallets
- Updated sidebar links for better navigation

> The following files were skipped due to too many changes: `apps/portal/src/app/connect/sidebar.tsx`, `apps/portal/redirects.mjs`

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

<!-- end pr-codex -->
2024-09-20 23:11:42 +00:00
Joaquim VergesandGitHub f8e69989a3 feat(playground): update ecosystem for prod (#4731) 2024-09-21 10:52:32 +12:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
07d191bd8b Version Packages (#4710)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-21 10:12:04 +12:00
arcoraven f8d3578bbc Add Engine Alerts to dashboard (#4695)
<!-- start pr-codex -->

## PR-Codex overview
This PR adds alert management features to the dashboard, including alert rules, notifications, and recent alerts.

### Detailed summary
- Added alert management pages and components
- Updated cache keys for alert-related data
- Improved styling for autofilled inputs
- Created modals for deleting alerts
- Added stubs for alert-related entities
- Implemented UI components for recent alerts section

> The following files were skipped due to too many changes: `apps/dashboard/src/components/engine/alerts/RecentEngineAlerts.tsx`, `apps/dashboard/src/components/engine/alerts/ManageEngineAlerts.stories.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts`, `apps/dashboard/src/components/engine/alerts/EngineAlertDialogForm.tsx`, `apps/dashboard/src/components/engine/alerts/ManageEngineAlerts.tsx`

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

<!-- end pr-codex -->
2024-09-20 22:02:01 +00:00
MananTank 60a77e0b31 Fix '0' rendered in Contract Overview page (#4729)
Because `0 && <div />` returns `0` and react renders `0`
2024-09-20 20:41:13 +00:00
MananTank eb79810afa Replace all isLoading to isPending in dashboard (#4728)
### Why?

we recently moved from react-query v4 to v5.

In v5, `isLoading` is now called `isPending`

but we haven't changed `isLoading` to `isPending` and that has introduced some bugs related to loading spinners/skeletons where they are not shown if the query is not currently being fetched (not enabled yet)

`isLoading` is no longer `true` from the beggining as it is assumed to be throughout the codebase in v5 - it is only true when a fetch is in-flight

To get a the "loading" state in v5 - we should be using the `isPending` state. `isPending` is true  -> "if there's no cached data and no query attempt was finished yet" according to the docs:

https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#status-loading-has-been-changed-to-status-pending-and-isloading-has-been-changed-to-ispending-and-isinitialloading-has-now-been-renamed-to-isloading

in v5 , `isLoading` is  `isFetching && isPending`
2024-09-20 20:41:11 +00:00
MananTank 200e8da84b Fix login page centered layout + some UI adjustments (#4726)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to update the styling of the login page in the dashboard app for better UI/UX.

### Detailed summary
- Updated main layout and styling of the login page
- Adjusted heading size and alignment
- Applied shadow to the custom connect component
- Positioned background image dynamically based on screen size

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

<!-- end pr-codex -->
2024-09-20 17:05:54 +00:00
MananTank 2ece661620 Remove Configure Network Modal contexts + Fix in App Router (#4725)
FIXES: DASH-259
2024-09-20 16:55:21 +00:00
MananTank fb92f873cd Fix body height issue in dashboard (#4724)
Fixes body not expanding to content size:

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/yNOf1svJ8o3zjO7zQouZ/de2d88d4-db8c-4325-9135-8fc3d400a75b.png)
2024-09-20 14:15:20 +00:00
arcoraven 63ada206a4 Add Engine troubleshooting page (#4723)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is to add a troubleshooting page with detailed explanations and solutions for common transaction issues in the Engine application.

### Detailed summary
- Added troubleshooting page for transaction issues
- Imported `OutOfGasImage` for visual aid
- Provided detailed explanations and resolutions for common transaction errors

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

<!-- end pr-codex -->
2024-09-20 06:56:18 +00:00
joaquim-verges 7d547a4e33 fix: show user info for ecosystem wallets (#4715)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on fixing the display of user information for ecosystem wallets in the Thirdweb application.

### Detailed summary
- Updated function name from `getEnclaveUserStatus` to `getUserStatus` in multiple files.
- Modified parameters in functions to include `ecosystem` for ecosystem wallets.
- Updated caching mechanism for InAppWalletConnector.
- Added logic to handle ecosystem wallets in `InAppWalletUserInfo` component.

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

<!-- end pr-codex -->
2024-09-20 03:35:18 +00:00
gregandGitHub c1008a5314 [SDK] Feature: Enclave migration (#4684) 2024-09-20 15:11:36 +12:00
gregfromstl 11a833e1dc [React] Fix: Improve social button icon arrangement (#4718)
![Screenshot 2024-09-19 at 5 13 13 PM](https://github.com/user-attachments/assets/ae06781e-6b44-4231-9385-0a4ac27be563)
![Screenshot 2024-09-19 at 5 13 07 PM](https://github.com/user-attachments/assets/23b3f073-b6ab-4aae-8be8-a5820763cd65)

<!-- start pr-codex -->

---

## PR-Codex overview
This PR improves the layout of social icons in the Connect UI by organizing them into columns for better user experience.

### Detailed summary
- Added logic to arrange social icons into columns based on the number of login options
- Updated the layout of social buttons to display them in columns instead of a row

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

<!-- end pr-codex -->
2024-09-20 01:29:03 +00:00
joaquim-verges c67d8a9dd2 feat(playground): add ecosystem and sponsored transaction pages for in-app wallet (#4719)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to enhance the in-app wallet feature by adding expanded navigation links, ecosystem connection, and sponsored transaction previews.

### Detailed summary
- Added expanded navigation links in the in-app wallet section
- Implemented ecosystem connection functionality
- Included sponsored transaction previews for in-app wallets

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

<!-- end pr-codex -->
2024-09-20 01:10:26 +00:00
gregfromstl dab7b4ebd8 style(dashboard): add padding to partners table (#4713)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to rename `EcosystemRow` component to `PartnerRow` and update styling in the partners table.

### Detailed summary
- Renamed `EcosystemRow` component to `PartnerRow`.
- Updated styling in the partners table for better alignment.

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

<!-- end pr-codex -->
2024-09-20 00:46:44 +00:00
MananTank a491d8bd7c Show Deterministic Deploy warning banner if constructor params exist (#4717)
FIXES DASH-257

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to enhance the Custom Contract Form in the dashboard by adding a warning for deterministic deployment.

### Detailed summary
- Added `CircleAlertIcon` and warning message for deterministic deployment
- Updated the layout of the deterministic deploy checkbox and warning display

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

<!-- end pr-codex -->
2024-09-20 00:26:34 +00:00
joaquim-verges a7b3d0e72c feat: remove prompting for enclave ecosystem wallets (#4711)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR removes the usage of `getEcosystemPartnerPermissions` function and related logic in `IFrameWallet` class.

### Detailed summary
- Removed `getEcosystemPartnerPermissions` function usage
- Removed logic related to partner permissions and showing the iframe based on permissions

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

<!-- end pr-codex -->
2024-09-20 00:04:05 +00:00
gregfromstl b0337849a5 [React] Feature: Shows the social provider name in details modal (#4709)
![Screenshot 2024-09-19 at 2 22 09 PM](https://github.com/user-attachments/assets/9c8a14e9-3328-4e6f-8d56-0c420bdeea17)

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on displaying the social login provider in the details modal for wallet connections.

### Detailed summary
- Added `SocialAuthOption` type and `socialAuthOptions` import
- Updated logic to display social login provider in details modal based on active wallet and last auth provider

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

<!-- end pr-codex -->
2024-09-19 23:45:23 +00:00
MananTank 3229e1f03c Fix Explorer page component re-renders (#4716)
FIXES: DASH-207
2024-09-19 23:34:04 +00:00
gregfromstl f228e03ecc [Playground] Fix: Correct IAW checkbox labels (#4708)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `InAppWalletFormGroup` component to dynamically generate unique IDs for `htmlFor` and `id` attributes based on the `method`.

### Detailed summary
- Dynamically generate unique IDs for `htmlFor` and `id` attributes based on the `method` value in `InAppWalletFormGroup.tsx`

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

<!-- end pr-codex -->
2024-09-19 22:05:35 +00:00
gregfromstl 0a7448cb2b [SDK] Feature: Coinbase login (#4692)
DO NOT MERGE, needs IAW PR to merge first

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces Coinbase as a new authentication option across various wallet-related components.

### Detailed summary
- Added `coinbase` as an authentication option in wallets
- Updated icons and authentication types to include Coinbase
- Updated in-app wallet authentication types to include Coinbase
- Updated login and connection options to support Coinbase

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

<!-- end pr-codex -->
2024-09-19 21:29:08 +00:00
jnsdls f51dd8f1ac ban createContext usage from dashboard (#4707)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/VXaCVgbx1DROqoQd8UuD/786700f4-1588-4238-a7eb-884c2df54584.png)
2024-09-19 21:02:18 +00:00
MananTank 448dc21cba Add multi-select component that can render thousands of options efficiently (#4698)
## Problem solved

Added this to use it for multi network selector component. The current multi network selector component is abysmally slow.

This component is not added to website yet on this PR - coming up in next stack

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/yNOf1svJ8o3zjO7zQouZ/911a10ef-405b-4dab-8da8-ab008a1fca73.png)

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on adding a `useShowMore` hook for lazy loading items and a `MultiSelect` component with search functionality and item selection.

### Detailed summary
- Added `useShowMore` hook for lazy loading items in `useShowMore.ts`
- Implemented `MultiSelect` component with search, selection, and display options in `multi-select.tsx`
- Created stories for `MultiSelect` component variations in `multi-select.stories.tsx`

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

<!-- end pr-codex -->
2024-09-19 19:53:54 +00:00
jnsdls 2a51d3c5ae improve deploy deterministic test accuracy (#4706) 2024-09-19 19:25:01 +00:00
gregfromstl 80a2eba0f4 [SDK] Feature: Enclave wallets (#4651)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the enclave wallet functionality by adding new types and methods for wallet setup, authentication, and actions.

### Detailed summary
- Added `IWebWallet` interface with new methods
- Updated wallet setup and authentication types
- Implemented actions for enclave wallet generation, signing messages, transactions, and typed data
- Modified `IFrameWallet` class for wallet setup
- Added new authentication methods for enclave wallets

> The following files were skipped due to too many changes: `packages/thirdweb/src/wallets/in-app/web/lib/auth/iframe-auth.ts`, `packages/thirdweb/src/wallets/in-app/web/lib/enclave-wallet.ts`, `packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts`

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

<!-- end pr-codex -->
2024-09-19 18:46:23 +00:00
YashandGitHub 5b48590034 add faq for account abstraction - trusted forwarders (#4705) 2024-09-19 10:45:42 -07:00
Jonas DanielsandGitHub 8fa6227fe4 Refactor client authentication logic (#4703) 2024-09-19 09:45:11 -07:00
arcoraven fbffa718b4 Fix: Add keypair auth ID to table (#4700)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates the `.env.example` file with a new API server secret and adds a new column to display Key ID in the `keypairs-table.tsx` component.

### Detailed summary
- Updated API_SERVER_SECRET in `.env.example`
- Added a new column to display Key ID in `keypairs-table.tsx`

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

<!-- end pr-codex -->
2024-09-19 16:27:18 +00:00
kien-ngo 9e11c15c2d [Dashboard] Clean up some clipboard code (#4702)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a `useClipboard` custom hook to manage clipboard functionality in multiple components, replacing direct clipboard interactions with a reusable hook.

### Detailed summary
- Introduced `useClipboard` custom hook for clipboard functionality
- Replaced direct clipboard interactions with `useClipboard` in `CopyButton.tsx`, `CopyTextButton.tsx`, and `WalletAddress.tsx`
- Removed `useState` for managing copied state in favor of `useClipboard` in all affected components

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

<!-- end pr-codex -->
2024-09-19 16:18:43 +00:00
kien-ngo 5c23e5237f [Dashboard] Remove chakra-ui Flex (2) (#4699)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates various components to replace `<Flex>` with `<div>` for styling consistency.

### Detailed summary
- Replaced `<Flex>` with `<div>` in multiple components for styling consistency.

> The following files were skipped due to too many changes: `apps/dashboard/src/components/mission/MajorSection.tsx`

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

<!-- end pr-codex -->
2024-09-19 14:50:39 +00:00
kien-ngo a16293a10c [Dashboard] Remove chakra <Flex/ > (1) (#4697)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates various components to replace `<Flex>` with `<div>` for styling consistency.

### Detailed summary
- Replaces `<Flex>` with `<div>` in multiple components for styling consistency.

> The following files were skipped due to too many changes: `apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/credits/SettingsCreditsPage.tsx`, `apps/dashboard/src/pages/dashboard/settings/devices.tsx`, `apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/UsagePage.tsx`, `apps/dashboard/src/components/dashboard/Changelog.tsx`, `apps/dashboard/src/pages/dashboard/settings/api-keys/index.tsx`

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

<!-- end pr-codex -->
2024-09-19 14:50:34 +00:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
7ff46a50d2 Version Packages (#4694)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-19 22:08:29 +12:00
Joaquim VergesandGitHub 0c97d7965e Handle default chain when connecting via redirect URL (#4696) 2024-09-19 22:00:25 +12:00
jnsdls 4a87109289 Fix getClaimConditions for ERC20, ERC721, and ERC1155 (#4693)
FIXES: DASH-249
2024-09-19 06:35:12 +00:00
jnsdls dddf95daca v2 auth endpoints (#4689)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is to add a new API server secret variable and update authentication actions to use it securely.

### Detailed summary
- Added `API_SERVER_SECRET` variable in `.env.example`
- Updated authentication actions to use `API_SERVER_SECRET` securely
- Replaced `json.token` with `jwt` for better security

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

<!-- end pr-codex -->
2024-09-19 05:55:18 +00:00
jnsdls 06cd6a62d9 fix split deployments (#4690)
FIXES: DASH-253
2024-09-19 05:29:13 +00:00
546 changed files with 8856 additions and 13120 deletions
-1
View File
@@ -11,7 +11,6 @@ packages/thirdweb/src/react/ @joaquim-verges @gregfromstl @MananTank @jnsdls @ed
packages/thirdweb/src/reactive/ @joaquim-verges @gregfromstl @MananTank @jnsdls
packages/thirdweb/src/gas/ @joaquim-verges @jnsdls
packages/thirdweb/src/pay/ @joaquim-verges @gregfromstl @MananTank @jnsdls @edwardysun
packages/typedoc-gen/ @MananTank @jnsdls @joaquim-verges
packages/service-utils/ @arcoraven @jnsdls @joaquim-verges
packages/eslint-config-thirdweb/ @jnsdls @joaquim-verges
packages/tw-tsconfig/ @jnsdls @joaquim-verges
+4 -1
View File
@@ -93,4 +93,7 @@ UNTHREAD_GROWTH_TIER_ID=""
UNTHREAD_PRO_TIER_ID=""
# Demo Engine
NEXT_PUBLIC_DEMO_ENGINE_URL=""
NEXT_PUBLIC_DEMO_ENGINE_URL=""
# API server secret (required for thirdweb.com SIWE login). Copy from Vercel.
API_SERVER_SECRET=""
+7
View File
@@ -14,6 +14,11 @@ module.exports = {
message:
'Are you *sure* you need to use "useEffect" here? If you loading any async function prefer using "useQuery".',
},
{
selector: "CallExpression[callee.name='createContext']",
message:
'Are you *sure* you need to use a "Context"? In almost all cases you should prefer passing props directly.',
},
{
selector: "CallExpression[callee.name='defineChain']",
message:
@@ -50,6 +55,8 @@ module.exports = {
"FormErrorMessage",
"MenuGroup",
"MenuItem",
"VStack",
"HStack",
// also the types
"ButtonProps",
"BadgeProps",
+1 -1
View File
@@ -3,6 +3,6 @@
"next": true,
"ignore": ["src/@/components/ui/**"],
"project": ["src/**"],
"ignoreBinaries": ["only-allow", "knip", "biome"],
"ignoreBinaries": ["only-allow", "biome"],
"ignoreDependencies": ["@storybook/blocks", "@thirdweb-dev/service-utils"]
}
+10 -8
View File
@@ -10,6 +10,7 @@
"format": "biome format ./src --write",
"lint": "biome check ./src && knip && eslint ./src",
"fix": "biome check ./src --fix && eslint ./src --fix",
"typecheck": "tsc --noEmit",
"gen:theme-typings": "chakra-cli tokens src/theme/index.ts",
"postinstall": "pnpm run gen:theme-typings",
"postbuild": "next-sitemap",
@@ -110,21 +111,21 @@
"@next/bundle-analyzer": "14.2.12",
"@next/eslint-plugin-next": "14.2.12",
"@playwright/test": "1.47.1",
"@storybook/addon-essentials": "8.3.1",
"@storybook/addon-interactions": "8.3.1",
"@storybook/addon-links": "8.3.1",
"@storybook/addon-onboarding": "8.3.1",
"@storybook/addon-essentials": "8.3.2",
"@storybook/addon-interactions": "8.3.2",
"@storybook/addon-links": "8.3.2",
"@storybook/addon-onboarding": "8.3.2",
"@storybook/addon-viewport": "8.3.1",
"@storybook/blocks": "8.3.1",
"@storybook/nextjs": "8.3.1",
"@storybook/react": "8.3.1",
"@storybook/test": "8.3.1",
"@storybook/react": "8.3.2",
"@storybook/test": "8.3.2",
"@types/color": "^3.0.6",
"@types/node": "20.14.9",
"@types/papaparse": "^5.3.14",
"@types/pluralize": "^0.0.33",
"@types/qrcode": "^1.5.5",
"@types/react": "^18.3.7",
"@types/react": "^18.3.8",
"@types/react-dom": "^18",
"@types/react-table": "^7.7.20",
"@types/spdx-correct": "^3.1.3",
@@ -137,9 +138,10 @@
"eslint-config-biome": "1.8.3",
"eslint-plugin-react-compiler": "0.0.0-experimental-ca16900-20240916",
"eslint-plugin-storybook": "^0.8.0",
"knip": "^5.30.4",
"next-sitemap": "^4.2.3",
"postcss": "8.4.47",
"storybook": "8.3.1",
"storybook": "8.3.2",
"tailwindcss": "3.4.12",
"typescript": "5.6.2"
}
@@ -36,7 +36,7 @@ function Story() {
description="This is a description"
buttonLabel="Some Action"
buttonOnClick={() => {}}
isLoading={false}
isPending={false}
confirmationDialog={{
title: "This is confirmation title",
description: "This is confirmation description",
@@ -50,7 +50,7 @@ function Story() {
description="This is a description"
buttonLabel="Some Action"
buttonOnClick={() => {}}
isLoading={true}
isPending={true}
confirmationDialog={{
title: "This is confirmation title",
description: "This is confirmation description",
@@ -17,7 +17,7 @@ export function DangerSettingCard(props: {
description: string;
buttonLabel: string;
buttonOnClick: () => void;
isLoading: boolean;
isPending: boolean;
confirmationDialog: {
title: string;
description: string;
@@ -39,9 +39,9 @@ export function DangerSettingCard(props: {
<Button
variant="destructive"
className="bg-red-600 hover:bg-red-600/80 text-white font-semibold gap-2"
disabled={props.isLoading}
disabled={props.isPending}
>
{props.isLoading && <Spinner className="size-3" />}
{props.isPending && <Spinner className="size-3" />}
{props.buttonLabel}
</Button>
</DialogTrigger>
@@ -66,9 +66,9 @@ export function DangerSettingCard(props: {
variant="destructive"
className="bg-red-600 hover:bg-red-600/80 text-white font-semibold gap-2"
onClick={props.buttonOnClick}
disabled={props.isLoading}
disabled={props.isPending}
>
{props.isLoading && <Spinner className="size-3" />}
{props.isPending && <Spinner className="size-3" />}
{props.buttonLabel}
</Button>
</DialogFooter>
@@ -0,0 +1,36 @@
import { Label } from "@/components/ui/label";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { AsteriskIcon, InfoIcon } from "lucide-react";
export function FormFieldSetup(props: {
htmlFor: string;
label: string;
errorMessage: React.ReactNode | undefined;
children: React.ReactNode;
tooltip: React.ReactNode | undefined;
isRequired: boolean;
}) {
return (
<div>
<div className="mb-2 inline-flex gap-1 items-center">
<Label htmlFor={props.htmlFor}>{props.label}</Label>
{props.isRequired && (
<AsteriskIcon className="text-destructive-text size-3.5" />
)}
{props.tooltip && (
<ToolTipLabel label={props.tooltip}>
<InfoIcon className="text-muted-foreground size-3.5" />
</ToolTipLabel>
)}
</div>
{props.children}
{props.errorMessage && (
<p className="text-destructive-text text-sm mt-1">
{props.errorMessage}
</p>
)}
</div>
);
}
@@ -81,7 +81,7 @@ function Story() {
}}
saveButton={{
disabled: false,
isLoading: false,
isPending: false,
onClick: () => {},
}}
noPermissionText={"You do not have permission to edit this"}
@@ -100,7 +100,7 @@ function Story() {
}}
saveButton={{
disabled: false,
isLoading: false,
isPending: false,
onClick: () => {},
}}
noPermissionText={undefined}
@@ -119,7 +119,7 @@ function Story() {
}}
saveButton={{
disabled: true,
isLoading: false,
isPending: false,
onClick: () => {},
}}
noPermissionText={undefined}
@@ -138,7 +138,7 @@ function Story() {
}}
saveButton={{
disabled: false,
isLoading: true,
isPending: true,
onClick: () => {},
}}
noPermissionText={undefined}
@@ -15,7 +15,7 @@ export function SettingsCard(props: {
saveButton?: {
onClick?: () => void;
disabled: boolean;
isLoading: boolean;
isPending: boolean;
type?: "submit";
};
}) {
@@ -59,12 +59,12 @@ export function SettingsCard(props: {
size="sm"
className="gap-2"
onClick={props.saveButton.onClick}
disabled={props.saveButton.disabled || props.saveButton.isLoading}
disabled={props.saveButton.disabled || props.saveButton.isPending}
variant="outline"
type={props.saveButton.type}
>
{props.saveButton.isLoading && <Spinner className="size-3" />}
{props.saveButton.isLoading ? "Saving" : "Save"}
{props.saveButton.isPending && <Spinner className="size-3" />}
{props.saveButton.isPending ? "Saving" : "Save"}
</Button>
)}
</div>
@@ -0,0 +1,79 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useMemo, useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { MultiSelect } from "./multi-select";
const meta = {
title: "blocks/MultiSelect",
component: Story,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function createList(len: number) {
return Array.from({ length: len }, (_, i) => ({
value: `${i}`,
label: `Item ${i}`,
}));
}
function Story() {
return (
<div className="py-6 max-w-[600px] mx-auto w-full px-4 flex flex-col gap-6">
<VariantTest defaultValues={[]} storyLabel="5 items" listLen={5} />
<VariantTest defaultValues={[]} storyLabel="5000 items" listLen={5000} />
<VariantTest
defaultValues={["1", "3", "5"]}
storyLabel="20 items, 3 selected by default"
listLen={20}
/>
<VariantTest
defaultValues={["1", "2", "3", "4", "5", "6", "7"]}
storyLabel="20 items, 7 selected, show max 3"
listLen={20}
maxCount={3}
/>
</div>
);
}
function VariantTest(props: {
defaultValues: string[];
maxCount?: number;
storyLabel: string;
listLen: number;
}) {
const list = useMemo(() => createList(props.listLen), [props.listLen]);
const [values, setValues] = useState<string[]>(props.defaultValues);
return (
<BadgeContainer label={props.storyLabel}>
<MultiSelect
selectedValues={values}
options={list}
onSelectedValuesChange={(values) => {
setValues(values);
}}
placeholder="Select items"
maxCount={props.maxCount}
/>
</BadgeContainer>
);
}
@@ -0,0 +1,263 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import { CheckIcon, ChevronDown, SearchIcon, XIcon } from "lucide-react";
import * as React from "react";
import { useShowMore } from "../../lib/useShowMore";
import { ScrollShadow } from "../ui/ScrollShadow/ScrollShadow";
import { Input } from "../ui/input";
interface MultiSelectProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
options: {
label: string;
value: string;
}[];
selectedValues: string[];
onSelectedValuesChange: (value: string[]) => void;
placeholder: string;
/**
* Maximum number of items to display. Extra selected items will be summarized.
* Optional, defaults to 3.
*/
maxCount?: number;
className?: string;
}
export const MultiSelect = React.forwardRef<
HTMLButtonElement,
MultiSelectProps
>(
(
{
options,
onSelectedValuesChange,
placeholder,
maxCount = Number.POSITIVE_INFINITY,
className,
selectedValues,
...props
},
ref,
) => {
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const [searchValue, setSearchValue] = React.useState("");
const handleInputKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
setIsPopoverOpen(true);
} else if (event.key === "Backspace" && !event.currentTarget.value) {
const newSelectedValues = [...selectedValues];
newSelectedValues.pop();
onSelectedValuesChange(newSelectedValues);
}
},
[selectedValues, onSelectedValuesChange],
);
const toggleOption = React.useCallback(
(option: string) => {
const newSelectedValues = selectedValues.includes(option)
? selectedValues.filter((value) => value !== option)
: [...selectedValues, option];
onSelectedValuesChange(newSelectedValues);
},
[selectedValues, onSelectedValuesChange],
);
const handleClear = React.useCallback(() => {
onSelectedValuesChange([]);
}, [onSelectedValuesChange]);
const handleTogglePopover = () => {
setIsPopoverOpen((prev) => !prev);
};
const clearExtraOptions = React.useCallback(() => {
const newSelectedValues = selectedValues.slice(0, maxCount);
onSelectedValuesChange(newSelectedValues);
}, [selectedValues, onSelectedValuesChange, maxCount]);
// show 50 initially and then 20 more when reaching the end
const { itemsToShow, lastItemRef } = useShowMore<HTMLButtonElement>(50, 20);
const optionsToShow = React.useMemo(() => {
const searchValLowercase = searchValue.toLowerCase();
const filteredOptions = options.filter((option) => {
return option.label.toLowerCase().includes(searchValLowercase);
});
return filteredOptions.slice(0, itemsToShow);
}, [options, searchValue, itemsToShow]);
return (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
<Button
ref={ref}
{...props}
onClick={handleTogglePopover}
className={cn(
"flex w-full p-3 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit",
className,
)}
>
{selectedValues.length > 0 ? (
<div className="flex justify-between w-full">
{/* badges */}
<div className="flex flex-wrap items-center gap-1.5">
{selectedValues.slice(0, maxCount).map((value) => {
const option = options.find((o) => o.value === value);
return (
<ClosableBadge
key={value}
label={option?.label || ""}
onClose={() => toggleOption(value)}
/>
);
})}
{/* +X more */}
{selectedValues.length > maxCount && (
<ClosableBadge
label={`+ ${selectedValues.length - maxCount} more`}
onClose={clearExtraOptions}
/>
)}
</div>
<div className="flex items-center justify-between gap-2">
{/* Clear All */}
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
{/* biome-ignore lint/a11y/useFocusableInteractive: <explanation> */}
<div
role="button"
onClick={(event) => {
event.stopPropagation();
handleClear();
}}
className="hover:bg-muted p-1 rounded"
>
<XIcon className="h-4 cursor-pointer text-muted-foreground" />
</div>
<Separator
orientation="vertical"
className="flex min-h-6 h-full"
/>
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
</div>
</div>
) : (
<div className="flex items-center justify-between w-full">
<span className="text-sm text-muted-foreground">
{placeholder}
</span>
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
</div>
)}
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
align="center"
sideOffset={10}
onEscapeKeyDown={() => setIsPopoverOpen(false)}
style={{
width: "var(--radix-popover-trigger-width)",
maxHeight: "var(--radix-popover-content-available-height)",
}}
>
<div>
{/* Search */}
<div className="relative">
<Input
placeholder="Search"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
className="pl-10 py-4 !h-auto focus-visible:ring-0 focus-visible:ring-offset-0 border-0 border-b border-border rounded-b-none"
onKeyDown={handleInputKeyDown}
/>
<SearchIcon className="size-4 absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground" />
</div>
<ScrollShadow
scrollableClassName="max-h-[350px] p-1"
className="rounded"
>
{/* List */}
<div>
{optionsToShow.length === 0 && (
<div className="py-10 justify-center flex">
No results found
</div>
)}
{optionsToShow.map((option, i) => {
const isSelected = selectedValues.includes(option.value);
return (
<Button
key={option.value}
role="option"
aria-selected={isSelected}
onClick={() => toggleOption(option.value)}
variant="ghost"
className="cursor-pointer flex gap-3 py-2 px-3 w-full justify-start rounded-sm"
ref={
i === optionsToShow.length - 1 ? lastItemRef : undefined
}
>
<div
className={cn(
"flex size-4 items-center justify-center rounded-sm border border-foreground",
isSelected
? "bg-inverted text-inverted-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className="size-4" />
</div>
<span>{option.label}</span>
</Button>
);
})}
</div>
</ScrollShadow>
</div>
</PopoverContent>
</Popover>
);
},
);
function ClosableBadge(props: {
label: string;
onClose: () => void;
}) {
return (
<span className="rounded-xl py-1 px-2 text-xs flex items-center gap-3 bg-muted border border-border text-foreground">
{props.label}
<XIcon
className="h-3 w-3 cursor-pointer text-muted-foreground"
onClick={(e) => {
e.stopPropagation();
props.onClose();
}}
/>
</span>
);
}
MultiSelect.displayName = "MultiSelect";
@@ -7,8 +7,9 @@ import {
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useClipboard } from "hooks/useClipboard";
import { Check, Copy, ExternalLinkIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { useMemo } from "react";
import { type ThirdwebClient, isAddress } from "thirdweb";
import { ZERO_ADDRESS } from "thirdweb";
import {
@@ -45,7 +46,7 @@ export function WalletAddress(props: {
client: thirdwebClient,
});
const [isCopied, setIsCopied] = useState(false);
const { onCopy, hasCopied } = useClipboard(address, 2000);
if (!isAddress(address)) {
return <span>Invalid Address ({address})</span>;
@@ -56,16 +57,6 @@ export function WalletAddress(props: {
return <span className="font-mono cursor-pointer">{shortenedAddress}</span>;
}
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(address);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000); // Reset after 2 seconds
} catch (err) {
console.error("Failed to copy: ", err);
}
};
return (
<HoverCard>
<HoverCardTrigger asChild>
@@ -102,22 +93,22 @@ export function WalletAddress(props: {
<Button
variant="outline"
size="sm"
onClick={copyToClipboard}
onClick={onCopy}
className="flex items-center gap-2"
>
{isCopied ? (
{hasCopied ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
{isCopied ? "Copied!" : "Copy"}
{hasCopied ? "Copied!" : "Copy"}
</Button>
</div>
<p className="text-sm font-mono bg-muted p-2 rounded text-center">
{lessShortenedAddress}
</p>
<h3 className="text-lg font-semibold">Social Profiles</h3>
{profiles.isLoading ? (
{profiles.isPending ? (
<p className="text-sm text-muted-foreground">Loading profiles...</p>
) : !profiles.data?.length ? (
<p className="text-sm text-muted-foreground">No profiles found</p>
@@ -1,8 +1,8 @@
"use client";
import { cn } from "@/lib/utils";
import { useClipboard } from "hooks/useClipboard";
import { CheckIcon, CopyIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "./button";
import { ToolTipLabel } from "./tooltip";
@@ -12,20 +12,16 @@ export function CopyButton(props: {
iconClassName?: string;
variant?: "ghost" | "primary" | "secondary";
}) {
const [isCopied, setIsCopied] = useState(false);
const { hasCopied, onCopy } = useClipboard(props.text, 1000);
return (
<ToolTipLabel label="Copy">
<Button
variant={props.variant || "ghost"}
aria-label="Copy"
className={cn("h-auto w-auto p-1", props.className)}
onClick={() => {
navigator.clipboard.writeText(props.text);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 1000);
}}
onClick={onCopy}
>
{isCopied ? (
{hasCopied ? (
<CheckIcon
className={cn("size-4 text-green-500", props.iconClassName)}
/>
@@ -1,8 +1,8 @@
"use client";
import { cn } from "@/lib/utils";
import { useClipboard } from "hooks/useClipboard";
import { CheckIcon, CopyIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "./button";
import { ToolTipLabel } from "./tooltip";
@@ -22,8 +22,8 @@ export function CopyTextButton(props: {
copyIconPosition: "left" | "right";
onClick?: () => void;
}) {
const [isCopied, setIsCopied] = useState(false);
const copyButton = isCopied ? (
const { hasCopied, onCopy } = useClipboard(props.textToCopy, 1000);
const copyButton = hasCopied ? (
<CheckIcon
className={cn("size-3 text-green-500 shrink-0", props.iconClassName)}
/>
@@ -46,9 +46,7 @@ export function CopyTextButton(props: {
props.className,
)}
onClick={(e) => {
navigator.clipboard.writeText(props.textToCopy);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 1000);
onCopy();
e.stopPropagation();
props.onClick?.();
}}
@@ -71,8 +71,15 @@ export function ScrollShadow(props: {
});
});
content.addEventListener("scroll", handleScroll);
const resizeObserver = new ResizeObserver(() => {
handleScroll();
});
resizeObserver.observe(content);
return () => {
content.removeEventListener("scroll", handleScroll);
resizeObserver.disconnect();
};
}, []);
@@ -29,7 +29,7 @@ const RadioGroupItem = React.forwardRef<
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"aspect-square h-4 w-4 rounded-full border border-inverted text-inverted ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
@@ -51,12 +51,12 @@ const RadioGroupItemButton = React.forwardRef<
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"cursor-pointer group flex items-center px-3 peer-hover:border-nonce py-4 space-x-3 space-y-0 border-2 rounded-md min-w-32 font-medium hover:border-foreground/25 transition-all data-[state=checked]:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"cursor-pointer group flex items-center px-3 peer-hover:border-nonce py-4 space-x-3 space-y-0 border-2 rounded-md min-w-32 font-medium hover:border-foreground/25 transition-all data-[state=checked]:border-inverted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<div className="flex items-center justify-center w-4 h-4 border-2 rounded-full transition-all group-data-[state=checked]:border-primary text-primary group-hover:border-foreground/25 aspect-square ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50">
<div className="flex items-center justify-center w-4 h-4 border-2 rounded-full transition-all group-data-[state=checked]:border-inverted text-inverted group-hover:border-foreground/25 aspect-square ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50">
{/* Show on checked */}
<RadioGroupPrimitive.Indicator className="flex items-center justify-center transition-all rounded-full">
<Circle className="w-2 h-2 text-current fill-current" />
@@ -1,4 +1,3 @@
import {} from "@/constants/env";
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { useActiveAccount } from "thirdweb/react";
+7 -1
View File
@@ -1,4 +1,6 @@
type Store<T> = {
import { useSyncExternalStore } from "react";
export type Store<T> = {
getValue(): T;
setValue(newValue: T): void;
subscribe(listener: () => void): () => void;
@@ -45,3 +47,7 @@ export function createStore<T>(initialValue: T): Store<T> {
},
};
}
export function useStore<T>(store: Store<T>): T {
return useSyncExternalStore(store.subscribe, store.getValue, store.getValue);
}
+37
View File
@@ -0,0 +1,37 @@
"use client";
import { useCallback, useState } from "react";
/**
*
* @internal
*/
export function useShowMore<T extends HTMLElement>(
initialItemsToShow: number,
itemsToAdd: number,
) {
// start with showing first `initialItemsToShow` items, when the last item is in view, show `itemsToAdd` more
const [itemsToShow, setItemsToShow] = useState(initialItemsToShow);
const lastItemRef = useCallback(
(node: T) => {
if (!node) {
return;
}
const observer = new IntersectionObserver(
(entries) => {
if (entries[0]?.isIntersecting) {
setItemsToShow((prev) => prev + itemsToAdd); // show 10 more items
}
},
{ threshold: 1 },
);
observer.observe(node);
// when the node is removed from the DOM, observer will be disconnected automatically by the browser
},
[itemsToAdd],
);
return { itemsToShow, lastItemRef };
}
+18 -1
View File
@@ -140,7 +140,24 @@
}
}
body,
html {
height: 100%;
}
body {
min-height: 100%;
}
/* Fix colors on autofilled inputs */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
/* Revert text color */
-webkit-text-fill-color: hsl(var(--foreground)) !important;
color: hsl(var(--foreground)) !important;
caret-color: hsl(var(--foreground)) !important;
/* Revert background color */
transition: background-color 5000s ease-in-out 0s;
}
@@ -104,4 +104,11 @@ export const engineKeys = {
[...engineKeys.all, engineId, "systemMetrics"] as const,
queueMetrics: (engineId: string) =>
[...engineKeys.all, engineId, "queueMetrics"] as const,
alertRules: (engineId: string) =>
[...engineKeys.all, engineId, "alertRules"] as const,
alerts: (engineId: string) =>
[...engineKeys.all, engineId, "alerts"] as const,
notificationChannels: (engineId: string) =>
[...engineKeys.all, engineId, "notificationChannels"] as const,
};
@@ -3,27 +3,31 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useStore } from "@/lib/reactive";
import { getSDKTheme } from "app/components/sdk-component-theme";
import { CustomChainRenderer } from "components/selects/CustomChainRenderer";
import { mapV4ChainToV5Chain } from "contexts/map-chains";
import { useTrack } from "hooks/analytics/useTrack";
import {
useSupportedChains,
useSupportedChainsRecord,
} from "hooks/chains/configureChains";
import {
useAddRecentlyUsedChainId,
useRecentlyUsedChains,
} from "hooks/chains/recentlyUsedChains";
import { useSetIsNetworkConfigModalOpen } from "hooks/networkConfigModal";
import { useTheme } from "next-themes";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useCallback, useMemo } from "react";
import { useCallback, useMemo, useState } from "react";
import type { Chain } from "thirdweb";
import { AutoConnect, ConnectButton, useConnectModal } from "thirdweb/react";
import { useFavoriteChains } from "../../hooks/useFavoriteChains";
import {
AutoConnect,
ConnectButton,
type NetworkSelectorProps,
useConnectModal,
} from "thirdweb/react";
import { useFavoriteChainIds } from "../../../../app/(dashboard)/(chain)/components/client/star-button";
import { LazyConfigureNetworkModal } from "../../../../components/configure-networks/LazyConfigureNetworkModal";
import { useAllChainsData } from "../../../../hooks/chains/allChains";
import {
type StoredChain,
addRecentlyUsedChainId,
recentlyUsedChainIdsStore,
} from "../../../../stores/chainStores";
import { useLoggedInUser } from "../../hooks/useLoggedInUser";
import { popularChains } from "../popularChains";
@@ -35,51 +39,93 @@ export const CustomConnectWallet = (props: {
const thirdwebClient = useThirdwebClient();
const loginRequired =
props.loginRequired === undefined ? true : props.loginRequired;
const { theme } = useTheme();
const recentChainsv4 = useRecentlyUsedChains();
const addRecentlyUsedChainId = useAddRecentlyUsedChainId();
// const setIsNetworkConfigModalOpen = useSetIsNetworkConfigModalOpen();
const t = theme === "light" ? "light" : "dark";
const allv4Chains = useSupportedChains();
const chainsRecord = useSupportedChainsRecord();
const favChainsQuery = useFavoriteChains();
const setIsNetworkConfigModalOpen = useSetIsNetworkConfigModalOpen();
const allChains = useMemo(() => {
return allv4Chains.map(mapV4ChainToV5Chain);
}, [allv4Chains]);
const popularChainsWithMeta = useMemo(() => {
return popularChains.map((x) => chainsRecord[x.id] || x);
}, [chainsRecord]);
// chains
const favChainIdsQuery = useFavoriteChainIds();
const recentChainIds = useStore(recentlyUsedChainIdsStore);
const { idToChain, allChains } = useAllChainsData();
const chainSections = useMemo(() => {
const allChainsWithMetadata = useMemo(
() => allChains.map(mapV4ChainToV5Chain),
[allChains],
);
const recentlyUsedChainsWithMetadata = useMemo(
() =>
recentChainIds
.map((id) => {
const c = idToChain.get(id);
// eslint-disable-next-line no-restricted-syntax
return c ? mapV4ChainToV5Chain(c) : undefined;
})
.filter((x) => !!x),
[recentChainIds, idToChain],
);
const favoriteChainsWithMetadata = useMemo(() => {
return (favChainIdsQuery.data || [])
.map((id) => {
const c = idToChain.get(Number(id));
// eslint-disable-next-line no-restricted-syntax
return c ? mapV4ChainToV5Chain(c) : undefined;
})
.filter((x) => !!x);
}, [idToChain, favChainIdsQuery.data]);
const popularChainsWithMetadata = useMemo(() => {
// eslint-disable-next-line no-restricted-syntax
return popularChains.map((x) =>
// eslint-disable-next-line no-restricted-syntax
{
const v4Chain = idToChain.get(x.id);
// eslint-disable-next-line no-restricted-syntax
return v4Chain ? mapV4ChainToV5Chain(v4Chain) : x;
},
);
}, [idToChain]);
// Network Config Modal
const [isNetworkConfigModalOpen, setIsNetworkConfigModalOpen] =
useState(false);
const [editChain, setEditChain] = useState<StoredChain | undefined>(
undefined,
);
const chainSections: NetworkSelectorProps["sections"] = useMemo(() => {
return [
{
label: "Favorites",
chains: favChainsQuery.data.map(mapV4ChainToV5Chain),
},
{
label: "Popular",
chains: popularChainsWithMeta.map(mapV4ChainToV5Chain),
chains: favoriteChainsWithMetadata,
},
{
label: "Recent",
chains: recentChainsv4.map(mapV4ChainToV5Chain),
chains: recentlyUsedChainsWithMetadata,
},
{
label: "Popular",
chains: popularChainsWithMetadata,
},
];
}, [recentChainsv4, favChainsQuery.data, popularChainsWithMeta]);
}, [
recentlyUsedChainsWithMetadata,
favoriteChainsWithMetadata,
popularChainsWithMetadata,
]);
// ensures login status on pages that need it
const { isLoading, isLoggedIn } = useLoggedInUser();
const { isPending, isLoggedIn } = useLoggedInUser();
const pathname = usePathname();
if (isLoading) {
if (isPending) {
return (
<>
<div className="w-[144px] h-[48px] bg-muted border border-border rounded-lg flex items-center justify-center">
<Spinner className="size-4" />
</div>
{/* need autoconnect here so that we actually connect */}
{/* need auto connect here so that we actually connect */}
<AutoConnect client={thirdwebClient} />
</>
);
@@ -95,57 +141,76 @@ export const CustomConnectWallet = (props: {
Sign In
</Link>
</Button>
{/* need autoconnect here so that we actually connect */}
{/* need auto connect here so that we actually connect */}
<AutoConnect client={thirdwebClient} />
</>
);
}
return (
<ConnectButton
theme={getSDKTheme(t)}
client={thirdwebClient}
connectModal={{
privacyPolicyUrl: "/privacy",
termsOfServiceUrl: "/tos",
showThirdwebBranding: false,
welcomeScreen: () => <ConnectWalletWelcomeScreen theme={t} />,
}}
appMetadata={{
name: "thirdweb",
logoUrl: "https://thirdweb.com/favicon.ico",
url: "https://thirdweb.com",
}}
onDisconnect={async () => {
try {
// log out the user
await fetch("/api/auth/logout", {
method: "POST",
});
} catch (err) {
console.error("Failed to log out", err);
}
}}
connectButton={{
className: props.connectButtonClassName,
}}
detailsButton={{
className: props.detailsButtonClassName,
}}
chains={allChains}
detailsModal={{
networkSelector: {
sections: chainSections,
onSwitch(chain) {
addRecentlyUsedChainId(chain.id);
<>
<ConnectButton
theme={getSDKTheme(t)}
client={thirdwebClient}
connectModal={{
privacyPolicyUrl: "/privacy",
termsOfServiceUrl: "/tos",
showThirdwebBranding: false,
welcomeScreen: () => <ConnectWalletWelcomeScreen theme={t} />,
}}
appMetadata={{
name: "thirdweb",
logoUrl: "https://thirdweb.com/favicon.ico",
url: "https://thirdweb.com",
}}
onDisconnect={async () => {
try {
// log out the user
await fetch("/api/auth/logout", {
method: "POST",
});
} catch (err) {
console.error("Failed to log out", err);
}
}}
connectButton={{
className: props.connectButtonClassName,
}}
detailsButton={{
className: props.detailsButtonClassName,
}}
chains={allChainsWithMetadata}
detailsModal={{
networkSelector: {
sections: chainSections,
onSwitch(chain) {
addRecentlyUsedChainId(chain.id);
},
renderChain(props) {
return (
<CustomChainRenderer
{...props}
openEditChainModal={(c) => {
setIsNetworkConfigModalOpen(true);
setEditChain(c);
}}
/>
);
},
onCustomClick: () => {
setEditChain(undefined);
setIsNetworkConfigModalOpen(true);
},
},
renderChain: CustomChainRenderer,
onCustomClick: () => {
setIsNetworkConfigModalOpen(true);
},
},
}}
/>
}}
/>
<LazyConfigureNetworkModal
open={isNetworkConfigModalOpen}
onOpenChange={setIsNetworkConfigModalOpen}
editChain={editChain}
/>
</>
);
};
@@ -17,10 +17,14 @@ export type EVMContractInfo = {
type SetEVMContractInfo = (info: EVMContractInfo) => void;
// Legacy: will be removed with contract page -> app router refactor
// eslint-disable-next-line no-restricted-syntax
const EVMContractInfoContext = createContext<EVMContractInfo | undefined>(
undefined,
);
// Legacy: will be removed with contract page -> app router refactor
// eslint-disable-next-line no-restricted-syntax
const SetEVMContractInfoContext = createContext<SetEVMContractInfo | undefined>(
undefined,
);
@@ -5,7 +5,6 @@ import {
useQueryClient,
} from "@tanstack/react-query";
import { THIRDWEB_ANALYTICS_API_HOST, THIRDWEB_API_HOST } from "constants/urls";
import type { ChainMetadata } from "thirdweb/chains";
import invariant from "tiny-invariant";
import { accountKeys, apiKeys, authorizedWallets } from "../cache-keys";
import { useMutationWithInvalidate } from "./query/useQueryWithNetwork";
@@ -947,28 +946,3 @@ export function useAuthorizedWallets() {
gcTime: 0,
});
}
/**
*
*/
async function fetchChainsFromApi() {
// always fetch from prod for chains for now
// TODO: re-visit this
const res = await fetch("https://api.thirdweb.com/v1/chains");
const json = await res.json();
if (json.error) {
throw new Error(json.error.message);
}
return json.data as ChainMetadata[];
}
export function useApiChains() {
return useQuery({
queryKey: ["all-chains"],
queryFn: () => {
return fetchChainsFromApi();
},
});
}
@@ -1539,3 +1539,177 @@ export function useEngineSystemMetrics(engineId: string) {
refetchInterval: 5_000,
});
}
export interface EngineAlertRule {
id: string;
title: string;
// A unique string identifying the alert.
// Note the "." which allows `subscriptionRoutes` to use wildcards in notificationChannel.
// Example: "alert.sla-5xx-99"
routingKey: string;
description: string;
createdAt: Date;
pausedAt: Date | null;
}
export function useEngineAlertRules(engineId: string) {
const { isLoggedIn } = useLoggedInUser();
return useQuery({
queryKey: engineKeys.alertRules(engineId),
queryFn: async () => {
const res = await fetch(
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/alert-rules`,
{ method: "GET" },
);
if (!res.ok) {
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
}
const json = await res.json();
return json.data as EngineAlertRule[];
},
enabled: isLoggedIn,
});
}
export interface EngineAlert {
id: string;
alertRuleId: string;
status: "pending" | "firing" | "resolved";
startsAt: Date;
endsAt: Date | null;
}
export function useEngineAlerts(engineId: string, limit: number, offset = 0) {
const { isLoggedIn } = useLoggedInUser();
return useQuery({
queryKey: engineKeys.alerts(engineId),
queryFn: async () => {
const res = await fetch(
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/alerts?limit=${limit}&offset=${offset}`,
{ method: "GET" },
);
if (!res.ok) {
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
}
const json = await res.json();
return json.data as EngineAlert[];
},
enabled: isLoggedIn,
});
}
export const EngineNotificationChannelTypeConfig = {
slack: {
display: "Slack",
valueDisplay: "Slack Webhook URL",
},
email: {
display: "Email",
valueDisplay: "Email Address",
},
} as const;
type EngineNotificationChannelType =
keyof typeof EngineNotificationChannelTypeConfig;
export type EngineNotificationChannel = {
id: string;
type: EngineNotificationChannelType;
value: string;
createdAt: Date;
pausedAt: Date | null;
// A list of routingKeys to listen to. Supports wildcards.
// Example: [ 'alert.sla-5xx-99' ] or [ 'alert.*' ] will both notify when
// the alert with routingKey `alert.sla-5xx-99` triggers.
subscriptionRoutes: string[];
};
export function useEngineNotificationChannels(engineId: string) {
const { isLoggedIn } = useLoggedInUser();
return useQuery({
queryKey: engineKeys.notificationChannels(engineId),
queryFn: async () => {
const res = await fetch(
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels`,
{ method: "GET" },
);
if (!res.ok) {
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
}
const json = await res.json();
return json.data as EngineNotificationChannel[];
},
enabled: isLoggedIn,
});
}
export interface CreateNotificationChannelInput {
subscriptionRoutes: string[];
type: "slack" | "email"; // TODO: Add others when implemented.
value: string;
}
export function useEngineCreateNotificationChannel(engineId: string) {
const { isLoggedIn } = useLoggedInUser();
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: CreateNotificationChannelInput) => {
invariant(isLoggedIn, "Must be logged in.");
const res = await fetch(
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels`,
{
method: "POST",
body: JSON.stringify(input),
},
);
if (!res.ok) {
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
}
const json = await res.json();
return json.data as EngineNotificationChannel;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.notificationChannels(engineId),
});
},
},
);
}
export function useEngineDeleteNotificationChannel(engineId: string) {
const { isLoggedIn } = useLoggedInUser();
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (notificationChannelId: string) => {
invariant(isLoggedIn, "Must be logged in.");
const res = await fetch(
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels/${notificationChannelId}`,
{ method: "DELETE" },
);
if (!res.ok) {
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
}
res.body?.cancel();
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.notificationChannels(engineId),
});
},
},
);
}
@@ -1,31 +0,0 @@
import { useFavouriteChainIds } from "app/(dashboard)/(chain)/components/client/star-button";
import { useSupportedChains } from "hooks/chains/configureChains";
import { useMemo } from "react";
import type { ChainMetadata } from "thirdweb/chains";
export function useFavoriteChains() {
const allChains = useSupportedChains();
const favChainsQuery = useFavouriteChainIds();
const data = useMemo(() => {
if (favChainsQuery.data) {
const _chains: ChainMetadata[] = [];
// biome-ignore lint/complexity/noForEach: FIXME
favChainsQuery.data.forEach((chainId) => {
const chain = allChains.find((c) => String(c.chainId) === chainId);
if (chain) {
_chains.push(chain);
}
});
return _chains;
}
return [] as ChainMetadata[];
}, [favChainsQuery.data, allChains]);
return {
isLoading: favChainsQuery.isLoading,
data,
};
}
@@ -17,7 +17,7 @@ declare global {
}
export function useLoggedInUser(): {
isLoading: boolean;
isPending: boolean;
isLoggedIn: boolean;
user: { address: string; jwt?: string } | null;
} {
@@ -82,7 +82,7 @@ export function useLoggedInUser(): {
// if we are "disconnected" we are not logged in
if (connectionStatus === "disconnected") {
return {
isLoading: false,
isPending: false,
isLoggedIn: false,
user: null,
};
@@ -91,7 +91,7 @@ export function useLoggedInUser(): {
// if we are still connecting, we are "loading"
if (connectionStatus === "connecting") {
return {
isLoading: true,
isPending: true,
isLoggedIn: false,
user: null,
};
@@ -100,7 +100,7 @@ export function useLoggedInUser(): {
// same if we do not yet have a path
if (!pathname) {
return {
isLoading: true,
isPending: true,
isLoggedIn: false,
user: null,
};
@@ -109,7 +109,7 @@ export function useLoggedInUser(): {
// if we do not have an address we are not logged in
if (!connectedAddress) {
return {
isLoading: false,
isPending: false,
isLoggedIn: false,
user: null,
};
@@ -118,7 +118,7 @@ export function useLoggedInUser(): {
// if we are not on a logged in path, we can simply return the connected address
if (!isLoginRequired(pathname)) {
return {
isLoading: false,
isPending: false,
isLoggedIn: true,
user: { address: connectedAddress },
};
@@ -126,7 +126,7 @@ export function useLoggedInUser(): {
// otherwise we return the query data
return {
isLoading: query.isLoading,
isPending: query.isPending,
isLoggedIn: query.data ? query.data.isLoggedIn : false,
user: query.data?.isLoggedIn
? { address: connectedAddress, jwt: query.data.jwt }
@@ -8,7 +8,6 @@ import {
import type { BasicContract } from "contract-ui/types/types";
import { getAllMultichainRegistry } from "dashboard-extensions/common/read/getAllMultichainRegistry";
import { useAllChainsData } from "hooks/chains/allChains";
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
import { useMemo } from "react";
import { sendAndConfirmTransaction } from "thirdweb";
import { remove } from "thirdweb/extensions/thirdweb";
@@ -42,7 +41,8 @@ export const useAllContractList = (
) => {
const multiChainQuery = useMultiChainRegContractList(walletAddress);
const configuredChainsRecord = useSupportedChainsRecord();
// TODO - instead of using ALL chains, fetch only the ones used here
const { idToChain } = useAllChainsData();
const contractList = useMemo(() => {
const data = multiChainQuery.data || [];
@@ -51,14 +51,12 @@ export const useAllContractList = (
// biome-ignore lint/complexity/noForEach: FIXME
data.forEach((net) => {
if (net.chainId in configuredChainsRecord) {
const chainRecord = configuredChainsRecord[net.chainId];
if (chainRecord.status !== "deprecated") {
if (chainRecord.testnet) {
testnets.push(net);
} else {
mainnets.push(net);
}
const chn = idToChain.get(net.chainId);
if (chn && chn.status !== "deprecated") {
if (chn.testnet) {
testnets.push(net);
} else {
mainnets.push(net);
}
}
});
@@ -71,7 +69,7 @@ export const useAllContractList = (
testnets.sort((a, b) => a.chainId - b.chainId);
return mainnets.concat(testnets);
}, [multiChainQuery.data, onlyMainnet, configuredChainsRecord]);
}, [multiChainQuery.data, onlyMainnet, idToChain]);
return {
...multiChainQuery,
@@ -85,12 +83,10 @@ type RemoveContractParams = {
};
export function useRemoveContractMutation() {
const { chainIdToChainRecord } = useAllChainsData();
const queryClient = useQueryClient();
const account = useActiveAccount();
return useMutation({
mutationFn: async (data: RemoveContractParams) => {
invariant(chainIdToChainRecord, "chains not initialzed yet");
invariant(data.chainId, "chainId not provided");
invariant(account, "No wallet connected");
const { contractAddress, chainId } = data;
@@ -118,7 +118,7 @@ export function FaucetButton({
faucetWalletBalanceQuery.data.value < toUnits("1", 17);
// loading state
if (faucetWalletBalanceQuery.isLoading || canClaimFaucetQuery.isLoading) {
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
return (
<Button variant="outline" className="w-full gap-2">
Checking Faucet <Spinner className="size-3" />
@@ -42,7 +42,7 @@ async function removeChainFromFavorites(chainId: number) {
return result?.data?.favorite;
}
export function useFavouriteChainIds() {
export function useFavoriteChainIds() {
const loggedInUser = useLoggedInUser();
return useQuery({
queryKey: ["favoriteChains", loggedInUser.user?.address],
@@ -59,7 +59,7 @@ export function StarButton(props: {
}) {
const loggedInUser = useLoggedInUser();
const queryClient = useQueryClient();
const favChainsQuery = useFavouriteChainIds();
const favChainsQuery = useFavoriteChainIds();
const mutation = useMutation({
mutationFn: (preferred: boolean) => {
@@ -92,7 +92,7 @@ export function StarButton(props: {
disabled={
!loggedInUser.user?.address ||
mutation.isPending ||
favChainsQuery.isLoading
favChainsQuery.isPending
}
>
<ToolTipLabel label={label}>
@@ -0,0 +1,41 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import type { Ecosystem } from "../../../../types";
import { AddPartnerForm } from "./add-partner-form.client";
export function AddPartnerDialogButton(props: {
ecosystem: Ecosystem;
}) {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="gap-2 max-sm:w-full">
<PlusIcon className="size-4" />
Add Partner
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="mb-2">
<DialogTitle className="text-2xl font-semibold tracking-tight">
Add Partner
</DialogTitle>
</DialogHeader>
<AddPartnerForm
ecosystem={props.ecosystem}
onPartnerAdded={() => setOpen(false)}
/>
</DialogContent>
</Dialog>
);
}
@@ -1,4 +1,5 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import {
Form,
@@ -6,17 +7,10 @@ import {
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
@@ -26,14 +20,17 @@ import type { Ecosystem } from "../../../../types";
import { partnerFormSchema } from "../../constants";
import { useAddPartner } from "../../hooks/use-add-partner";
export function AddPartnerForm({ ecosystem }: { ecosystem: Ecosystem }) {
export function AddPartnerForm({
ecosystem,
onPartnerAdded,
}: { ecosystem: Ecosystem; onPartnerAdded: () => void }) {
const form = useForm<z.input<typeof partnerFormSchema>>({
resolver: zodResolver(partnerFormSchema),
});
const { mutateAsync: addPartner, isPending } = useAddPartner({
onSuccess: () => {
form.reset();
onPartnerAdded();
},
onError: (error) => {
const message =
@@ -57,18 +54,18 @@ export function AddPartnerForm({ ecosystem }: { ecosystem: Ecosystem }) {
allowlistedBundleIds: values.bundleIds
.split(/,| /)
.filter((d) => d.length > 0),
permissions: [values.permissions],
});
})}
className="flex flex-col gap-2 lg:flex-row"
className="flex flex-col gap-6"
>
<div className="grid gap-2 lg:grid-cols-12 grow">
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="name"
defaultValue="" // Note: you *must* provide a default value here or the field won't reset
render={({ field }) => (
<FormItem className="col-span-4 lg:col-span-3">
<FormItem>
<FormLabel> App Name </FormLabel>
<FormControl>
<Input
placeholder="App name"
@@ -94,22 +91,16 @@ export function AddPartnerForm({ ecosystem }: { ecosystem: Ecosystem }) {
name="domains"
defaultValue="" // Note: you *must* provide a default value here or the field won't reset
render={({ field }) => (
<FormItem className="col-span-4 lg:col-span-4">
<FormItem>
<FormLabel> Domains </FormLabel>
<FormControl>
<>
<Input placeholder="Domains" className="peer" {...field} />
<FormDescription
className={cn(
"hidden text-xs transition-all lg:block lg:-translate-y-4 lg:opacity-0 peer-focus-visible:opacity-100 peer-focus-visible:translate-y-0",
form.formState.errors.domains?.message &&
"text-destructive lg:translate-y-0 lg:opacity-100 block", // If there are errors show them rather than the tip
)}
>
{form.formState.errors.domains?.message ??
"Space or comma-separated list of regex domains (e.g. *.example.com)"}
</FormDescription>
</>
<Input placeholder="Domains" className="peer" {...field} />
</FormControl>
<FormDescription>
Space or comma-separated list of regex domains (e.g.
*.example.com)
</FormDescription>
</FormItem>
)}
/>
@@ -118,75 +109,24 @@ export function AddPartnerForm({ ecosystem }: { ecosystem: Ecosystem }) {
name="bundleIds"
defaultValue="" // Note: you *must* provide a default value here or the field won't reset
render={({ field }) => (
<FormItem className="col-span-4 lg:col-span-3">
<FormItem>
<FormLabel> Bundle ID </FormLabel>
<FormControl>
<>
<Input
placeholder="Bundle ID"
className="peer"
{...field}
/>
<FormDescription
className={cn(
"hidden text-xs transition-all lg:block lg:-translate-y-4 lg:opacity-0 peer-focus-visible:opacity-100 peer-focus-visible:translate-y-0",
form.formState.errors.bundleIds?.message &&
"text-destructive translate-y-0 opacity-100 block",
)}
>
{form.formState.errors.bundleIds?.message ??
"Space or comma-separated list of bundle IDs"}
</FormDescription>
</>
<Input placeholder="Bundle ID" className="peer" {...field} />
</FormControl>
<FormDescription>
Space or comma-separated list of bundle IDs
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="permissions"
defaultValue="PROMPT_USER_V1" // Note: you *must* provide a default value here or the field won't reset
render={({ field }) => (
<FormItem className="col-span-4 lg:col-span-2">
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<ToolTipLabel label="Should wallet actions prompt the user for approval?">
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="Wallet prompts" />
</SelectTrigger>
</FormControl>
</ToolTipLabel>
<SelectContent>
<SelectItem value="FULL_CONTROL_V1">
Never prompt
</SelectItem>
<SelectItem value="PROMPT_USER_V1">Prompt user</SelectItem>
</SelectContent>
<FormDescription
className={cn(
"hidden text-xs transition-all lg:block lg:-translate-y-4 lg:opacity-0 peer-focus-visible:opacity-100 peer-focus-visible:translate-y-0",
form.formState.errors.permissions?.message &&
"text-destructive lg:translate-y-0 lg:opacity-100 block", // If there are errors show them rather than the tip
)}
>
{form.formState.errors.permissions?.message ??
"Wallet signing"}
</FormDescription>
</Select>
</FormItem>
)}
/>
</div>
<Button
disabled={isPending}
type="submit"
variant="outline"
className="w-full lg:w-auto"
>
<Button disabled={isPending} type="submit" className="w-full gap-2">
{isPending && <Spinner className="size-4" />}
Add
</Button>
</form>
@@ -64,9 +64,9 @@ function EcosystemSelect(props: {
ecosystem: Ecosystem;
ecosystemLayoutPath: string;
}) {
const { data: ecosystems, isLoading } = useEcosystemList();
const { data: ecosystems, isPending } = useEcosystemList();
return isLoading ? (
return isPending ? (
<Skeleton className="h-10 w-full md:w-[160px]" />
) : (
<DropdownMenu>
@@ -1,4 +1,5 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import {
Form,
@@ -6,20 +7,12 @@ import {
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2 } from "lucide-react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type { z } from "zod";
@@ -38,7 +31,6 @@ export function UpdatePartnerForm({
name: partner.name,
domains: partner.allowlistedDomains.join(","),
bundleIds: partner.allowlistedBundleIds.join(","),
permissions: partner.permissions[0],
},
});
@@ -70,18 +62,18 @@ export function UpdatePartnerForm({
allowlistedBundleIds: values.bundleIds
.split(/,| /)
.filter((d) => d.length > 0),
permissions: [values.permissions],
});
})}
className="flex flex-col gap-4"
>
<div className="grid gap-4 grow">
<div className="flex flex-col gap-5 grow">
<FormField
control={form.control}
name="name"
defaultValue="" // Note: you *must* provide a default value here or the field won't reset
render={({ field }) => (
<FormItem className="col-span-4">
<FormLabel> Name </FormLabel>
<FormControl>
<Input
placeholder="App name"
@@ -108,21 +100,20 @@ export function UpdatePartnerForm({
defaultValue="" // Note: you *must* provide a default value here or the field won't reset
render={({ field }) => (
<FormItem className="col-span-4 lg:col-span-4">
<FormLabel> Domains </FormLabel>
<FormControl>
<>
<Input placeholder="Domains" className="peer" {...field} />
<FormDescription
className={cn(
"text-xs block",
form.formState.errors.domains?.message &&
"text-destructive translate-y-0 opacity-100 block", // If there are errors show them rather than the tip
)}
>
{form.formState.errors.domains?.message ??
"Space or comma-separated list of regex domains (e.g. *.example.com)"}
</FormDescription>
</>
<Input placeholder="Domains" className="peer" {...field} />
</FormControl>
<FormDescription
className={cn(
"text-xs block",
form.formState.errors.domains?.message &&
"text-destructive translate-y-0 opacity-100 block", // If there are errors show them rather than the tip
)}
>
{form.formState.errors.domains?.message ??
"Space or comma-separated list of regex domains (e.g. *.example.com)"}
</FormDescription>
</FormItem>
)}
/>
@@ -132,75 +123,32 @@ export function UpdatePartnerForm({
defaultValue="" // Note: you *must* provide a default value here or the field won't reset
render={({ field }) => (
<FormItem className="col-span-4">
<FormLabel> Bundle ID </FormLabel>
<FormControl>
<>
<Input
placeholder="Bundle ID"
className="peer"
{...field}
/>
<FormDescription
className={cn(
"text-xs block",
form.formState.errors.bundleIds?.message &&
"text-destructive block",
)}
>
{form.formState.errors.bundleIds?.message ??
"Space or comma-separated list of bundle IDs"}
</FormDescription>
</>
<Input placeholder="Bundle ID" className="peer" {...field} />
</FormControl>
<FormDescription
className={cn(
"text-xs block",
form.formState.errors.bundleIds?.message &&
"text-destructive block",
)}
>
{form.formState.errors.bundleIds?.message ??
"Space or comma-separated list of bundle IDs"}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="permissions"
defaultValue="PROMPT_USER_V1" // Note: you *must* provide a default value here or the field won't reset
render={({ field }) => (
<FormItem className="col-span-4">
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<ToolTipLabel label="Should wallet actions prompt the user for approval?">
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="Wallet prompts" />
</SelectTrigger>
</FormControl>
</ToolTipLabel>
<SelectContent>
<SelectItem value="FULL_CONTROL_V1">
Never prompt
</SelectItem>
<SelectItem value="PROMPT_USER_V1">Prompt user</SelectItem>
</SelectContent>
<FormDescription
className={cn(
"hidden text-xs transition-all peer-focus-visible:opacity-100 peer-focus-visible:translate-y-0",
form.formState.errors.permissions?.message &&
"text-destructive block", // If there are errors show them rather than the tip
)}
>
{form.formState.errors.permissions?.message ??
"Wallet signing"}
</FormDescription>
</Select>
</FormItem>
)}
/>
</div>
<Button
disabled={isPending}
type="submit"
variant="outline"
className="w-full"
className="w-full gap-2 mt-4"
>
{isPending && <Loader2 className="size-4 mr-1 animate-spin" />}
{isPending && <Spinner className="size-4" />}
Update
</Button>
</form>
@@ -20,11 +20,8 @@ export function UpdatePartnerModal({
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger>{children}</DialogTrigger>
<DialogContent
className="w-[90vw] sm:w-auto z-[10001]"
dialogOverlayClassName="z-[10000]"
>
<DialogHeader>
<DialogContent className="z-[10001]" dialogOverlayClassName="z-[10000]">
<DialogHeader className="mb-2">
<DialogTitle>Update {partner.name}</DialogTitle>
</DialogHeader>
<UpdatePartnerForm
@@ -1,24 +1,28 @@
import type { Ecosystem } from "../../../../types";
import { AddPartnerForm } from "../client/add-partner-form.client";
import { AddPartnerDialogButton } from "../client/AddPartnerDialogButton";
import { PartnersTable } from "./partners-table";
export function EcosystemPartnersSection({
ecosystem,
}: { ecosystem: Ecosystem }) {
return (
<section className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<h4 className="text-2xl font-semibold text-foreground">
Ecosystem partners
</h4>
<p className="text-sm text-muted-foreground">
Configure apps that can use your wallet. Creating a partner will
generate a unique partner ID that can access your ecosystem. You will
need to generate at least one partner ID for use in your own app.
</p>
<section className="flex flex-col gap-8 lg:gap-6">
<div className="flex lg:justify-between gap-6 lg:items-start flex-col lg:flex-row">
<div className="flex flex-col gap-1">
<h4 className="text-2xl font-semibold text-foreground">
Ecosystem partners
</h4>
<p className="text-sm text-muted-foreground leading-normal">
Configure apps that can use your wallet. Creating a partner will
generate a unique partner ID that can access your ecosystem. <br />{" "}
You will need to generate at least one partner ID for use in your
own app.
</p>
</div>
<AddPartnerDialogButton ecosystem={ecosystem} />
</div>
<AddPartnerForm ecosystem={ecosystem} />
<PartnersTable ecosystem={ecosystem} />
</section>
);
@@ -22,9 +22,9 @@ import { usePartners } from "../../hooks/use-partners";
import { UpdatePartnerModal } from "../client/update-partner-modal.client";
export function PartnersTable({ ecosystem }: { ecosystem: Ecosystem }) {
const { partners, isLoading } = usePartners({ ecosystem });
const { partners, isPending } = usePartners({ ecosystem });
if (isLoading) {
if (isPending) {
return (
<div className="flex flex-col gap-2">
{Array.from({ length: 3 }).map((_, i) => (
@@ -44,16 +44,14 @@ export function PartnersTable({ ecosystem }: { ecosystem: Ecosystem }) {
<TableHead className="hidden md:table-cell">Domains</TableHead>
<TableHead className="hidden md:table-cell">Bundle ID</TableHead>
<TableHead className="hidden sm:table-cell">Partner ID</TableHead>
<TableHead className="hidden lg:table-cell">
Wallet Prompts
</TableHead>
{/* Empty space for delete button */}
<th className="table-cell" />
</TableRow>
</TableHeader>
<TableBody>
{[...partners].reverse().map((partner: Partner) => (
<EcosystemRow
<PartnerRow
key={partner.id}
partner={partner}
ecosystem={ecosystem}
@@ -65,7 +63,7 @@ export function PartnersTable({ ecosystem }: { ecosystem: Ecosystem }) {
);
}
function EcosystemRow(props: {
function PartnerRow(props: {
partner: Partner;
ecosystem: Ecosystem;
}) {
@@ -108,13 +106,9 @@ function EcosystemRow(props: {
</div>
</ToolTipLabel>
</TableCell>
<TableCell className="hidden align-top lg:table-cell">
{props.partner.permissions.includes("PROMPT_USER_V1")
? "Prompt user"
: "Never prompt"}
</TableCell>
<td className="table-cell py-1 align-middle">
<div className="flex gap-1.5 justify-end">
<div className="flex gap-1.5 pr-1.5 justify-end">
<UpdatePartnerModal
partner={props.partner}
ecosystem={props.ecosystem}
@@ -1,6 +1,4 @@
import z from "zod";
import type { PartnerPermission } from "../../types";
import { isValidPermission } from "./utils";
const isDomainRegex =
/^(?:\*|(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*(?:\*(?:\.[a-z0-9][a-z0-9-]{0,61}[a-z0-9](?:\.[a-z0-9][a-z0-9-]{0,61}[a-z0-9])*)?)|(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]|localhost(?::\d{1,5})?)$/;
@@ -32,10 +30,4 @@ export const partnerFormSchema = z.object({
.filter((d) => d.length > 0)
.join(","),
),
permissions: z.custom<PartnerPermission>(
(value) => isValidPermission(value),
{
message: "Invalid permissions setting",
},
),
});
@@ -11,7 +11,6 @@ type AddPartnerParams = {
name: string;
allowlistedDomains: string[];
allowlistedBundleIds: string[];
permissions: ["PROMPT_USER_V1" | "FULL_CONTROL_V1"];
};
export function useAddPartner(
@@ -44,7 +43,8 @@ export function useAddPartner(
name: params.name,
allowlistedDomains: params.allowlistedDomains,
allowlistedBundleIds: params.allowlistedBundleIds,
permissions: params.permissions,
// TODO - remove the requirement for permissions in API endpoint
permissions: ["FULL_CONTROL_V1"],
}),
},
);
@@ -29,7 +29,7 @@ export function usePartners({ ecosystem }: { ecosystem: Ecosystem }) {
});
return {
isLoading: user?.jwt ? partnersQuery.isLoading : false,
isPending: user?.jwt ? partnersQuery.isPending : false,
partners: (partnersQuery.data ?? []) satisfies Partner[],
};
}
@@ -12,7 +12,6 @@ type UpdatePartnerParams = {
name: string;
allowlistedDomains: string[];
allowlistedBundleIds: string[];
permissions: ["PROMPT_USER_V1" | "FULL_CONTROL_V1"];
};
export function useUpdatePartner(
@@ -45,7 +44,6 @@ export function useUpdatePartner(
name: params.name,
allowlistedDomains: params.allowlistedDomains,
allowlistedBundleIds: params.allowlistedBundleIds,
permissions: params.permissions,
}),
},
);
@@ -1,6 +0,0 @@
import type { PartnerPermission } from "../../types.ts";
export const isValidPermission = (
permission: string,
): permission is PartnerPermission =>
["FULL_CONTROL_V1", "PROMPT_USER_V1"].includes(permission);
@@ -26,7 +26,7 @@ export function useEcosystemList() {
return {
...ecosystemQuery,
isLoading: isLoggedIn ? ecosystemQuery.isLoading : false,
isPending: isLoggedIn ? ecosystemQuery.isPending : false,
ecosystems: (ecosystemQuery.data ?? []) satisfies Ecosystem[],
};
}
@@ -11,7 +11,7 @@ export type Ecosystem = {
updatedAt: string;
};
export type PartnerPermission = "PROMPT_USER_V1" | "FULL_CONTROL_V1";
type PartnerPermission = "PROMPT_USER_V1" | "FULL_CONTROL_V1";
export type Partner = {
id: string;
name: string;
@@ -78,7 +78,7 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) {
},
});
if (webhooksQuery.isLoading) {
if (webhooksQuery.isPending) {
return <Spinner className="size-8 mx-auto" />;
}
@@ -5,7 +5,7 @@ import { ErrorProvider } from "../../contexts/error-handler";
export default function DashboardLayout(props: { children: React.ReactNode }) {
return (
<ErrorProvider>
<div className="flex flex-col h-full bg-background">
<div className="flex flex-col min-h-screen bg-background">
<DashboardHeader />
<main className="grow">{props.children}</main>
<AppFooter />
@@ -50,7 +50,8 @@ export const TransactionSimulator = (props: {
}) => {
const activeAccount = useActiveAccount();
const initialFormValues = props.searchParams;
const [isLoading, setIsLoading] = useState(false);
// TODO - replace this with a mutation.isPending
const [isPending, setIsPending] = useState(false);
const [state, setState] = useState<State>({
success: false,
message: "",
@@ -66,7 +67,7 @@ export const TransactionSimulator = (props: {
async function handleSimulation(data: SimulateTransactionForm) {
try {
setIsLoading(true);
setIsPending(true);
const { from, to, value, functionArgs, functionName } = data;
if (!chain) {
throw new Error("Invalid chainId");
@@ -154,7 +155,7 @@ ${Object.keys(populatedTransaction)
shareUrl: "",
});
}
setIsLoading(false);
setIsPending(false);
}
return (
<div className="max-w-[800px] space-y-4">
@@ -259,7 +260,7 @@ ${Object.keys(populatedTransaction)
</div>
<Button type="submit">
{isLoading ? (
{isPending ? (
<>
<Spinner className="w-4 h-4 mr-2" />
Simulating
@@ -341,11 +342,11 @@ const getCodeExample = (
}
return item;
});
return `import {
return `import {
getContract,
defineChain,
prepareContractCall,
createThirdwebClient
createThirdwebClient
} from "thirdweb";
const client = createThirdwebClient({
@@ -85,7 +85,7 @@ function AccountAvatarFormControl() {
saveButton={{
onClick: handleSave,
disabled: false,
isLoading: updateAvatarMutation.isPending,
isPending: updateAvatarMutation.isPending,
}}
noPermissionText={undefined}
errorText={undefined}
@@ -143,7 +143,7 @@ function AccountNameFormControl(props: {
saveButton={{
onClick: handleSave,
disabled: accountName.length === 0,
isLoading: updateAccountNameMutation.isPending,
isPending: updateAccountNameMutation.isPending,
}}
errorText={undefined}
noPermissionText={undefined} // TODO
@@ -191,7 +191,7 @@ function DeleteAccountCard() {
description={description}
buttonLabel={title}
buttonOnClick={handleDelete}
isLoading={deleteAccount.isPending}
isPending={deleteAccount.isPending}
confirmationDialog={{
title: "Are you sure you want to delete your account?",
description:
+25 -11
View File
@@ -13,34 +13,46 @@ import type {
const THIRDWEB_API_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
const THIRDWEB_API_SECRET = process.env.API_SERVER_SECRET || "";
export async function getLoginPayload(
params: GenerateLoginPayloadParams,
): Promise<LoginPayload> {
const res = await fetch(`${THIRDWEB_API_HOST}/v1/auth/payload`, {
if (!THIRDWEB_API_SECRET) {
throw new Error("API_SERVER_SECRET is not set");
}
const res = await fetch(`${THIRDWEB_API_HOST}/v2/siwe/payload`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-service-api-key": THIRDWEB_API_SECRET,
},
body: JSON.stringify({
address: params.address,
chainId: params.chainId?.toString(),
}),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
console.error("Failed to fetch login payload", res.status, res.statusText);
throw new Error("Failed to fetch login payload");
}
return (await res.json()).payload;
return (await res.json()).data.payload;
}
export async function doLogin(payload: VerifyLoginPayloadParams) {
if (!THIRDWEB_API_SECRET) {
throw new Error("API_SERVER_SECRET is not set");
}
// forward the request to the API server
const res = await fetch(`${THIRDWEB_API_HOST}/v1/auth/login`, {
const res = await fetch(`${THIRDWEB_API_HOST}/v2/siwe/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-service-api-key": THIRDWEB_API_SECRET,
},
body: JSON.stringify({ payload }),
body: JSON.stringify(payload),
});
if (!res.ok) {
@@ -67,7 +79,9 @@ export async function doLogin(payload: VerifyLoginPayloadParams) {
const json = await res.json();
if (!json.token) {
const jwt = json.data.jwt;
if (!jwt) {
console.error("Failed to login - invalid json", json);
throw new Error("Failed to login - invalid json");
}
@@ -77,7 +91,7 @@ export async function doLogin(payload: VerifyLoginPayloadParams) {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${json.token}`,
Authorization: `Bearer ${jwt}`,
},
});
// if we do not have a user, create one
@@ -85,7 +99,7 @@ export async function doLogin(payload: VerifyLoginPayloadParams) {
const newUserRes = await fetch(`${THIRDWEB_API_HOST}/v1/account/create`, {
method: "POST",
headers: {
Authorization: `Bearer ${json.token}`,
Authorization: `Bearer ${jwt}`,
},
});
@@ -104,7 +118,7 @@ export async function doLogin(payload: VerifyLoginPayloadParams) {
// set the token cookie
cookieStore.set(
COOKIE_PREFIX_TOKEN + getAddress(payload.payload.address),
json.token,
jwt,
{
httpOnly: true,
secure: true,
+15 -14
View File
@@ -13,25 +13,25 @@ import { doLogin, doLogout, getLoginPayload, isLoggedIn } from "./auth-actions";
export default function LoginPage() {
return (
<div className="bg-background h-full">
<div className="bg-muted/30 h-screen relative grid place-items-center">
<nav className="fixed top-0 w-full flex flex-row justify-between items-center px-6 py-4 z-20">
<ThirdwebMiniLogo className="max-h-7" />
<ColorModeToggle />
</nav>
<main className="h-full relative grid place-items-center">
<div className="flex flex-col gap-8 z-10">
<h1 className="font-semibold text-2xl">Get started with thirdweb</h1>
<Suspense>
<CustomConnectEmmbed />
</Suspense>
</div>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
alt=""
src="/assets/login/background.svg"
className="fixed bottom-0 right-0"
/>
<main className="flex flex-col gap-6 z-10">
<h1 className="font-semibold text-2xl lg:text-3xl tracking-tight text-center">
Get started with thirdweb
</h1>
<Suspense>
<CustomConnectEmmbed />
</Suspense>
</main>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
alt=""
src="/assets/login/background.svg"
className="fixed -bottom-12 -right-12 lg:bottom-0 lg:right-0"
/>
</div>
);
}
@@ -77,6 +77,7 @@ function CustomConnectEmmbed() {
client={client}
modalSize={isLG ? "wide" : "compact"}
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
className="shadow-lg"
/>
);
}
+7 -11
View File
@@ -1,25 +1,21 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AllChainsProvider } from "contexts/all-chains";
import { ChainsProvider } from "contexts/configured-chains";
import { ThemeProvider } from "next-themes";
import { ThirdwebProvider } from "thirdweb/react";
import { SyncChainStores } from "../stores/chainStores";
const queryClient = new QueryClient();
export function AppRouterProviders(props: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<AllChainsProvider>
<ChainsProvider>
<ThirdwebProvider>
<ThemeProvider attribute="class" defaultTheme="dark">
{props.children}
</ThemeProvider>
</ThirdwebProvider>
</ChainsProvider>
</AllChainsProvider>
<SyncChainStores />
<ThirdwebProvider>
<ThemeProvider attribute="class" defaultTheme="dark">
{props.children}
</ThemeProvider>
</ThirdwebProvider>
</QueryClientProvider>
);
}
@@ -2,14 +2,13 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
import { Flex } from "@chakra-ui/react";
import { ApplyForOpCreditsModal } from "components/onboarding/ApplyForOpCreditsModal";
import { Heading, LinkButton } from "tw-components";
export const SettingsGasCreditsPage = () => {
const { isLoading } = useLoggedInUser();
const { isPending } = useLoggedInUser();
if (isLoading) {
if (isPending) {
return (
<div className="grid w-full min-h-[400px] place-items-center">
<Spinner className="size-10" />
@@ -18,8 +17,8 @@ export const SettingsGasCreditsPage = () => {
}
return (
<Flex flexDir="column" gap={8}>
<Flex direction="row" gap={4} align="center">
<div className="flex flex-col gap-8">
<div className="flex flex-row gap-4 items-center">
<Heading size="title.lg" as="h1">
Apply to the Optimism Superchain App Accelerator
</Heading>
@@ -32,9 +31,9 @@ export const SettingsGasCreditsPage = () => {
>
Learn More
</LinkButton>
</Flex>
</div>
<ApplyForOpCreditsModal />
</Flex>
</div>
);
};
@@ -63,7 +63,7 @@ function TeamNameFormControl(props: {
saveButton={{
onClick: handleSave,
disabled: teamName.length === 0,
isLoading: updateTeamMutation.isPending,
isPending: updateTeamMutation.isPending,
}}
errorText={undefined}
noPermissionText={undefined} // TODO
@@ -121,7 +121,7 @@ function TeamSlugFormControl(props: {
saveButton={{
onClick: handleSave,
disabled: teamSlug.length === 0,
isLoading: updateTeamMutation.isPending,
isPending: updateTeamMutation.isPending,
}}
noPermissionText={undefined} // TODO
>
@@ -168,7 +168,7 @@ function TeamAvatarFormControl() {
saveButton={{
onClick: handleSave,
disabled: false,
isLoading: updateTeamAvatarMutation.isPending,
isPending: updateTeamAvatarMutation.isPending,
}}
noPermissionText={undefined} // TODO
errorText={undefined}
@@ -250,7 +250,7 @@ export function LeaveTeamCard(props: {
description={description}
buttonLabel={title}
buttonOnClick={handleLeave}
isLoading={leaveTeam.isPending}
isPending={leaveTeam.isPending}
confirmationDialog={{
title: `Are you sure you want to leave team "${props.teamName}" ?`,
description:
@@ -311,7 +311,7 @@ export function DeleteTeamCard(props: {
description={description}
buttonLabel={title}
buttonOnClick={handleDelete}
isLoading={deleteTeam.isPending}
isPending={deleteTeam.isPending}
confirmationDialog={{
title: `Are you sure you want to delete team "${props.teamName}" ?`,
description: description,
@@ -2,7 +2,6 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { useAccount, useAccountUsage } from "@3rdweb-sdk/react/hooks/useApi";
import { Flex, HStack } from "@chakra-ui/react";
import { BillingPeriod } from "components/settings/Account/Billing/Period";
import { BillingPlan } from "components/settings/Account/Billing/Plan";
import { Usage } from "components/settings/Account/Usage";
@@ -12,7 +11,7 @@ export const SettingsUsagePage = () => {
const usageQuery = useAccountUsage();
const account = meQuery.data;
if (meQuery.isLoading || !account) {
if (meQuery.isPending || !account) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<Spinner className="size-10" />
@@ -21,20 +20,16 @@ export const SettingsUsagePage = () => {
}
return (
<Flex flexDir="column" gap={8}>
<Flex direction="column">
<div className="flex flex-col gap-8">
<div className="flex flex-col">
<h1 className="font-semibold text-3xl tracking-tight mb-2">Usage</h1>
<HStack
justifyContent="space-between"
flexDir={{ base: "column", lg: "row" }}
alignItems={{ base: "flex-start", lg: "center" }}
>
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center">
<BillingPlan account={account} />
<BillingPeriod account={account} usage={usageQuery.data} />
</HStack>
</Flex>
</div>
</div>
<Usage usage={usageQuery.data} usageLoading={usageQuery.isLoading} />
</Flex>
<Usage usage={usageQuery.data} usagePending={usageQuery.isPending} />
</div>
);
};
@@ -65,7 +65,7 @@ export function AccountAbstractionPage(props: {
<div className="h-6" />
{looggedInUserQuery.isLoading ? (
{looggedInUserQuery.isPending ? (
<div className="flex items-center justify-center h-[400px] border border-border rounded-lg">
<Spinner className="size-14" />
</div>
@@ -34,8 +34,8 @@ export function ConnectAnalyticsDashboard(props: {
<ConnectAnalyticsDashboardUI
walletUsage={walletUsageQuery.data || []}
aggregateWalletUsage={walletUsageAggregateQuery.data || []}
isLoading={
walletUsageQuery.isLoading || walletUsageAggregateQuery.isLoading
isPending={
walletUsageQuery.isPending || walletUsageAggregateQuery.isPending
}
/>
);
@@ -9,7 +9,7 @@ import { WalletDistributionChartCard } from "./_components/WalletDistributionCha
export function ConnectAnalyticsDashboardUI(props: {
walletUsage: WalletStats[];
aggregateWalletUsage: WalletStats[];
isLoading: boolean;
isPending: boolean;
}) {
const { totalWallets, uniqueWallets } = useMemo(() => {
return props.aggregateWalletUsage.reduce(
@@ -36,17 +36,17 @@ export function ConnectAnalyticsDashboardUI(props: {
<DailyConnectionsChartCard
walletStats={props.walletUsage}
isLoading={props.isLoading}
isPending={props.isPending}
/>
<WalletConnectorsChartCard
walletStats={props.walletUsage}
isLoading={props.isLoading}
isPending={props.isPending}
/>
<WalletDistributionChartCard
walletStats={props.walletUsage}
isLoading={props.isLoading}
isPending={props.isPending}
/>
</div>
);
@@ -31,7 +31,7 @@ function Component() {
<ConnectAnalyticsDashboardUI
walletUsage={createWalletStatsStub(30)}
aggregateWalletUsage={createWalletStatsStub(30)}
isLoading={false}
isPending={false}
/>
</div>
);
@@ -34,35 +34,35 @@ function Component() {
<BadgeContainer label="30 days">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(30)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="60 days">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(60)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="10 days">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(10)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="0 days">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(0)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(0)}
isLoading={true}
isPending={true}
/>
</BadgeContainer>
</div>
@@ -46,7 +46,7 @@ const chartLabelToShow: Record<ChartToShow, string> = {
export function DailyConnectionsChartCard(props: {
walletStats: WalletStats[];
isLoading: boolean;
isPending: boolean;
}) {
const { walletStats } = props;
@@ -73,7 +73,7 @@ export function DailyConnectionsChartCard(props: {
return Array.from(chartDataMap.values());
}, [walletStats]);
const disableActions = props.isLoading || barChartData.length === 0;
const disableActions = props.isPending || barChartData.length === 0;
return (
<div className="bg-muted/50 border border-border rounded-lg p-4 md:p-6 relative w-full">
@@ -126,7 +126,7 @@ export function DailyConnectionsChartCard(props: {
config={chartConfig}
className="w-full h-[250px] md:h-[350px]"
>
{props.isLoading ? (
{props.isPending ? (
<LoadingChartState />
) : barChartData.length === 0 ? (
<EmptyChartState />
@@ -34,7 +34,7 @@ const chartLabelToShow: Record<ChartToShow, string> = {
};
export function WalletConnectorsChartCard(props: {
walletStats: WalletStats[];
isLoading: boolean;
isPending: boolean;
}) {
const { walletStats } = props;
const [chartToShow, setChartToShow] = useState<ChartToShow>(
@@ -90,7 +90,7 @@ export function WalletConnectorsChartCard(props: {
}, [walletStats, chartToShow]);
const uniqueWalletTypes = Object.keys(chartConfig);
const disableActions = props.isLoading || chartData.length === 0;
const disableActions = props.isPending || chartData.length === 0;
return (
<div className="bg-muted/50 border border-border rounded-lg p-4 md:p-6 relative w-full">
@@ -142,7 +142,7 @@ export function WalletConnectorsChartCard(props: {
{/* Chart */}
<ChartContainer config={chartConfig} className="w-full h-[400px]">
{props.isLoading ? (
{props.isPending ? (
<LoadingChartState />
) : chartData.length === 0 ? (
<EmptyChartState />
@@ -34,35 +34,35 @@ function Component() {
<BadgeContainer label="30 days">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(30)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="60 days">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(60)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="10 days">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(10)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="0 days">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(0)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(0)}
isLoading={true}
isPending={true}
/>
</BadgeContainer>
</div>
@@ -34,35 +34,35 @@ function Component() {
<BadgeContainer label="30 days">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(30)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="60 days">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(60)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="10 days">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(10)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="0 days">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(0)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(0)}
isLoading={true}
isPending={true}
/>
</BadgeContainer>
</div>
@@ -37,7 +37,7 @@ const chartLabelToShow: Record<ChartToShow, string> = {
export function WalletDistributionChartCard(props: {
walletStats: WalletStats[];
isLoading: boolean;
isPending: boolean;
}) {
const { walletStats } = props;
const [chartToShow, setChartToShow] = useState<ChartToShow>("total");
@@ -114,7 +114,7 @@ export function WalletDistributionChartCard(props: {
};
}, [walletStats]);
const disableActions = props.isLoading || chartData.length === 0;
const disableActions = props.isPending || chartData.length === 0;
return (
<div className="bg-muted/50 border border-border rounded-lg p-4 md:p-6 relative w-full">
<h3 className="text-xl md:text-2xl font-semibold tracking-tight mb-1">
@@ -131,7 +131,7 @@ export function WalletDistributionChartCard(props: {
setChartToShow(v as ChartToShow);
}}
value={chartToShow}
disabled={props.isLoading || chartData.length === 0}
disabled={props.isPending || chartData.length === 0}
>
<SelectTrigger>
<SelectValue />
@@ -184,7 +184,7 @@ export function WalletDistributionChartCard(props: {
config={chartConfig}
className="w-full h-[500px] [&_.recharts-pie-label-text]:fill-foreground mt-6"
>
{props.isLoading ? (
{props.isPending ? (
<LoadingChartState />
) : chartData.length === 0 ? (
<EmptyChartState />
@@ -11,7 +11,7 @@ export default function Page() {
const hasContracts =
deployedContracts.data && deployedContracts.data?.length > 0;
if (deployedContracts.isLoading) {
if (deployedContracts.isPending) {
return (
<div className="min-h-[400px] flex justify-center items-center">
<Spinner className="size-10" />
@@ -0,0 +1,17 @@
"use client";
import { WithEngineInstance } from "components/engine/EnginePageLayout";
import { EngineAlertsPage } from "../../../../../../../../components/engine/alerts/EngineAlertsPage";
import type { EngineInstancePageProps } from "../types";
export default function Page(props: EngineInstancePageProps) {
const { params } = props;
return (
<WithEngineInstance
engineId={props.params.engineId}
content={(res) => <EngineAlertsPage instance={res.instance} />}
rootPath={`/team/${params.team_slug}/${params.project_slug}/engine`}
/>
);
}
@@ -283,7 +283,7 @@ function ProjectNameSetting(props: {
saveButton={{
onClick: handleSubmit,
disabled: !isNameDirty,
isLoading: updateMutation.isPending && isNameDirty,
isPending: updateMutation.isPending && isNameDirty,
}}
bottomText="Please use 64 characters at maximum"
>
@@ -349,7 +349,7 @@ function AllowedDomainsSetting(props: {
saveButton={{
onClick: handleSubmit,
disabled: !isDomainsDirty,
isLoading: updateMutation.isPending && isDomainsDirty,
isPending: updateMutation.isPending && isDomainsDirty,
}}
bottomText={"This is only applicable for web applications"}
>
@@ -417,7 +417,7 @@ function AllowedBundleIDsSetting(props: {
saveButton={{
onClick: handleSubmit,
disabled: !isBundleIdsDirty,
isLoading: updateMutation.isPending && isBundleIdsDirty,
isPending: updateMutation.isPending && isBundleIdsDirty,
}}
noPermissionText={undefined}
header={{
@@ -523,7 +523,7 @@ function EnabledServicesSetting(props: {
saveButton={{
onClick: handleSubmit,
disabled: !form.formState.isDirty,
isLoading: updateMutation.isPending,
isPending: updateMutation.isPending,
}}
bottomText=""
>
@@ -739,7 +739,7 @@ function DeleteProject(props: {
description: description,
}}
description={description}
isLoading={deleteMutation.isPending}
isPending={deleteMutation.isPending}
title={wording === "project" ? "Delete Project" : "Delete API Key"}
/>
);
@@ -6,7 +6,7 @@ export default function RootTeamLayout(props: {
params: { team_slug: string };
}) {
return (
<div className="min-h-full flex flex-col bg-background">
<div className="min-h-screen flex flex-col">
<div className="grow flex flex-col">{props.children}</div>
<TWAutoConnect />
<AppFooter />
@@ -4,31 +4,24 @@ import {
EVMContractInfoProvider,
} from "@3rdweb-sdk/react";
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
import { Flex, SimpleGrid } from "@chakra-ui/react";
import { SimpleGrid } from "@chakra-ui/react";
import {
type DehydratedState,
HydrationBoundary,
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import { ConfigureNetworkModal } from "components/configure-networks/ConfigureNetworkModal";
import { AppShell, type AppShellProps } from "components/layout/app-shell";
import { Onboarding as OnboardingModal } from "components/onboarding";
import { OpCreditsGrantedModalWrapper } from "components/onboarding/OpCreditsGrantedModalWrapper";
import { PosthogIdentifier } from "components/wallets/PosthogIdentifier";
import { AllChainsProvider } from "contexts/all-chains";
import { ChainsProvider } from "contexts/configured-chains";
import { ErrorProvider } from "contexts/error-handler";
import { isSanctionedAddress } from "data/eth-sanctioned-addresses";
import { useAddRecentlyUsedChainId } from "hooks/chains/recentlyUsedChains";
import {
useIsNetworkConfigModalOpen,
useSetIsNetworkConfigModalOpen,
} from "hooks/networkConfigModal";
import { useEffect, useMemo, useState } from "react";
import { useActiveAccount } from "thirdweb/react";
import { Heading } from "tw-components";
import type { ComponentWithChildren } from "types/component-with-children";
import { SyncChainStores } from "../../stores/chainStores";
import { DashboardThirdwebProvider } from "./providers";
interface AppLayoutProps extends AppShellProps {
@@ -56,23 +49,19 @@ export const AppLayout: ComponentWithChildren<AppLayoutProps> = (props) => {
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={props.dehydratedState}>
<ErrorProvider>
<AllChainsProvider>
<ChainsProvider>
<EVMContractInfoProvider value={props.contractInfo}>
<DashboardThirdwebProvider>
<SanctionedAddressesChecker>
<PosthogIdentifier />
<ConfigModal />
<SyncChainStores />
<EVMContractInfoProvider value={props.contractInfo}>
<DashboardThirdwebProvider>
<SanctionedAddressesChecker>
<PosthogIdentifier />
<OnboardingModal />
<OpCreditsGrantedModalWrapper />
<OnboardingModal />
<OpCreditsGrantedModalWrapper />
<AppShell {...props} />
</SanctionedAddressesChecker>
</DashboardThirdwebProvider>
</EVMContractInfoProvider>
</ChainsProvider>
</AllChainsProvider>
<AppShell {...props} />
</SanctionedAddressesChecker>
</DashboardThirdwebProvider>
</EVMContractInfoProvider>
</ErrorProvider>
</HydrationBoundary>
</QueryClientProvider>
@@ -96,31 +85,12 @@ const SanctionedAddressesChecker: ComponentWithChildren = ({ children }) => {
bg="black"
zIndex="banner"
>
<Flex gap={4} direction="column" align="center">
<div className="gap-4 flex flex-col items-center">
<Heading as="p">Address is blocked</Heading>
<CustomConnectWallet />
</Flex>
</div>
</SimpleGrid>
);
}
return <>{children}</>;
};
function ConfigModal() {
const isNetworkConfigModalOpen = useIsNetworkConfigModalOpen();
const setIsNetworkConfigModalOpen = useSetIsNetworkConfigModalOpen();
const addRecentlyUsedChains = useAddRecentlyUsedChainId();
if (!isNetworkConfigModalOpen) {
return null;
}
return (
<ConfigureNetworkModal
onNetworkAdded={(_chain) => {
addRecentlyUsedChains(_chain.chainId);
}}
onClose={() => setIsNetworkConfigModalOpen(false)}
/>
);
}
@@ -37,7 +37,6 @@ import type {
import { getSDKTheme } from "app/components/sdk-component-theme";
import { LOCAL_NODE_PKEY } from "constants/misc";
import { useTrack } from "hooks/analytics/useTrack";
import { useSupportedChain } from "hooks/chains/configureChains";
import { ExternalLinkIcon } from "lucide-react";
import { useTheme } from "next-themes";
import Link from "next/link";
@@ -58,6 +57,7 @@ import {
import { privateKeyToAccount } from "thirdweb/wallets";
import { Button, type ButtonProps, Card, Heading, Text } from "tw-components";
import { THIRDWEB_API_HOST } from "../../constants/urls";
import { useAllChainsData } from "../../hooks/chains/allChains";
import { useV5DashboardChain } from "../../lib/v5-adapter";
const GAS_FREE_CHAINS = [
@@ -121,7 +121,7 @@ export const MismatchButton = forwardRef<HTMLButtonElement, ButtonProps>(
>
<PopoverTrigger>
<Button
isLoading={evmBalance.isLoading}
isLoading={evmBalance.isPending}
{...props}
type={networksMismatch || notEnoughBalance ? "button" : type}
loadingText={loadingText}
@@ -408,10 +408,12 @@ const MismatchNotice: React.FC<{
const activeWallet = useActiveWallet();
const actuallyCanAttemptSwitch =
activeWallet && activeWallet.id !== "global.safe";
const walletConnectedNetworkInfo = useSupportedChain(connectedChainId || -1);
const { idToChain } = useAllChainsData();
const walletConnectedNetworkInfo = connectedChainId
? idToChain.get(connectedChainId)
: undefined;
const isMobile = useBreakpointValue({ base: true, md: false });
const chain = useSupportedChain(desiredChainId || -1);
const chain = desiredChainId ? idToChain.get(desiredChainId) : undefined;
const chainV5 = useV5DashboardChain(desiredChainId);
const onSwitchWallet = useCallback(async () => {
@@ -49,7 +49,7 @@ function useWalletRequiresExternalConfirmation() {
export const TransactionButton: React.FC<TransactionButtonProps> = ({
children,
transactionCount,
isLoading,
isLoading: isPending,
size,
colorScheme,
variant,
@@ -88,7 +88,7 @@ export const TransactionButton: React.FC<TransactionButtonProps> = ({
returnFocusOnClose={false}
initialFocusRef={initialFocusRef}
isLazy
isOpen={walletRequiresExternalConfirmation && isLoading}
isOpen={walletRequiresExternalConfirmation && isPending}
>
<PopoverTrigger>
<ButtonComponent
@@ -96,14 +96,14 @@ export const TransactionButton: React.FC<TransactionButtonProps> = ({
position="relative"
role="group"
colorScheme={colorScheme}
isLoading={isLoading}
isLoading={isPending}
size={size}
variant={variant}
{...restButtonProps}
overflow="hidden"
boxSizing="border-box"
pl={
isLoading || !isConnected
isPending || !isConnected
? undefined
: `calc(${52 + numberWidth}px + var(--chakra-space-${
size === "sm" ? 3 : size === "lg" ? 6 : size === "xs" ? 2 : 4
@@ -1,4 +1,3 @@
import { Flex } from "@chakra-ui/react";
import { ChakraNextImage } from "components/Image";
const partnersCompanies = [
@@ -46,13 +45,7 @@ const partnersCompanies = [
const PartnersSection = () => {
return (
<Flex
alignItems="center"
justifyContent="center"
flexWrap="wrap"
gap={10}
mt="24px"
>
<div className="items-center justify-center flex-wrap gap-10 mt-6 flex flex-row">
{partnersCompanies.slice(0, 10).map((partner, idx) => (
<ChakraNextImage
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
@@ -62,7 +55,7 @@ const PartnersSection = () => {
alt="partner"
/>
))}
</Flex>
</div>
);
};
@@ -1,26 +1,21 @@
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Alert,
AlertIcon,
FormControl,
Radio,
RadioGroup,
} from "@chakra-ui/react";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { ChainIcon } from "components/icons/ChainIcon";
import type { StoredChain } from "contexts/configured-chains";
import { useAllChainsData } from "hooks/chains/allChains";
import { useSupportedChainsNameRecord } from "hooks/chains/configureChains";
import { useRemoveChainModification } from "hooks/chains/useModifyChain";
import { getDashboardChainRpc } from "lib/rpc";
import { CircleAlertIcon } from "lucide-react";
import { useForm } from "react-hook-form";
import { FormErrorMessage, FormLabel } from "tw-components";
import { useAllChainsData } from "../../hooks/chains/allChains";
import {
type StoredChain,
removeChainOverrides,
} from "../../stores/chainStores";
import { ChainIdInput } from "./Form/ChainIdInput";
import { ConfirmationPopover } from "./Form/ConfirmationPopover";
import { IconUpload } from "./Form/IconUpload";
import { NetworkIDInput } from "./Form/NetworkIdInput";
import { RpcInput } from "./Form/RpcInput";
import { TooltipBox } from "./Form/TooltipBox";
export type NetworkConfigFormData = {
name: string;
@@ -30,7 +25,6 @@ export type NetworkConfigFormData = {
type: "testnet" | "mainnet";
icon: string;
slug: string;
status: string;
};
// lowercase it, replace all spaces with hyphens, and then strip all non-alphanumeric characters
@@ -55,9 +49,7 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
prefillSlug,
prefillChainId,
}) => {
const configuredChainNameRecord = useSupportedChainsNameRecord();
const { chainIdToChainRecord } = useAllChainsData();
const removeChainModification = useRemoveChainModification();
const { idToChain, nameToChain } = useAllChainsData();
const form = useForm<NetworkConfigFormData>({
values: {
@@ -73,7 +65,6 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
type: editingChain?.testnet ? "testnet" : "mainnet",
icon: editingChain?.icon?.url || "",
slug: editingChain?.slug || "",
status: editingChain?.status === "deprecated" ? "deprecated" : "active",
},
defaultValues: {
slug: prefillSlug,
@@ -81,17 +72,15 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
mode: "onChange",
});
const isFullyEditable =
!editingChain || editingChain?.isCustom || editingChain.isOverwritten;
const isFullyEditable = !editingChain || editingChain?.isCustom;
const chainId = Number(form.watch("chainId"));
const isChainIdDirty = form.formState.dirtyFields.chainId;
const overwritingChain = isChainIdDirty && chainIdToChainRecord[chainId];
const overwritingChain = isChainIdDirty && idToChain.get(chainId);
const editedFrom =
editingChain?.isModified || editingChain?.isOverwritten
? chainIdToChainRecord[editingChain.chainId]
: undefined;
const editedFrom = editingChain?.isModified
? idToChain.get(editingChain.chainId)
: undefined;
const { ref } = form.register("name", {
required: true,
@@ -104,7 +93,7 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
return true;
}
const chain = configuredChainNameRecord[value];
const chain = nameToChain.get(value);
// valid if chain is not found
if (!chain) {
@@ -118,6 +107,10 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
});
const handleSubmit = form.handleSubmit((data) => {
if (overwritingChain) {
return;
}
let configuredNetwork: StoredChain;
if (editingChain) {
@@ -143,7 +136,6 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
format: "",
},
testnet: data.type === "testnet",
status: data.status,
};
} else {
configuredNetwork = {
@@ -168,40 +160,25 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
format: "",
}
: undefined,
status: data.status,
};
}
if (editingChain && editingChain.chainId !== configuredNetwork.chainId) {
removeChainModification(editingChain.chainId);
removeChainOverrides(editingChain.chainId);
}
if (overwritingChain) {
configuredNetwork.isOverwritten = true;
configuredNetwork.isCustom = false;
configuredNetwork.isModified = false;
} else {
if (editingChain) {
if (editingChain.isCustom) {
configuredNetwork.isOverwritten = false;
configuredNetwork.isCustom = true;
configuredNetwork.isModified = false;
} else if (editingChain.isOverwritten) {
configuredNetwork.isOverwritten = true;
configuredNetwork.isCustom = false;
configuredNetwork.isModified = false;
} else {
configuredNetwork.isCustom = false;
configuredNetwork.isModified = true;
configuredNetwork.isOverwritten = false;
}
} else {
if (editingChain) {
if (editingChain.isCustom) {
configuredNetwork.isCustom = true;
configuredNetwork.isModified = false;
configuredNetwork.isOverwritten = false;
} else {
configuredNetwork.isCustom = false;
configuredNetwork.isModified = true;
}
} else {
configuredNetwork.isCustom = true;
configuredNetwork.isModified = false;
}
onSubmit(configuredNetwork);
});
@@ -211,17 +188,7 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
? "Network already exists"
: "Network name is required");
const disableSubmit = !form.formState.isDirty || !form.formState.isValid;
const submitBtn = (
<Button
variant="primary"
type={disableSubmit ? "submit" : overwritingChain ? "button" : "submit"}
disabled={disableSubmit}
>
{editingChain ? "Update Network" : "Add Network"}
</Button>
);
const disableSubmit = !form.formState.isDirty || !!overwritingChain;
return (
<form
@@ -231,176 +198,164 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
return handleSubmit(e);
}}
>
{/* Network Name for Custom Network */}
<FormControl isRequired isInvalid={!!networkNameErrorMessage}>
<FormLabel>Network name</FormLabel>
<Input
autoComplete="off"
placeholder="e.g. My Network"
type="text"
onChange={(e) => {
const value = e.target.value;
<div className="flex flex-col gap-5 mt-4 px-6 pb-6">
{/* Name + Slug */}
<div className="grid grid-cols-2 gap-4">
{/* name */}
<FormFieldSetup
htmlFor="name"
label="Name"
errorMessage={networkNameErrorMessage}
tooltip={undefined}
isRequired
>
<Input
id="name"
autoComplete="off"
placeholder="My Network"
className="disabled:opacity-100 disabled:bg-muted/50 disabled:text-muted-foreground bg-muted/50"
type="text"
onChange={(e) => {
const value = e.target.value;
form.setValue("name", value, {
shouldValidate: true,
shouldDirty: true,
});
if (isFullyEditable) {
if (!form.formState.dirtyFields.slug) {
form.setValue("slug", nameToSlug(value), {
form.setValue("name", value, {
shouldValidate: true,
shouldDirty: true,
});
}
}
}}
ref={ref}
/>
<FormErrorMessage>{networkNameErrorMessage}</FormErrorMessage>
</FormControl>
if (isFullyEditable) {
if (!form.formState.dirtyFields.slug) {
form.setValue("slug", nameToSlug(value), {
shouldValidate: true,
});
}
}
}}
ref={ref}
/>
</FormFieldSetup>
{/* Slug URL */}
<NetworkIDInput form={form} disabled={!isFullyEditable} />
{/* Slug */}
<NetworkIDInput form={form} disabled={!isFullyEditable} />
</div>
<div className="flex flex-col gap-6 mt-6">
{/* Chain ID + Currency Symbol */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-2 gap-4">
<ChainIdInput form={form} disabled={!isFullyEditable} />
{/* Currency Symbol */}
<FormControl isRequired>
<FormLabel>Currency Symbol</FormLabel>
<FormFieldSetup
isRequired
label="Currency Symbol"
errorMessage={form.formState.errors.currencySymbol?.message}
tooltip={undefined}
htmlFor="currency-symbol"
>
<Input
id="currency-symbol"
disabled={!isFullyEditable}
placeholder="e.g. ETH"
placeholder="ETH"
autoComplete="off"
className="disabled:opacity-100 disabled:bg-muted/50 disabled:text-muted-foreground bg-muted/50 font-mono"
type="text"
{...form.register("currencySymbol", { required: true })}
/>
</FormControl>
</FormFieldSetup>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-2 gap-4">
{/* Testnet / Mainnet */}
<FormControl>
<FormLabel className="!flex gap-1 items-center">
Network type
<TooltipBox
content={
<>
<span className="mb-2 block">
The network type indicates whether it is intended for
production or testing.
</span>
<FormFieldSetup
htmlFor="network-type"
errorMessage={undefined}
isRequired
label="Type"
tooltip={
<span>
<span className="mb-2 block">
The network type indicates whether it is intended for
production or testing.
</span>
<span className="block">
It{`'`}s only used for displaying network type on the
dashboard and does not affect functionality.
</span>
</>
}
/>
</FormLabel>
<span className="block">
It{`'`}s only used for displaying network type on the
dashboard and does not affect functionality.
</span>
</span>
}
>
<RadioGroup
onChange={(value: "testnet" | "mainnet") => {
form.setValue("type", value, {
id="network-type"
value={form.watch("type")}
onValueChange={(v) => {
form.setValue("type", v === "testnet" ? "testnet" : "mainnet", {
shouldValidate: true,
shouldDirty: true,
});
}}
value={form.watch("type")}
className="flex gap-4"
>
<div className="flex gap-4 mt-3">
<Radio value="mainnet">Mainnet</Radio>
<Radio value="testnet">Testnet</Radio>
<div className="flex items-center gap-2">
<RadioGroupItem value="mainnet" id="mainnet" />
<Label htmlFor="mainnet">Mainnet</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="testnet" id="testnet" />
<Label htmlFor="testnet">Testnet</Label>
</div>
</RadioGroup>
</FormControl>
</FormFieldSetup>
{/* Icon */}
<FormControl isInvalid={!!form.formState.errors.icon}>
<FormLabel>Icon</FormLabel>
<FormFieldSetup
isRequired={false}
errorMessage={form.formState.errors.icon?.message}
htmlFor="network-icon"
label="Icon"
tooltip={undefined}
>
<div className="flex gap-1 items-center">
<ChainIcon size={24} ipfsSrc={form.watch("icon")} />
<ChainIcon size={20} ipfsSrc={form.watch("icon")} />
<IconUpload
onUpload={(uri) => {
form.setValue("icon", uri, { shouldDirty: true });
}}
/>
</div>
</FormControl>
</FormFieldSetup>
</div>
{editingChain?.status === "deprecated" && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Active / Deprecated */}
<FormControl>
<FormLabel display="flex">Network status</FormLabel>
<RadioGroup
onChange={(value: "active" | "deprecated") => {
form.setValue("status", value, {
shouldValidate: true,
shouldDirty: true,
});
}}
value={form.watch("status")}
>
<div className="flex mt-3 gap-4">
<Radio value="active">Live</Radio>
<Radio value="deprecated">Deprecated</Radio>
</div>
</RadioGroup>
</FormControl>
</div>
)}
{/* RPC URL */}
<RpcInput form={form} />
{overwritingChain && (
<Alert status="error" fontSize={"small"} borderRadius="md" p={3}>
<AlertIcon />
Chain ID {chainId} is being used by {`"${overwritingChain.name}"`}{" "}
<br />
Adding this network will overwrite it
</Alert>
<div className="p-3 text-sm text-destructive-text rounded-lg border border-border flex items-center gap-2">
<CircleAlertIcon className="size-4" />
<p>
Chain ID {chainId} is taken by {`${overwritingChain.name}`}
</p>
</div>
)}
</div>
{/* Buttons */}
<div className="flex gap-4 flex-col md:flex-row justify-end">
{/* Add / Update Button */}
{overwritingChain && !disableSubmit ? (
<ConfirmationPopover
onConfirm={handleSubmit}
prompt="Are you sure?"
confirmationText="Yes"
description={
<>
This action can be reversed later by resetting this network
{`'s`} config
</>
}
>
{submitBtn}
</ConfirmationPopover>
) : (
submitBtn
)}
{/* Buttons */}
<div className="flex gap-4 flex-col md:flex-row justify-end p-6 bg-muted/50 border-t border-border">
<Button type={"submit"} disabled={disableSubmit}>
{editingChain ? "Update Network" : "Add Network"}
</Button>
{editedFrom && (
<Button
variant="outline"
onClick={() => {
onSubmit(editedFrom);
removeChainModification(editedFrom.chainId);
}}
>
Reset
</Button>
)}
</div>
{editedFrom && (
<Button
variant="outline"
onClick={() => {
onSubmit(editedFrom);
removeChainOverrides(editedFrom.chainId);
}}
>
Reset
</Button>
)}
</div>
</form>
);
@@ -1,41 +1,38 @@
import { Dialog, DialogContent } from "@/components/ui/dialog";
import type { StoredChain } from "contexts/configured-chains";
import { useSetEditChain } from "hooks/networkConfigModal";
import {
type StoredChain,
addRecentlyUsedChainId,
} from "../../stores/chainStores";
import { ConfigureNetworks } from "./ConfigureNetworks";
interface AddNetworkModalProps {
onClose: () => void;
export type ConfigureNetworkModalProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
onNetworkAdded?: (chain: StoredChain) => void;
}
editChain: StoredChain | undefined;
};
export const ConfigureNetworkModal: React.FC<AddNetworkModalProps> = (
export const ConfigureNetworkModal: React.FC<ConfigureNetworkModalProps> = (
props,
) => {
const setEditChain = useSetEditChain();
const onModalClose = () => {
props.onClose();
setEditChain(undefined);
props.onOpenChange(false);
};
return (
<Dialog
open={true}
onOpenChange={(v) => {
if (!v) {
onModalClose();
}
}}
>
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
<DialogContent
className="p-0 z-[10001] max-w-[480px]"
dialogOverlayClassName="z-[10000]"
>
<ConfigureNetworks
onNetworkAdded={(chain) => {
addRecentlyUsedChainId(chain.chainId);
props.onNetworkAdded?.(chain);
onModalClose();
}}
onNetworkConfigured={onModalClose}
editChain={props.editChain}
/>
</DialogContent>
</Dialog>
@@ -1,9 +1,10 @@
import type { StoredChain } from "contexts/configured-chains";
import { useTrack } from "hooks/analytics/useTrack";
import { useAddRecentlyUsedChainId } from "hooks/chains/recentlyUsedChains";
import { useModifyChain } from "hooks/chains/useModifyChain";
import { useEditChain } from "hooks/networkConfigModal";
import { toast } from "sonner";
import {
type StoredChain,
addChainOverrides,
addRecentlyUsedChainId,
} from "../../stores/chainStores";
import { ConfigureNetworkForm } from "./ConfigureNetworkForm";
function useChainConfigTrack() {
@@ -22,16 +23,15 @@ interface ConfigureNetworksProps {
onNetworkAdded?: (chain: StoredChain) => void;
prefillSlug?: string;
prefillChainId?: string;
editChain: StoredChain | undefined;
}
export const ConfigureNetworks: React.FC<ConfigureNetworksProps> = (props) => {
const trackChainConfig = useChainConfigTrack();
const addRecentlyUsedChainId = useAddRecentlyUsedChainId();
const modifyChain = useModifyChain();
const editChain = useEditChain();
const { editChain } = props;
const handleSubmit = (chain: StoredChain) => {
modifyChain(chain);
addChainOverrides(chain);
addRecentlyUsedChainId(chain.chainId);
if (chain.isCustom) {
@@ -52,27 +52,31 @@ export const ConfigureNetworks: React.FC<ConfigureNetworksProps> = (props) => {
};
return (
<div className="p-6">
<h3 className="text-2xl font-semibold tracking-tight mb-5">
{editChain ? "Edit Network" : "Add Custom Network"}
</h3>
<div>
<div className="px-6 pt-6">
<h3 className="text-xl font-semibold tracking-tight">
{editChain ? "Edit Network" : "Add Custom Network"}
</h3>
</div>
{/* Modify the given chain */}
{editChain && (
<ConfigureNetworkForm
editingChain={editChain}
onSubmit={handleSubmit}
/>
)}
<div>
{/* Modify the given chain */}
{editChain && (
<ConfigureNetworkForm
editingChain={editChain}
onSubmit={handleSubmit}
/>
)}
{/* Custom chain */}
{!editChain && (
<ConfigureNetworkForm
prefillSlug={props.prefillSlug}
prefillChainId={props.prefillChainId}
onSubmit={handleSubmit}
/>
)}
{/* Custom chain */}
{!editChain && (
<ConfigureNetworkForm
prefillSlug={props.prefillSlug}
prefillChainId={props.prefillChainId}
onSubmit={handleSubmit}
/>
)}
</div>
</div>
);
};
@@ -1,7 +1,6 @@
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
import { Input } from "@/components/ui/input";
import { FormControl } from "@chakra-ui/react";
import type { UseFormReturn } from "react-hook-form";
import { FormLabel } from "tw-components";
import type { NetworkConfigFormData } from "../ConfigureNetworkForm";
export const ChainIdInput: React.FC<{
@@ -9,15 +8,22 @@ export const ChainIdInput: React.FC<{
disabled: boolean;
}> = ({ form, disabled }) => {
return (
<FormControl
<FormFieldSetup
label="Chain ID"
htmlFor="chain-id"
isRequired
isInvalid={form.formState.errors.chainId?.type === "taken"}
errorMessage={
form.formState.errors.chainId?.type === "taken"
? form.formState.errors.chainId?.message
: undefined
}
tooltip={undefined}
>
<FormLabel>Chain ID</FormLabel>
<Input
disabled={disabled}
placeholder="e.g. 152"
id="chain-id"
autoComplete="off"
className="disabled:opacity-100 disabled:bg-muted/50 disabled:text-muted-foreground bg-muted/50 font-mono"
onKeyDown={(e) => {
// prevent typing e, +, -
if (e.key === "e" || e.key === "+" || e.key === "-") {
@@ -29,6 +35,6 @@ export const ChainIdInput: React.FC<{
required: true,
})}
/>
</FormControl>
</FormFieldSetup>
);
};
@@ -1,57 +0,0 @@
import {
ButtonGroup,
Popover,
PopoverArrow,
PopoverContent,
PopoverFooter,
PopoverHeader,
PopoverTrigger,
useDisclosure,
} from "@chakra-ui/react";
import { Button, Card, Heading, Text } from "tw-components";
export const ConfirmationPopover: React.FC<{
onConfirm: () => void;
children: React.ReactNode;
prompt: string;
description?: React.ReactNode;
confirmationText: React.ReactNode;
}> = (props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
<PopoverTrigger>{props.children}</PopoverTrigger>
<Card
maxW="md"
mr={4}
mb={1}
w="330px"
p={2}
as={PopoverContent}
bg="backgroundBody"
boxShadow="0px 0px 2px 0px var(--popper-arrow-shadow-color)"
>
<PopoverArrow bg="backgroundBody" />
<PopoverHeader border="none">
<Heading size="label.lg" mb={4}>
{props.prompt}
</Heading>
<Text mb={4}> {props.description} </Text>
</PopoverHeader>
<PopoverFooter border="none" display="flex">
<ButtonGroup size="sm">
<Button colorScheme="red" onClick={props.onConfirm}>
{props.confirmationText}
</Button>
<Button onClick={onClose} variant="outline">
Cancel
</Button>
</ButtonGroup>
</PopoverFooter>
</Card>
</Popover>
);
};
@@ -36,14 +36,22 @@ export const IconUpload: React.FC<{ onUpload: (url: string) => void }> = ({
};
return (
<FileInput setValue={handleIconUpload} accept={{ "image/*": [] }}>
<Button variant="ghost" className="gap-2 px-2" size="sm">
{storageUpload.isPending ? (
<Spinner className="size-4" />
) : (
<FiUpload className="size-4" />
)}
<FileInput
setValue={handleIconUpload}
accept={{ "image/*": [] }}
className="flex items-center"
>
<Button
variant="ghost"
className="gap-1.5 px-1 py-0 !h-auto hover:bg-transparent text-muted-foreground hover:text-foreground"
size="sm"
>
Upload Icon
{storageUpload.isPending ? (
<Spinner className="size-3" />
) : (
<FiUpload className="size-3" />
)}
</Button>
</FileInput>
);
@@ -1,42 +1,49 @@
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
import { Input } from "@/components/ui/input";
import { FormControl } from "@chakra-ui/react";
import { useAllChainsData } from "hooks/chains/allChains";
import type { UseFormReturn } from "react-hook-form";
import { FormErrorMessage, FormLabel } from "tw-components";
import { useAllChainsData } from "../../../hooks/chains/allChains";
import type { NetworkConfigFormData } from "../ConfigureNetworkForm";
import { TooltipBox } from "./TooltipBox";
export const NetworkIDInput: React.FC<{
form: UseFormReturn<NetworkConfigFormData>;
disabled?: boolean;
}> = ({ form, disabled }) => {
const slug = form.watch("slug");
const { slugToChainRecord } = useAllChainsData();
const { slugToChain } = useAllChainsData();
const existingChain = slugToChain.get(slug);
return (
<FormControl
<FormFieldSetup
htmlFor="slug"
isRequired
mt={6}
isInvalid={form.formState.errors.slug?.type === "taken"}
label="Slug"
tooltip={
<span>
Network slug is used to identify the network in the thirdweb
dashboard.
<p className="font-semibold mt-2 mb-1">Example</p>
<p className="text-muted-foreground text-sm">
{"thirdweb.com/<slug>/..."}
</p>
</span>
}
errorMessage={
form.formState.errors.slug?.type === "taken" ? (
<>
Can not use {`"${slug}"`}.{" "}
{slug &&
existingChain &&
`It is being used by "${existingChain.name}"`}
</>
) : undefined
}
>
<FormLabel className="!flex items-center gap-1">
Network ID
<TooltipBox
content={
<>
Network ID is used to identify the network in the URL
<p className="font-semibold mt-2 mb-1">Example</p>
<p className="text-link-foreground text-xs">
{"thirdweb.com/<network-id>/<contract-address>"}
</p>
</>
}
/>
</FormLabel>
<Input
disabled={disabled}
autoComplete="off"
placeholder="e.g. ethereum"
placeholder="ethereum"
id="slug"
className="disabled:opacity-100 disabled:bg-muted/50 disabled:text-muted-foreground bg-muted/50 font-mono"
onKeyDown={(e) => {
// only allow alphanumeric characters and dashes
if (!/^[a-z0-9-]*$/i.test(e.key)) {
@@ -52,22 +59,11 @@ export const NetworkIDInput: React.FC<{
return true;
}
return !(_slug in slugToChainRecord);
return !slugToChain.has(_slug);
},
},
})}
/>
<FormErrorMessage>
Can not use Network ID {`"${slug}"`}.
{slug && slug in slugToChainRecord && (
<>
{" "}
It is being used by {`"`}
{slugToChainRecord[slug].name}
{`"`}
</>
)}
</FormErrorMessage>
</FormControl>
</FormFieldSetup>
);
};
@@ -1,8 +1,6 @@
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
import { Input } from "@/components/ui/input";
import { Alert, AlertIcon, FormControl } from "@chakra-ui/react";
import type { UseFormReturn } from "react-hook-form";
import { IoWarning } from "react-icons/io5";
import { FormErrorMessage, FormLabel } from "tw-components";
import type { NetworkConfigFormData } from "../ConfigureNetworkForm";
export const RpcInput: React.FC<{
@@ -23,21 +21,26 @@ export const RpcInput: React.FC<{
});
return (
<FormControl
<FormFieldSetup
isRequired
isInvalid={!!form.formState.errors.rpcUrl}
isDisabled={form.watch("status") !== "active"}
errorMessage={form.formState.errors.rpcUrl?.message}
htmlFor="rpc-url"
tooltip={undefined}
label="RPC URL"
>
<FormLabel>RPC URL</FormLabel>
<Input autoComplete="off" placeholder="https://" type="url" {...reg} />
<Input
id="rpc-url"
autoComplete="off"
placeholder="https://"
type="url"
className="bg-muted/50"
{...reg}
/>
<FormErrorMessage fontSize="12px">Invalid RPC URL</FormErrorMessage>
<Alert bg="transparent" p={0} mt={2} fontSize="12px" color="paragraph">
<AlertIcon as={IoWarning} color="inherit" />
Only add custom networks that you trust. <br /> Malicious RPCs can
record activity and lie about the state of the network.
</Alert>
</FormControl>
<p className="text-sm text-muted-foreground mt-2 leading-normal">
Only add RPC URL that you trust. Malicious RPCs can record activity and
lie about the state of the network.
</p>
</FormFieldSetup>
);
};
@@ -1,12 +0,0 @@
import { ToolTipLabel } from "@/components/ui/tooltip";
import { CircleHelpIcon } from "lucide-react";
export const TooltipBox: React.FC<{
content: React.ReactNode;
}> = ({ content }) => {
return (
<ToolTipLabel label={<div>{content}</div>}>
<CircleHelpIcon className="size-4 text-muted-foreground" />
</ToolTipLabel>
);
};
@@ -0,0 +1,28 @@
"use client";
import { Suspense, useEffect, useState } from "react";
import {
ConfigureNetworkModal,
type ConfigureNetworkModalProps,
} from "./ConfigureNetworkModal";
export function LazyConfigureNetworkModal(props: ConfigureNetworkModalProps) {
const [hasEverOpened, setHasEverOpened] = useState(false);
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (props.open) {
setHasEverOpened(true);
}
}, [props.open]);
if (hasEverOpened) {
return (
<Suspense fallback={null}>
<ConfigureNetworkModal {...props} />
</Suspense>
);
}
return null;
}
@@ -1,6 +1,7 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Alert, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
import { ToolTipLabel } from "@/components/ui/tooltip";
@@ -22,7 +23,7 @@ import { SolidityInput } from "contract-ui/components/solidity-inputs";
import { useTrack } from "hooks/analytics/useTrack";
import { useTxNotifications } from "hooks/useTxNotifications";
import { replaceTemplateValues } from "lib/deployment/template-values";
import { ExternalLinkIcon } from "lucide-react";
import { CircleAlertIcon, ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { useCallback, useMemo } from "react";
import { FormProvider, type UseFormReturn, useForm } from "react-hook-form";
@@ -394,6 +395,16 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
});
}
// handle split things
const payees: string[] = [];
const shares: string[] = [];
if (isSplit && params.recipients?.length) {
for (const recipient of params.recipients) {
payees.push(recipient.address);
shares.push(recipient.sharesBps.toString());
}
}
if (metadata.name === "MarketplaceV3") {
// special case for marketplace
return await deployMarketplaceContract({
@@ -416,6 +427,8 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
const initializeParams = {
...params.contractMetadata,
...params.deployParams,
payees,
shares,
_contractURI,
};
@@ -468,6 +481,9 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
enabled: walletChain !== undefined && metadata !== undefined,
});
const shouldShowDeterministicDeployWarning =
constructorParams.length > 0 && form.watch("deployDeterministic");
return (
<>
<FormProvider {...form}>
@@ -825,23 +841,37 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
{metadata?.deployType === "standard" && (
<>
{/* Deterministic deploy */}
<CheckboxWithLabel>
<Checkbox
{...form.register("deployDeterministic")}
checked={form.watch("deployDeterministic")}
onCheckedChange={(c) =>
form.setValue("deployDeterministic", !!c)
}
/>
<ToolTipLabel label="Allows having the same contract address on multiple chains. You can control the address by specifying a salt for create2 deployment below">
<div className="inline-flex gap-1.5 items-center">
<span className="tex-sm">
Deploy at a deterministic address
</span>
<FiHelpCircle className="size-4" />
</div>
</ToolTipLabel>
</CheckboxWithLabel>
<div className="flex flex-col gap-3">
<CheckboxWithLabel>
<Checkbox
{...form.register("deployDeterministic")}
checked={form.watch("deployDeterministic")}
onCheckedChange={(c) =>
form.setValue("deployDeterministic", !!c)
}
/>
<ToolTipLabel label="Allows having the same contract address on multiple chains. You can control the address by specifying a salt for create2 deployment below">
<div className="inline-flex gap-1.5 items-center">
<span className="tex-sm">
Deploy at a deterministic address
</span>
<FiHelpCircle className="size-4" />
</div>
</ToolTipLabel>
</CheckboxWithLabel>
{shouldShowDeterministicDeployWarning && (
<Alert variant="warning">
<CircleAlertIcon className="size-5" />
<AlertTitle>
Deterministic deployment would only result in the same
contract address if you use the same contructor params
on every deployment.
</AlertTitle>
</Alert>
)}
</div>
{/* Optional Salt Input */}
{isCreate2Deployment && (
@@ -1,9 +1,8 @@
import { Flex } from "@chakra-ui/react";
import { Select } from "chakra-react-select";
import type { SizeProp } from "chakra-react-select";
import { useSupportedChains } from "hooks/chains/configureChains";
import { useMemo } from "react";
import { useFormContext } from "react-hook-form";
import { useAllChainsData } from "../../../hooks/chains/allChains";
interface NetworkDropdownProps {
useCleanChainName?: boolean;
@@ -27,10 +26,10 @@ export const NetworkDropdown: React.FC<NetworkDropdownProps> = ({
size = "md",
}) => {
const form = useFormContext();
const supportedChains = useSupportedChains();
const { allChains } = useAllChainsData();
const options = useMemo(() => {
return supportedChains.map((chain) => {
return allChains.map((chain) => {
return {
label: useCleanChainName
? cleanChainName(chain.name)
@@ -38,7 +37,7 @@ export const NetworkDropdown: React.FC<NetworkDropdownProps> = ({
value: chain.chainId,
};
});
}, [supportedChains, useCleanChainName]);
}, [allChains, useCleanChainName]);
const defaultValues = useMemo(() => {
const networksEnabled = form?.watch(
@@ -54,7 +53,7 @@ export const NetworkDropdown: React.FC<NetworkDropdownProps> = ({
}, [form, options]);
return (
<Flex gap={2} alignItems="center" w="full">
<div className="flex flex-row gap-2 items-center w-full">
<Select
size={size}
placeholder={`${
@@ -106,6 +105,6 @@ export const NetworkDropdown: React.FC<NetworkDropdownProps> = ({
: options.find(({ value: val }) => val === value)
}
/>
</Flex>
</div>
);
};
@@ -1,4 +1,3 @@
import { Flex } from "@chakra-ui/react";
import type { Abi } from "abitype";
import { Select } from "chakra-react-select";
import { useMemo } from "react";
@@ -27,7 +26,7 @@ export const AbiSelector: React.FC<AbiSelectorProps> = ({
}, [abi]);
return (
<Flex gap={2} alignItems="center" w="full">
<div className="flex flex-row gap-2 items-center w-full">
<Select
placeholder="Select function"
options={options}
@@ -45,6 +44,6 @@ export const AbiSelector: React.FC<AbiSelectorProps> = ({
}
}}
/>
</Flex>
</div>
);
};
@@ -173,7 +173,7 @@ export function ContractPublishForm(props: {
: initializerParams;
// during loading and after success we should stay in loading state
const isLoading = sendTx.isPending || sendTx.isSuccess;
const isPending = sendTx.isPending || sendTx.isSuccess;
useIsomorphicLayoutEffect(() => {
window?.scrollTo({
@@ -400,7 +400,7 @@ export function ContractPublishForm(props: {
position="relative"
role="group"
colorScheme="blue"
isLoading={isLoading}
isLoading={isPending}
form="contract-release-form"
isDisabled={isDisabled}
loadingText={
@@ -96,7 +96,7 @@ function ImportForm() {
const address = useActiveAccount()?.address;
const registry = useAllContractList(address);
const isLoading =
const showLoading =
form.formState.isSubmitting ||
addToDashboard.isPending ||
addToDashboard.isSuccess ||
@@ -205,13 +205,13 @@ function ImportForm() {
<div className="flex justify-end">
<Button type="submit" className="gap-2">
{isLoading ? (
{showLoading ? (
<Spinner className="size-4" />
) : (
<PlusIcon className="size-4" />
)}
{isLoading
{showLoading
? addToDashboard.isSuccess || isRedirecting
? "Redirecting"
: "Importing contract"
@@ -14,7 +14,7 @@ export const AddressesModal: React.FC<AddressesModalProps> = ({
buttonTitle,
modalTitle,
}) => {
const { chainIdToChainRecord } = useAllChainsData();
const { idToChain } = useAllChainsData();
const { isOpen, onOpen, onClose } = useDisclosure();
return (
@@ -35,47 +35,45 @@ export const AddressesModal: React.FC<AddressesModalProps> = ({
</Link>
</Text>
{chainIdToChainRecord && (
<Drawer
size="lg"
allowPinchZoom
isOpen={isOpen}
onClose={onClose}
header={{
children: (
<Heading as="h3" size="title.sm">
{modalTitle}
</Heading>
),
}}
>
<Flex flexDir="column" gap={{ base: 2, md: 1 }}>
{Object.entries(chainAddressRecord).map(([chainId, address]) =>
!address ? null : (
<Flex
key={chainId}
flexDir={{ base: "column", md: "row" }}
alignItems={{ base: "auto", md: "center" }}
justifyContent="space-between"
<Drawer
size="lg"
allowPinchZoom
isOpen={isOpen}
onClose={onClose}
header={{
children: (
<Heading as="h3" size="title.sm">
{modalTitle}
</Heading>
),
}}
>
<Flex flexDir="column" gap={{ base: 2, md: 1 }}>
{Object.entries(chainAddressRecord).map(([chainId, address]) =>
!address ? null : (
<Flex
key={chainId}
flexDir={{ base: "column", md: "row" }}
alignItems={{ base: "auto", md: "center" }}
justifyContent="space-between"
>
<Text size="body.md">
{idToChain.get(Number.parseInt(chainId))?.name}
</Text>
<Link
href={`${THIRDWEB_DOMAIN}/${chainId}/${address}`}
isExternal
color="blue.500"
>
<Text size="body.md">
{chainIdToChainRecord[Number.parseInt(chainId)]?.name}
<Text color="inherit" fontFamily="mono">
{address}
</Text>
<Link
href={`${THIRDWEB_DOMAIN}/${chainId}/${address}`}
isExternal
color="blue.500"
>
<Text color="inherit" fontFamily="mono">
{address}
</Text>
</Link>
</Flex>
),
)}
</Flex>
</Drawer>
)}
</Link>
</Flex>
),
)}
</Flex>
</Drawer>
</>
);
};
@@ -14,7 +14,7 @@ interface PublisherAvatarProps extends Omit<MaskedAvatarProps, "src"> {
export const PublisherAvatar: React.FC<PublisherAvatarProps> = ({
address,
isLoading,
isPending,
...restProps
}) => {
const client = useThirdwebClient();
@@ -24,7 +24,7 @@ export const PublisherAvatar: React.FC<PublisherAvatarProps> = ({
);
return (
<MaskedAvatar
isLoading={isLoading || ensQuery.isLoading || publisherProfile.isLoading}
isPending={isPending || ensQuery.isPending || publisherProfile.isPending}
src={
publisherProfile.data?.avatar
? resolveScheme({
@@ -1,4 +1,3 @@
import { Flex } from "@chakra-ui/react";
import type { Abi } from "abitype";
import { Link, Text } from "tw-components";
import type { SourceFile } from "../types";
@@ -11,8 +10,8 @@ interface SourcesPanelProps {
export const SourcesPanel: React.FC<SourcesPanelProps> = ({ sources, abi }) => {
return (
<Flex flexDir="column" gap={8}>
<Flex flexDir="column" gap={4}>
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
{sources && sources?.length > 0 ? (
<SourcesAccordion sources={sources} abi={abi} />
) : (
@@ -27,7 +26,7 @@ export const SourcesPanel: React.FC<SourcesPanelProps> = ({ sources, abi }) => {
</Link>
</Text>
)}
</Flex>
</Flex>
</div>
</div>
);
};
@@ -26,9 +26,6 @@ import {
import { ChainIcon } from "components/icons/ChainIcon";
import { NetworkSelectDropdown } from "components/selects/NetworkSelectDropdown";
import type { BasicContract } from "contract-ui/types/types";
import { useAllChainsData } from "hooks/chains/allChains";
import { useChainSlug } from "hooks/chains/chainSlug";
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
import {
DownloadIcon,
EllipsisVerticalIcon,
@@ -45,6 +42,8 @@ import {
usePagination,
useTable,
} from "react-table";
import { useAllChainsData } from "../../../hooks/chains/allChains";
import { useChainSlug } from "../../../hooks/chains/chainSlug";
import { ImportModal } from "../import-contract/modal";
import { AsyncContractNameCell, AsyncContractTypeCell } from "./cells";
import { ShowMoreButton } from "./show-more-button";
@@ -115,7 +114,7 @@ export const DeployedContracts: React.FC<DeployedContractsProps> = ({
combinedList={contractListQuery.data}
limit={limit}
chainIdsWithDeployments={chainIdsWithDeployments}
loading={contractListQuery.isLoading}
loading={contractListQuery.isPending}
/>
{contractListQuery.data.length === 0 && contractListQuery.isFetched && (
@@ -198,8 +197,7 @@ const ContractTable: React.FC<ContractTableProps> = ({
chainIdsWithDeployments,
loading,
}) => {
const { chainIdToChainRecord } = useAllChainsData();
const configuredChains = useSupportedChainsRecord();
const { idToChain } = useAllChainsData();
const columns: Column<(typeof combinedList)[number]>[] = useMemo(
() => [
@@ -231,9 +229,7 @@ const ContractTable: React.FC<ContractTableProps> = ({
filter: "equals",
// biome-ignore lint/suspicious/noExplicitAny: FIXME
Cell: (cell: any) => {
const data =
configuredChains[cell.row.original.chainId] ||
chainIdToChainRecord[cell.row.original.chainId];
const data = idToChain.get(cell.row.original.chainId);
const cleanedChainName =
data?.name?.replace("Mainnet", "").trim() ||
`Unknown Network (#${cell.row.original.chainId})`;
@@ -296,7 +292,7 @@ const ContractTable: React.FC<ContractTableProps> = ({
},
},
],
[configuredChains, chainIdsWithDeployments, chainIdToChainRecord],
[chainIdsWithDeployments, idToChain],
);
const defaultColumn = useMemo(
@@ -89,7 +89,7 @@ export const PublishedContracts: React.FC<PublishedContractsProps> = ({
contractDetails={slicedData}
hidePublisher
>
{publishedContractsQuery.isLoading && (
{publishedContractsQuery.isPending && (
<Center>
<Flex py={4} direction="row" gap={4} align="center">
<Spinner size="sm" />
@@ -1,5 +1,4 @@
import { Skeleton } from "@/components/ui/skeleton";
import { Flex } from "@chakra-ui/react";
import { formatDistance } from "date-fns/formatDistance";
import { ArrowRightIcon } from "lucide-react";
import { Link } from "tw-components";
@@ -17,17 +16,12 @@ interface ChangelogProps {
export const Changelog: React.FC<ChangelogProps> = ({ changelog }) => {
return (
<Flex
flexDir="column"
gap={6}
position="relative"
className="border-l border-border py-2"
>
<div className="border-l border-border py-2 flex flex-col gap-6 relative">
{changelog.map((item) => (
<Flex key={item.title} gap={4}>
<div className="flex flex-row gap-4" key={item.title}>
<div className="size-2.5 bg-border rounded-full shrink-0 -translate-x-1/2 translate-y-1/2" />
<Flex flexDir="column">
<div className="flex flex-col">
<Link
isExternal
href={`${item.url}?utm_source=thirdweb&utm_campaign=changelog`}
@@ -43,8 +37,8 @@ export const Changelog: React.FC<ChangelogProps> = ({ changelog }) => {
})}
</ClientOnly>
</div>
</Flex>
</Flex>
</div>
</div>
))}
<Link
href="https://blog.thirdweb.com/changelog?utm_source=thirdweb&utm_campaign=changelog"
@@ -53,6 +47,6 @@ export const Changelog: React.FC<ChangelogProps> = ({ changelog }) => {
>
View More <ArrowRightIcon className="size-4" />
</Link>
</Flex>
</div>
);
};
@@ -155,7 +155,7 @@ export const InAppWalletUsersPageContent = (props: {
title="active in-app wallets"
data={itemsToShow}
columns={columns}
isLoading={walletsQuery.isLoading}
isPending={walletsQuery.isPending}
isFetched={walletsQuery.isFetched}
/>
@@ -46,14 +46,18 @@ const sidebarLinkMeta: Array<{ pathId: string; label: string }> = [
pathId: "webhooks",
label: "Webhooks",
},
{
pathId: "configuration",
label: "Configuration",
},
{
pathId: "metrics",
label: "Metrics",
},
{
pathId: "alerts",
label: "Alerts",
},
{
pathId: "configuration",
label: "Configuration",
},
];
const NEXT_PUBLIC_DEMO_ENGINE_URL = process.env.NEXT_PUBLIC_DEMO_ENGINE_URL;
@@ -121,7 +125,7 @@ function QueryAndRenderInstanceHeader(props: {
const instancesQuery = useEngineInstances();
const instance = instancesQuery.data?.find((x) => x.id === props.engineId);
if (instancesQuery.isLoading) {
if (instancesQuery.isPending) {
return <PageLoading />;
}
@@ -151,7 +155,7 @@ function EnsurePermissionAndRenderInstance(props: {
instanceUrl: props.instance.url,
});
if (permissionQuery.isLoading) {
if (permissionQuery.isPending) {
return <PageLoading />;
}
@@ -0,0 +1,234 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import {
type EngineAlertRule,
EngineNotificationChannelTypeConfig,
} from "../../../@3rdweb-sdk/react/hooks/useEngine";
const alertFormSchema = z.object({
subscriptionRoutes: z.array(z.string()),
type: z.enum(
Object.keys(EngineNotificationChannelTypeConfig) as [
keyof typeof EngineNotificationChannelTypeConfig,
],
),
value: z.string(),
});
type EngineAlertFormValues = z.infer<typeof alertFormSchema>;
export function EngineAlertDialogForm(props: {
alertRules: EngineAlertRule[];
title: string;
values: EngineAlertFormValues | null;
open: boolean;
onOpenChange: (open: boolean) => void;
submitButton: {
isLoading: boolean;
label: string;
onSubmit: (value: EngineAlertFormValues) => void;
};
}) {
const form = useForm<z.infer<typeof alertFormSchema>>({
resolver: zodResolver(alertFormSchema),
defaultValues: {
subscriptionRoutes: ["alert.*"],
type: "slack",
},
values: props.values ?? undefined,
});
function onSubmit(values: z.infer<typeof alertFormSchema>) {
props.submitButton.onSubmit(values);
}
const alertRuleOptions: EngineAlertRule[] = [
{
id: "_all_alerts",
routingKey: "alert.*",
title: "All alerts",
description: "Get notified of all alerts.",
createdAt: new Date(),
pausedAt: null,
},
...props.alertRules,
];
const selectedAlertRule = alertRuleOptions.find(
(alertRule) =>
alertRule.routingKey === form.getValues("subscriptionRoutes")[0],
);
return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
<DialogContent
className="p-0 z-[10001]"
dialogOverlayClassName="z-[10000]"
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="p-6">
<DialogHeader className="mb-4">
<DialogTitle className="text-2xl font-semibold tracking-tight">
{props.title}
</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-5">
{/* Alert Rule */}
<FormField
control={form.control}
name="subscriptionRoutes"
render={({ field }) => (
<FormItem>
<FormLabel>Alert Rule</FormLabel>
<FormControl>
<Select
onValueChange={(value) => field.onChange([value])}
value={field.value[0] ?? "alert.*"}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent className="z-[10001]">
<SelectGroup>
{alertRuleOptions.map((option) => (
<SelectItem
key={option.id}
value={option.routingKey}
>
{option.title}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
{selectedAlertRule?.description && (
<FormDescription>
{selectedAlertRule.description}
</FormDescription>
)}
</FormItem>
)}
/>
{/* Notification Type */}
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Notification Type</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
value={field.value}
>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select Notification Type" />
</SelectTrigger>
<SelectContent className="z-[10001]">
<SelectGroup>
{Object.entries(
EngineNotificationChannelTypeConfig,
).map(([type, config]) => (
<SelectItem key={type} value={type}>
{config.display}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Value (webhook URL, Slack URL, email address, etc.) */}
<FormField
control={form.control}
name="value"
render={({ field }) => (
<FormItem>
<FormLabel>
{EngineNotificationChannelTypeConfig[
form.getValues("type")
].valueDisplay ?? "Value"}
</FormLabel>
<FormControl>
<Input
type={
form.getValues("type") === "email"
? "email"
: "text"
}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<DialogFooter className="p-6 bg-muted/50 border-t border-border mt-4 gap-4 lg:gap-2 ">
<Button
variant="outline"
onClick={() => {
props.onOpenChange(false);
}}
>
Cancel
</Button>
<Button
type="submit"
className="min-w-28 gap-2"
disabled={props.submitButton.isLoading}
>
{props.submitButton.isLoading && <Spinner className="size-4" />}
{props.submitButton.label}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,29 @@
import {
type EngineInstance,
useEngineAlertRules,
} from "@3rdweb-sdk/react/hooks/useEngine";
import { ManageEngineAlertsSection } from "./ManageEngineAlerts";
import { RecentEngineAlertsSection } from "./RecentEngineAlerts";
export function EngineAlertsPage(props: {
instance: EngineInstance;
}) {
const alertRulesQuery = useEngineAlertRules(props.instance.id);
const alertRules = alertRulesQuery.data ?? [];
return (
<div>
<ManageEngineAlertsSection
alertRules={alertRules}
engineId={props.instance.id}
alertRulesIsLoading={alertRulesQuery.isLoading}
/>
<div className="h-8" />
<RecentEngineAlertsSection
alertRules={alertRules}
engineId={props.instance.id}
alertRulesIsLoading={alertRulesQuery.isLoading}
/>
</div>
);
}
@@ -0,0 +1,52 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { DialogDescription } from "@radix-ui/react-dialog";
export function EngineDeleteAlertModal(props: {
onConfirm: () => void;
isPending: boolean;
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
<DialogContent className="p-0">
<DialogHeader className="mb-4 p-6">
<DialogTitle className="text-2xl font-semibold tracking-tight">
Delete Alert
</DialogTitle>
<DialogDescription>
You will no longer receive notifications from this webhook.
</DialogDescription>
</DialogHeader>
<DialogFooter className="p-6 bg-muted/50 border-t border-border gap-4 lg:gap-2">
<Button
variant="outline"
onClick={() => {
props.onOpenChange(false);
}}
>
Cancel
</Button>
<Button
className="min-w-28 gap-2"
variant="destructive"
onClick={props.onConfirm}
>
{props.isPending && <Spinner className="size-4" />}
Delete Webhook
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,131 @@
import type {
CreateNotificationChannelInput,
EngineNotificationChannel,
} from "@3rdweb-sdk/react/hooks/useEngine";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { Toaster } from "sonner";
import {
createEngineAlertRuleStub,
createEngineNotificationChannelStub,
} from "../../../stories/stubs";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { ManageEngineAlertsSectionUI } from "./ManageEngineAlerts";
const meta = {
title: "engine/alerts/manage",
component: Story,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
const createEngineAlertMutation = useMutation({
mutationFn: async (values: CreateNotificationChannelInput) => {
console.log("creating alert using input:", values);
await new Promise((resolve) => setTimeout(resolve, 1000));
const channelStub: EngineNotificationChannel = {
createdAt: new Date(),
id: "new-channel",
pausedAt: null,
subscriptionRoutes: values.subscriptionRoutes,
value: values.value,
type: values.type,
};
console.log("created channel:", channelStub);
return channelStub;
},
});
async function deleteAlert(alertId: string) {
console.log("deleting alert:", alertId);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
return (
<div className="py-6 container max-w-[1154px] flex flex-col gap-14">
<BadgeContainer label="2 Alerts">
<ManageEngineAlertsSectionUI
isLoading={false}
onAlertsUpdated={() => {}}
engineId="test-engine-id"
createAlertMutation={createEngineAlertMutation}
deleteAlert={deleteAlert}
alertRules={[
createEngineAlertRuleStub("foo"),
createEngineAlertRuleStub("bar"),
]}
notificationChannels={[
createEngineNotificationChannelStub("foo"),
createEngineNotificationChannelStub("foo", {
pausedAt: null,
}),
]}
/>
</BadgeContainer>
<BadgeContainer label="No Alerts">
<ManageEngineAlertsSectionUI
alertRules={[
createEngineAlertRuleStub("foo"),
createEngineAlertRuleStub("bar"),
]}
engineId="test-engine-id"
notificationChannels={[]}
isLoading={false}
onAlertsUpdated={() => {}}
createAlertMutation={createEngineAlertMutation}
deleteAlert={deleteAlert}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<ManageEngineAlertsSectionUI
alertRules={[
createEngineAlertRuleStub("foo"),
createEngineAlertRuleStub("bar"),
]}
engineId="test-engine-id"
notificationChannels={[]}
isLoading={true}
onAlertsUpdated={() => {}}
createAlertMutation={createEngineAlertMutation}
deleteAlert={deleteAlert}
/>
</BadgeContainer>
<BadgeContainer label="No Alert Rules">
<ManageEngineAlertsSectionUI
alertRules={[]}
engineId="test-engine-id"
notificationChannels={[]}
isLoading={false}
onAlertsUpdated={() => {}}
createAlertMutation={createEngineAlertMutation}
deleteAlert={deleteAlert}
/>
</BadgeContainer>
<Toaster richColors />
</div>
);
}

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