Compare commits

...
Author SHA1 Message Date
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
b76d298245 Version Packages (#4666)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-19 11:01:37 +12:00
kien-ngo c13058a711 [Dashboard] ERC20 Claim form (#4669)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR refactors the token claim functionality by replacing a Drawer with a Sheet component and updating UI elements for a better user experience.

### Detailed summary
- Replaced `Drawer` with `Sheet` component for better UI
- Updated UI elements like buttons, form controls, and labels
- Added transaction handling logic for claiming tokens

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

<!-- end pr-codex -->
2024-09-18 22:32:21 +00:00
jnsdls a78e50413e cleanup posthog (#4678)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates PostHog dependencies across multiple files, replacing `posthog-js-opensource` with `posthog-js` for consistency and version upgrades.

### Detailed summary
- Replaced `posthog-js-opensource` imports with `posthog-js` in multiple files
- Updated `posthog-js` version to "1.67.1" in various package.json files

> The following files were skipped due to too many changes: `pnpm-lock.yaml`

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

<!-- end pr-codex -->
2024-09-18 22:19:14 +00:00
MananTank 3c72aeb8f3 Team layout UI adjustments (#4680)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds upgrade team links to various components and improves search functionality by making it case-insensitive.

### Detailed summary
- Added `upgradeTeamLink` prop to `TeamHeaderUI`, `TeamAndProjectSelectorPopoverButton`, `TeamSelectorMobileMenuButton`, and `TeamSelectionUI`
- Modified search functionality to be case-insensitive for projects and teams
- Updated `TeamSelectorMobileMenuButton` prop name to `TeamSelectorMobileMenuButtonProps`
- Adjusted `TeamSelectionUI` to use the `upgradeTeamLink` prop for displaying an upgrade button

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

<!-- end pr-codex -->
2024-09-18 22:02:12 +00:00
MananTank 90662e8e49 Move pay webhooks tab to page with route in project/connect (#4677)
## 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 and improve the Pay feature in the dashboard by renaming components and updating page structures.

### Detailed summary
- Renamed `WebhooksPage` component to `PayWebhooksPage`.
- Updated page structures and components related to Pay feature.
- Refactored and improved code readability in the Pay feature section.

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

<!-- end pr-codex -->
2024-09-18 22:02:12 +00:00
MananTank 80893aa085 Remove Pay settings tab in project/connect (#4676)
## 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 `PayPageUI` component to use `clientId` instead of `apiKey` for simplicity and clarity.

### Detailed summary
- Updated `PayPageUI` component to accept `clientId` as a string instead of `apiKey`
- Removed unnecessary fields from `apiKey` object
- Updated references to `props.apiKey.key` to `props.clientId` in `PayPageUI` component

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

<!-- end pr-codex -->
2024-09-18 22:02:11 +00:00
MananTank a587ebf125 Remove AA settings from project/connect (#4675)
## 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 `SmartWallets` component with `AccountFactories` in the `AccountAbstractionPage.tsx`.

### Detailed summary
- Replaced `SmartWallets` component with `AccountFactories`.
- Updated component import paths.
- Added a comment for future improvement.

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

<!-- end pr-codex -->
2024-09-18 22:02:11 +00:00
MananTank 009edeb5f5 Remove In-app wallet settings from project/connect (#4674)
## 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 the `Users` component to `InAppWalletUsersPageContent` for clarity and consistency in the embedded wallets feature.

### Detailed summary
- Renamed `Users` component to `InAppWalletUsersPageContent` for clarity
- Updated references to the renamed component in multiple files

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

<!-- end pr-codex -->
2024-09-18 22:02:10 +00:00
MananTank f5a5078ff5 Add Account Abstraction settings in product settings layout (chakra) (#4668)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
**Focus:** Refactoring SponsorshipPolicies to AccountAbstractionSettingsPage for better clarity and consistency.

### Detailed summary
- Renamed `SponsorshipPolicies` to `AccountAbstractionSettingsPage` for clarity
- Updated imports and references accordingly
- Updated form schema and component name
- Improved variable names and code readability

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

<!-- end pr-codex -->
2024-09-18 22:02:10 +00:00
MananTank c08b3b56b2 Add in-app wallet settings on project settings layout (#4667)
## 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 the `Configure` component to `InAppWalletSettingsPage` and update its usage throughout the codebase.

### Detailed summary
- Renamed `Configure` component to `InAppWalletSettingsPage`
- Updated references to `Configure` component to `InAppWalletSettingsPage`
- Updated component props and types for consistency
- Added a new page file for in-app wallet settings with API key retrieval logic

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

<!-- end pr-codex -->
2024-09-18 22:02:09 +00:00
MananTank 4bda0cedc4 Remove posthog cloud (#4671)
## 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 dependencies, remove `posthog-js`, adjust script sources, and optimize the Next.js configuration.

### Detailed summary
- Updated `posthog-js` to `posthog-js-opensource`
- Removed `posthog-js` initialization in `_app.tsx`
- Adjusted script sources in `next.config.js`
- Optimized Next.js configuration for better performance

> The following files were skipped due to too many changes: `pnpm-lock.yaml`

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

<!-- end pr-codex -->
2024-09-18 20:03:39 +00:00
jnsdls 32dbdab4e9 add contract deployment analytics back (#4672)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is to add event tracking using `useTrack` hook in the custom contract deployment form.

### Detailed summary
- Added `useTrack` hook for event tracking
- Implemented tracking for contract deployment events
- Added tracking for adding contract to dashboard
- Implemented error tracking for deployment failures

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

<!-- end pr-codex -->
2024-09-18 19:08:50 +00:00
jnsdls 406de39ecd handle JWT directly in thirdwebClient (#4659)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates dependencies, simplifies ERC721 and ERC1155 updateMetadata extension params, and allows using valid JWTs for authorization.

### Detailed summary
- Updated `tailwindcss` to versions `3.4.12`
- Added `JWTString` type for valid JWTs
- Simplified updateMetadata extension params for ERC721 and ERC1155
- Updated dependencies in various packages
- Removed unnecessary imports and components
- Refactored code to use `getThirdwebClient()` consistently

> The following files were skipped due to too many changes: `apps/dashboard/src/contract-ui/tabs/account/components/account-balance.tsx`, `packages/service-utils/package.json`, `apps/dashboard/src/components/wallets/ConnectWalletMiniPlayground/MiniPlayground.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/components/server/chain-icon.tsx`, `apps/dashboard/src/components/wallets/PosthogIdentifier.tsx`, `apps/dashboard/src/@/constants/thirdweb.server.ts`, `apps/dashboard/src/contract-ui/tabs/listings/components/list-form.tsx`, `packages/thirdweb/package.json`, `apps/dashboard/src/pages/api/frame/base/get-tx-frame.ts`, `apps/dashboard/src/lib/rpc.ts`, `apps/dashboard/src/contract-ui/tabs/manage/components/getModuleInstalledParams.ts`, `apps/dashboard/src/components/contract-components/publisher/masked-avatar.tsx`, `apps/dashboard/src/components/smart-wallets/AccountFactories/account-cell.tsx`, `apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/create/hooks/use-create-ecosystem.ts`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/opengraph-image.tsx`, `apps/dashboard/src/components/contract-components/publisher/edit-profile.tsx`, `apps/dashboard/src/tw-components/nft-media.tsx`, `apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/ecosystem-header.client.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/live-stats.tsx`, `packages/thirdweb/src/utils/fetch.test.ts`, `apps/dashboard/src/components/engine/contract-subscription/contract-subscriptions-table.tsx`, `apps/dashboard/src/app/(dashboard)/contracts/deploy/[compiler_uri]/page.tsx`, `apps/dashboard/src/components/engine/contract-subscription/add-contract-subscription-button.tsx`, `apps/dashboard/src/contract-ui/tabs/account/components/deposit-native.tsx`, `apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/utils/publishedContractOGImageTemplate.tsx`, `apps/dashboard/src/components/custom-contract/contract-header/metadata-header.tsx`, `apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/utils/getPublishedContractsWithPublisherMapping.ts`, `apps/dashboard/src/app/login/page.tsx`, `apps/dashboard/src/contract-ui/hooks/useContractSources.ts`, `apps/dashboard/src/app/(dashboard)/tools/transaction-simulator/components/TransactionSimulator.tsx`, `apps/dashboard/src/components/selects/NetworkSelectorButton.tsx`, `apps/dashboard/src/contract-ui/tabs/split/page.tsx`, `apps/dashboard/src/components/ipfs-upload/dropzone.tsx`, `apps/dashboard/src/utils/tx-frame.ts`, `apps/dashboard/src/contract-ui/tabs/claim-conditions/components/snapshot-upload.tsx`, `apps/dashboard/src/components/explore/contract-card/index.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/PayModal.tsx`, `apps/dashboard/src/components/contract-components/contract-publish-form/index.tsx`, `apps/dashboard/src/contract-ui/tabs/nfts/components/airdrop-upload.tsx`, `packages/thirdweb/src/client/client.test.ts`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/tx/[txHash]/page.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx`, `apps/dashboard/src/components/contract-components/tables/cells.tsx`, `packages/thirdweb/src/extensions/erc721/drops/write/updateMetadata.ts`, `packages/thirdweb/src/utils/fetch.ts`, `apps/dashboard/src/@3rdweb-sdk/react/components/connect-wallet/index.tsx`, `apps/dashboard/src/@/constants/thirdweb.client.ts`, `apps/dashboard/src/app/(dashboard)/contracts/publish/[publish_uri]/page.tsx`, `apps/dashboard/src/components/smart-wallets/AccountFactories/index.tsx`, `apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/[version]/page.tsx`, `packages/thirdweb/src/client/client.ts`, `packages/thirdweb/src/extensions/erc1155/drops/write/updateMetadata.ts`, `apps/dashboard/src/contract-ui/tabs/proposals/components/proposal-button.tsx`, `apps/dashboard/src/lib/ens.ts`, `apps/dashboard/src/@/components/blocks/wallet-address.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts`, `apps/dashboard/src/components/contract-components/contract-deploy-form/custom-contract.tsx`, `apps/dashboard/src/components/buttons/MismatchButton.tsx`, `apps/dashboard/src/contract-ui/tabs/manage/components/ModuleForm.tsx`, `apps/dashboard/src/app/(dashboard)/published-contract/components/publish-based-deploy.tsx`, `apps/dashboard/src/contract-ui/tabs/proposals/page.tsx`, `apps/dashboard/src/contract-ui/tabs/proposals/components/proposal-form.tsx`, `apps/dashboard/src/components/contract-components/fetch-contracts-with-versions.ts`, `apps/dashboard/src/contract-ui/tabs/proposals/components/delegate-button.tsx`, `apps/dashboard/src/components/contract-components/hooks.ts`, `apps/dashboard/src/contract-ui/tabs/manage/page.tsx`, `apps/dashboard/src/pages/[chain_id]/[...paths].tsx`, `apps/dashboard/src/contract-ui/tabs/sources/page.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useSplit.ts`, `apps/dashboard/src/components/contract-components/published-contract/index.tsx`, `apps/dashboard/src/contract-ui/tabs/proposals/components/proposal.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useVote.ts`, `pnpm-lock.yaml`

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

<!-- end pr-codex -->
2024-09-18 18:36:17 +00:00
MananTank 25e1135dc7 Move pay tabs to pages with routes, Add pay settings page in project layout (#4658)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on restructuring the Pay section in the dashboard, improving API key handling, and fixing endpoint issues.

### Detailed summary
- Deleted `PayAnalytics.tsx` and `pay-ui.client.tsx`
- Added components for `NoApiKeys`, `PayConfig`, `APIKeySelector`
- Updated text in `ProjectGeneralSettingsPage.tsx`
- Modified page layouts and settings
- Improved API key retrieval and handling

> The following files were skipped due to too many changes: `apps/dashboard/src/app/(dashboard)/dashboard/connect/pay/page.tsx`, `apps/dashboard/src/components/pay/PayConfig.tsx`

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

<!-- end pr-codex -->
2024-09-18 18:22:45 +00:00
kien-ngo 7a07d05398 [Dashboard] Migrate TokenERC20 Drawer to Sheet (#4662)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates token transfer, mint, and burn functionality in the dashboard.

### Detailed summary
- Updated UI components for token transfer, mint, and burn
- Replaced `useTxNotifications` with custom error handling and toast notifications
- Improved transaction handling and event tracking
- Enhanced form validations and error messages

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

<!-- end pr-codex -->
2024-09-18 18:06:29 +00:00
kien-ngo 53af73c9ae [Dashboard] ERC20 burn tab (#4661)
## 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 token transfer and burn components in the dashboard.

### Detailed summary
- Updated UI components for token transfer and burn
- Replaced `react-icons` with `lucide-react` and `sonner`
- Improved error handling and notifications for token transactions

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

<!-- end pr-codex -->
2024-09-18 18:06:28 +00:00
jnsdls bb51afa723 simplify build tooling and remove redundant dependencies (#4664)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates dependencies, TypeScript configurations, and package versions across multiple packages. It also refactors code in the `dashboard` app for improved functionality and type safety.

### Detailed summary
- Updated `next` version to `14.2.12` in `wallet-ui`, `playground-web`, and `portal`.
- Updated `@types/react` to `^18.3.7` in `wallet-ui`, `playground-web`, and `portal`.
- Updated `eslint` and related plugins in `dashboard` and `portal`.
- Refactored code in `dashboard` app for improved functionality and type safety.
- Updated `@walletconnect` packages to version `2.16.2` in `thirdweb`.
- Modified TypeScript configurations in `typedoc-gen` and `service-utils`.
- Updated `react-native-svg` to version `15.7.1` in `portal`.
- Updated `posthog-js` to version `1.161.6` in `wallet-ui` and `portal`.
- Updated `happy-dom` to version `^15.7.4` in `portal`.
- Updated `typescript` to version `5.6.2` in `typedoc-gen` and `dashboard`.

> The following files were skipped due to too many changes: `apps/dashboard/package.json`, `apps/dashboard/src/core-ui/sidebar/tunnel.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useLoggedInUser.ts`, `packages/typedoc-gen/tsconfig.base.json`, `packages/service-utils/tsconfig.base.json`, `packages/service-utils/package.json`, `pnpm-lock.yaml`

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

<!-- end pr-codex -->
2024-09-18 17:56:14 +00:00
kien-ngo f66f99096a [SDK] Improve tests for marketplace DirectListings (#4619)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates the Marketplace Direct Listings tests for ERC721 and ERC1155 contracts.

### Detailed summary
- Refactors test setup and contract deployment
- Updates NFT minting and approval transactions
- Improves listing creation and validation checks
- Enhances buy transaction testing for ERC721 tokens

> The following files were skipped due to too many changes: `packages/thirdweb/src/extensions/marketplace/direct-listings/direct-listings.test.ts`

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

<!-- end pr-codex -->
2024-09-18 12:40:31 +00:00
joaquim-verges 6ce7c83a3b chore: update @mobile-wallet-protocol/client to 0.0.3 (#4665)
Fixes #4663

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates `@mobile-wallet-protocol/client` to version 0.0.3 and aligns package dependencies and TypeScript type definitions.

### Detailed summary
- Updated `@mobile-wallet-protocol/client` to 0.0.3
- Aligned package dependencies
- Adjusted TypeScript type definitions for various modules

> The following files were skipped due to too many changes: `pnpm-lock.yaml`

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

<!-- end pr-codex -->
2024-09-18 12:18:39 +00:00
kien-ngo 6e69959dc1 [Dashboard] Replace Chakra-ui's useClipboard (#4660)
## 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 code by centralizing the `useClipboard` hook and importing it where needed.

### Detailed summary
- Centralized `useClipboard` hook in a separate file
- Imported `useClipboard` in multiple components for code reusability

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

<!-- end pr-codex -->
2024-09-18 11:36:33 +00:00
kien-ngo e5393efcee [Dashboard] erc20 transfer Drawer (#4643)
chakra-ui Drawer -> Shadcn's Sheet

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new `Sheet` component for modals and refactors the transfer token functionality to use the `Sheet` component.

### Detailed summary
- Added `Sheet`, `SheetContent`, `SheetFooter`, `SheetHeader`, `SheetTitle`, `SheetTrigger` components
- Refactored token transfer functionality to use the new `Sheet` component
- Updated imports and dependencies to include new components and utilities

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

<!-- end pr-codex -->
2024-09-18 10:18:49 +00:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
f68228f5b0 Version Packages (#4654)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-18 14:56:48 +12:00
gregfromstl acd56a7e46 Redirect to login from main ecosystem page (#4657)
## 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 authentication logic to a page component in the wallet-ui app.

### Detailed summary
- Added import for `getCurrentUser` from `@/lib/auth`
- Added import for `redirect` from `next/navigation`
- Updated `Page` function to fetch current user and redirect based on authentication status

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

<!-- end pr-codex -->
2024-09-18 01:54:21 +00:00
jnsdls 00ce74a060 Improve getNFTs compatibility for ERC1155 contracts (#4653) 2024-09-18 01:41:18 +00:00
gregfromstl ef2d406ce5 [SDK] Fix: Remove useless caching (#4656)
## 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 the `fetch.ts` file in the `thirdweb` package and add unit tests in `fetch.test.ts`.

### Detailed summary
- Removed `FETCH_CACHE` and `IS_THIRDWEB_URL_CACHE` WeakMap in `fetch.ts`
- Added `IS_THIRDWEB_URL_CACHE` export in `fetch.ts`
- Added unit tests for `getClientFetch` and `isThirdwebUrl` functions in `fetch.test.ts`

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

<!-- end pr-codex -->
2024-09-18 01:28:15 +00:00
gregfromstl ebbb87b26c Simplify wallet UI redirects (#4655)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the middleware logic in the wallet-ui app by simplifying the URL path handling and removing unnecessary JWT decoding.

### Detailed summary
- Updated URL path generation logic in middleware
- Removed unused JWT decoding and user handling
- Simplified redirection based on ecosystem and URL path

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

<!-- end pr-codex -->
2024-09-18 00:06:30 +00:00
kumaryash90 e89ffd4aa2 Handle custom id for published contract (#4644)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on handling a custom id for published contracts in the `thirdweb` package.

### Detailed summary
- Added handling of custom id for published contracts in `deploy-metadata.ts` file.

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

<!-- end pr-codex -->
2024-09-17 23:44:05 +00:00
gregfromstl cf1683c3fe [SDK] Fix: Switch active connection on wallet account change (#4618)
## 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 a subscription for when the account changes in the wallet manager.

### Detailed summary
- Added a subscription for "accountChanged" event in the wallet manager
- Calls `handleConnection` when the account changes
- Invokes `onConnect` with the new connected wallet
- Updates the active wallet after the account changes

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

<!-- end pr-codex -->
2024-09-17 23:23:27 +00:00
gregfromstl a46843992b [SDK] Feature: Guest accounts (#4631)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to introduce a "guest" login option in the wallet authentication system.

### Detailed summary
- Added "guest" login option for temporary accounts
- Updated locale files to support "guest" login
- Implemented "guest" authentication functionality
- Updated UI components to include "guest" login option

> The following files were skipped due to too many changes: `packages/thirdweb/src/wallets/in-app/core/authentication/types.ts`, `packages/thirdweb/src/react/web/wallets/shared/ConnectWalletSocialOptions.tsx`, `packages/thirdweb/src/react/native/ui/icons/svgs.ts`, `packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx`, `packages/thirdweb/src/wallets/in-app/native/auth/native-auth.ts`, `packages/thirdweb/src/wallets/in-app/native/native-connector.ts`, `packages/thirdweb/src/react/web/wallets/shared/GuestLogin.tsx`, `packages/thirdweb/src/react/core/utils/walletIcon.ts`

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

<!-- end pr-codex -->
2024-09-17 23:11:24 +00:00
gregfromstl 39a6f86b3b Fix wallet UI cookie (#4652)
## 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 console log messages for user address and redirection, and update cookie settings for JWT with additional security configurations.

### Detailed summary
- Added console log messages for user address and redirection
- Updated cookie settings for JWT with additional security configurations: name, value, httpOnly, secure, sameSite, maxAge, domain, path

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

<!-- end pr-codex -->
2024-09-17 23:08:13 +00:00
MananTank 589e81c8b7 Revamp API Key settings UI, Add Project General settings (#4645)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates file paths, refactors components, and improves code readability.

### Detailed summary
- Renamed and relocated several components for better organization
- Updated function names for clarity and consistency
- Improved type imports and removed unnecessary dependencies
- Enhanced code structure and readability for easier maintenance

> The following files were skipped due to too many changes: `apps/dashboard/src/app/(dashboard)/dashboard/connect/pay/components/pay-ui.client.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.stories.tsx`, `apps/dashboard/src/pages/dashboard/settings/api-keys/[id].tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.tsx`

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

<!-- end pr-codex -->
2024-09-17 22:22:53 +00:00
MananTank db16d8ba54 Set auth token everywhere ThirdwebProvider is used (#4650)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds a new `SetAuthHeaderForSDK` component to manage authentication headers for the SDK and updates providers to include it.

### Detailed summary
- Added `SetAuthHeaderForSDK` component to manage authentication headers for SDK
- Updated providers to include the new component for SDK authentication

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

<!-- end pr-codex -->
2024-09-17 22:01:45 +00:00
MananTank 9878f49790 Fix _defaultAdmin not prefilled with connected wallet address (#4649)
Not sure if this is the proper solution

refer to - https://linear.app/thirdweb/issue/DASH-255/custom-contract-deployment-issue-via-dashboard#comment-6afa6f35
2024-09-17 21:34:10 +00:00
gregfromstl a53e8ea13b [Wallet UI] Fix: Redirect properly from login page (#4646)
## 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 user authentication and ecosystem info handling in the wallet UI app.

### Detailed summary
- Added user authentication check and redirect in `layout.tsx`
- Improved error handling for fetching ecosystem info in `ecosystems.ts`
- Refactored middleware logic in `middleware.ts` for better routing

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

<!-- end pr-codex -->
2024-09-17 20:24:32 +00:00
jnsdls 5ee748921a fix auth on contract deploy (#4647) 2024-09-17 18:29:16 +00:00
kien-ngo 3be53f436d [Dashboard] Add from to claimTo (#4642)
Jake did mention sometimes we have issues with allowlist if `from` is not specified.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `claim-form.tsx` file in both the `tokens` and `nfts` directories to include the `from` field with the account address.

### Detailed summary
- Added `from: account.address` field to the transaction object.

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

<!-- end pr-codex -->
2024-09-17 09:26:16 +00:00
Jonas DanielsandGitHub 0946d90744 no-contract-card-ssr (#4641) 2024-09-16 22:35:22 -07:00
Jonas DanielsandGitHub 97a7d83e03 update dependencies & re-run biome (#4640) 2024-09-16 21:55:28 -07:00
jnsdls ca610d6b3d move explore to app router (#4637)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is updating components and pages in the dashboard app related to exploring smart contracts.

### Detailed summary
- Updated `MaskedAvatar` component with new props and styling
- Modified `ExplorePage` layout and content
- Improved `ContractRow` component functionality
- Enhanced `ContractCard` component with initial data handling

> The following files were skipped due to too many changes: `apps/dashboard/src/components/explore/contract-card/index.tsx`, `apps/dashboard/src/app/(dashboard)/explore/[category]/page.tsx`

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

<!-- end pr-codex -->
2024-09-17 03:23:43 +00:00
314 changed files with 8832 additions and 9960 deletions
+1 -3
View File
@@ -2,9 +2,7 @@ module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/typescript",
"plugin:@next/next/recommended",
"plugin:promise/recommended",
"plugin:storybook/recommended",
],
rules: {
@@ -98,7 +96,7 @@ module.exports = {
],
},
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "import", "react-compiler"],
plugins: ["@typescript-eslint", "react-compiler"],
parserOptions: {
ecmaVersion: 2019,
ecmaFeatures: {
+13 -5
View File
@@ -1,8 +1,16 @@
{
"$schema": "https://biomejs.dev/schemas/1.8.1/schema.json",
"$schema": "https://biomejs.dev/schemas/1.9.1/schema.json",
"extends": ["../../biome.json"],
"files": {
"ignore": ["src/graphql/**"]
}
"overrides": [
{
"include": ["src/css/swagger-ui.css"],
"linter": {
"rules": {
"suspicious": {
"noImportantInKeyframe": "off"
}
}
}
}
]
}
+4 -4
View File
@@ -8,7 +8,7 @@ const ContentSecurityPolicy = `
style-src 'self' 'unsafe-inline' https://vercel.live;
font-src 'self' https://vercel.live https://assets.vercel.com;
frame-src * data:;
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com pg.paper.xyz portal.usecontext.io;
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com portal.usecontext.io;
connect-src * data: blob:;
worker-src 'self' blob:;
block-all-mixed-content;
@@ -213,7 +213,7 @@ module.exports = withBundleAnalyzer(
* See: https://github.com/getsentry/sentry-javascript/issues/10468#issuecomment-2004710692
*/
disableServerWebpackPlugin: true,
},
),
),
}
)
)
);
+17 -20
View File
@@ -68,15 +68,14 @@
"ipaddr.js": "^2.2.0",
"lottie-react": "^2.4.0",
"lucide-react": "0.441.0",
"next": "14.2.11",
"next": "14.2.12",
"next-plausible": "^3.12.0",
"next-seo": "^6.5.0",
"next-themes": "^0.3.0",
"nextjs-toploader": "^1.6.12",
"papaparse": "^5.4.1",
"pluralize": "^8.0.0",
"posthog-js": "1.161.3",
"posthog-js-opensource": "npm:[email protected]",
"posthog-js": "1.67.1",
"prism-react-renderer": "^2.3.1",
"prismjs": "^1.29.0",
"qrcode": "^1.5.3",
@@ -108,24 +107,24 @@
"devDependencies": {
"@chakra-ui/cli": "^2.4.1",
"@chromatic-com/storybook": "2.0.2",
"@next/bundle-analyzer": "14.2.11",
"@next/eslint-plugin-next": "14.2.11",
"@next/bundle-analyzer": "14.2.12",
"@next/eslint-plugin-next": "14.2.12",
"@playwright/test": "1.47.1",
"@storybook/addon-essentials": "8.3.0",
"@storybook/addon-interactions": "8.3.0",
"@storybook/addon-links": "8.3.0",
"@storybook/addon-onboarding": "8.3.0",
"@storybook/addon-viewport": "8.3.0",
"@storybook/blocks": "8.3.0",
"@storybook/nextjs": "8.3.0",
"@storybook/react": "8.3.0",
"@storybook/test": "8.3.0",
"@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-viewport": "8.3.1",
"@storybook/blocks": "8.3.1",
"@storybook/nextjs": "8.3.1",
"@storybook/react": "8.3.1",
"@storybook/test": "8.3.1",
"@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.5",
"@types/react": "^18.3.7",
"@types/react-dom": "^18",
"@types/react-table": "^7.7.20",
"@types/spdx-correct": "^3.1.3",
@@ -136,14 +135,12 @@
"checkly": "^4.8.1",
"eslint": "8.57.0",
"eslint-config-biome": "1.8.3",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-promise": "^6.2.0",
"eslint-plugin-react-compiler": "0.0.0-experimental-9ed098e-20240725",
"eslint-plugin-react-compiler": "0.0.0-experimental-ca16900-20240916",
"eslint-plugin-storybook": "^0.8.0",
"next-sitemap": "^4.2.3",
"postcss": "8.4.47",
"storybook": "8.3.0",
"tailwindcss": "3.4.11",
"storybook": "8.3.1",
"tailwindcss": "3.4.12",
"typescript": "5.6.2"
}
}
@@ -1,12 +1,9 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
BadgeContainer,
mobileViewport,
} from "../../../../../../../stories/utils";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { DangerSettingCard } from "./DangerSettingCard";
const meta = {
title: "Team/Settings/components/DangerCard",
title: "blocks/DangerSettingCard",
component: Story,
parameters: {
nextjs: {
@@ -1,12 +1,9 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
BadgeContainer,
mobileViewport,
} from "../../../../../../../stories/utils";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { SettingsCard } from "./SettingsCard";
const meta = {
title: "Team/Settings/components/SettingsCard",
title: "blocks/SettingsCard",
component: Story,
parameters: {
nextjs: {
@@ -1,9 +1,10 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import type React from "react";
export function SettingsCard(props: {
bottomText: string;
bottomText: React.ReactNode;
header?: {
description: string | undefined;
title: string;
@@ -12,9 +13,10 @@ export function SettingsCard(props: {
errorText: string | undefined;
noPermissionText: string | undefined;
saveButton?: {
onClick: () => void;
onClick?: () => void;
disabled: boolean;
isLoading: boolean;
type?: "submit";
};
}) {
return (
@@ -59,6 +61,7 @@ export function SettingsCard(props: {
onClick={props.saveButton.onClick}
disabled={props.saveButton.disabled || props.saveButton.isLoading}
variant="outline"
type={props.saveButton.type}
>
{props.saveButton.isLoading && <Spinner className="size-3" />}
{props.saveButton.isLoading ? "Saving" : "Save"}
@@ -3,7 +3,7 @@ import { mobileViewport } from "../../../stories/utils";
import { SidebarLayout } from "./SidebarLayout";
const meta = {
title: "layouts/SidebarLayout",
title: "blocks/SidebarLayout",
component: Story,
parameters: {
nextjs: {
@@ -6,9 +6,10 @@ import {
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { Check, Copy, ExternalLinkIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { isAddress } from "thirdweb";
import { type ThirdwebClient, isAddress } from "thirdweb";
import { ZERO_ADDRESS } from "thirdweb";
import {
Blobbie,
@@ -17,7 +18,6 @@ import {
useSocialProfiles,
} from "thirdweb/react";
import { resolveScheme } from "thirdweb/storage";
import { thirdwebClient } from "../../constants/client";
import { cn } from "../../lib/utils";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
@@ -27,6 +27,7 @@ export function WalletAddress(props: {
shortenAddress?: boolean;
className?: string;
}) {
const thirdwebClient = useThirdwebClient();
// default back to zero address if no address provided
const address = useMemo(() => props.address || ZERO_ADDRESS, [props.address]);
@@ -77,7 +78,11 @@ export function WalletAddress(props: {
)}
>
{address && (
<WalletAvatar address={address} profiles={profiles.data || []} />
<WalletAvatar
address={address}
profiles={profiles.data || []}
thirdwebClient={thirdwebClient}
/>
)}
<span className="font-mono cursor-pointer">
{profiles.data?.[0]?.name || shortenedAddress}
@@ -178,6 +183,7 @@ export function WalletAddress(props: {
function WalletAvatar(props: {
address: string;
profiles: SocialProfile[];
thirdwebClient: ThirdwebClient;
}) {
const avatar = useMemo(() => {
return props.profiles.find(
@@ -191,7 +197,7 @@ function WalletAvatar(props: {
<div className="size-6 overflow-hidden rounded-full">
{avatar ? (
<MediaRenderer
client={thirdwebClient}
client={props.thirdwebClient}
src={avatar}
className="size-6"
/>
@@ -61,6 +61,7 @@ const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
// biome-ignore lint/a11y/useFocusableInteractive: required
<span
ref={ref}
role="link"
@@ -34,6 +34,7 @@ export function CheckboxWithLabel(props: {
className?: string;
}) {
return (
// biome-ignore lint/a11y/noLabelWithoutControl: cannot do this here because the checkbox is a child of the label
<label
className={cn(
"flex gap-2 items-start text-muted-foreground text-sm cursor-pointer select-none [&>button]:mt-0.5",
@@ -6,7 +6,6 @@ import { cn } from "@/lib/utils";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
@@ -0,0 +1,143 @@
"use client";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { type VariantProps, cva } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 border-border",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
},
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary border-border">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left border-border",
className,
)}
{...props}
/>
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 border-border",
className,
)}
{...props}
/>
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold text-foreground border-border",
className,
)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground border-border", className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};
-30
View File
@@ -1,30 +0,0 @@
import {
DASHBOARD_THIRDWEB_CLIENT_ID,
DASHBOARD_THIRDWEB_SECRET_KEY,
IPFS_GATEWAY_URL,
} from "@/constants/env";
import { createThirdwebClient } from "thirdweb";
export const thirdwebClient = createThirdwebClient(
DASHBOARD_THIRDWEB_SECRET_KEY
? {
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
config: {
storage: {
gatewayUrl: IPFS_GATEWAY_URL,
},
},
}
: {
clientId: DASHBOARD_THIRDWEB_CLIENT_ID,
config: {
storage: {
gatewayUrl: IPFS_GATEWAY_URL,
},
},
},
);
/**
* DO NOT ADD ANYTHING TO THIS FILE IF YOU ARE NOT ABSOLUTELY SURE IT IS OK
*/
+10
View File
@@ -1,2 +1,12 @@
import { cookies } from "next/headers";
export const COOKIE_ACTIVE_ACCOUNT = "tw_active_account";
export const COOKIE_PREFIX_TOKEN = "tw_token_";
export function getActiveAccountCookie() {
return cookies().get(COOKIE_ACTIVE_ACCOUNT)?.value;
}
export function getJWTCookie(address: string) {
return cookies().get(COOKIE_PREFIX_TOKEN + address)?.value;
}
@@ -0,0 +1,41 @@
import {} from "@/constants/env";
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { useActiveAccount } from "thirdweb/react";
import type { GetAuthTokenResponse } from "../../app/api/auth/get-auth-token/route";
import { getThirdwebClient } from "./thirdweb.server";
// returns a thirdweb client with optional JWT passed i
export function useThirdwebClient(jwt?: string) {
const account = useActiveAccount();
const query = useQuery({
queryKey: ["jwt", account?.address],
// only enable the query if there is an account and no JWT is passed in directly
enabled: !!account && !jwt,
retry: false,
queryFn: async () => {
if (!account) {
throw new Error("No account");
}
const res = await fetch(
`/api/auth/get-auth-token?address=${account.address}`,
);
if (!res.ok) {
throw new Error("Failed to get auth token");
}
const json = (await res.json()) as GetAuthTokenResponse;
return json.jwt || undefined;
},
});
return useMemo(
// prfer jwt from props over the one from the token query if it exists
() => getThirdwebClient(jwt || query.data),
[jwt, query.data],
);
}
/**
* DO NOT ADD ANYTHING TO THIS FILE IF YOU ARE NOT ABSOLUTELY SURE IT IS OK
*/
@@ -0,0 +1,23 @@
import {
DASHBOARD_THIRDWEB_CLIENT_ID,
DASHBOARD_THIRDWEB_SECRET_KEY,
IPFS_GATEWAY_URL,
} from "@/constants/env";
import { createThirdwebClient } from "thirdweb";
// returns a thirdweb client with optional JWT passed in
export function getThirdwebClient(jwt?: string) {
return createThirdwebClient({
secretKey: jwt ? jwt : DASHBOARD_THIRDWEB_SECRET_KEY,
clientId: DASHBOARD_THIRDWEB_CLIENT_ID,
config: {
storage: {
gatewayUrl: IPFS_GATEWAY_URL,
},
},
});
}
/**
* DO NOT ADD ANYTHING TO THIS FILE IF YOU ARE NOT ABSOLUTELY SURE IT IS OK
*/
@@ -1,5 +1,3 @@
import { ZERO_ADDRESS } from "thirdweb";
export const networkKeys = {
all: ["network"] as const,
chain: (chainId?: number) => [...networkKeys.all, chainId] as const,
@@ -107,41 +105,3 @@ export const engineKeys = {
queueMetrics: (engineId: string) =>
[...engineKeys.all, engineId, "queueMetrics"] as const,
};
export const splitsKeys = {
all: ["splits"] as const,
lists: () => [...splitsKeys.all, "list"] as const,
list: (address = ZERO_ADDRESS) => [...splitsKeys.lists(), address] as const,
detail: (address = ZERO_ADDRESS) => [...splitsKeys.all, address] as const,
currencies: (address = ZERO_ADDRESS) =>
[...splitsKeys.detail(address), "currencies"] as const,
balances: (address = ZERO_ADDRESS) =>
[...splitsKeys.detail(address), "balances"] as const,
};
export const voteKeys = {
all: ["vote"] as const,
detail: (address = ZERO_ADDRESS) => [...voteKeys.all, address] as const,
proposals: (address = ZERO_ADDRESS) =>
[...voteKeys.detail(address), "proposals"] as const,
proposal: (proposalId = "-1", address = ZERO_ADDRESS) =>
[...voteKeys.proposals(address), proposalId] as const,
balances: (address = ZERO_ADDRESS, addresses = [] as string[]) =>
[...voteKeys.detail(address), "balances", { addresses }] as const,
delegations: (address = ZERO_ADDRESS) =>
[...voteKeys.detail(address), "delegations"] as const,
delegation: (address = ZERO_ADDRESS, userAddress = ZERO_ADDRESS) =>
[...voteKeys.delegations(address), userAddress] as const,
userHasVotedOnProposal: (
proposalId = "-1",
address = ZERO_ADDRESS,
userAddress = ZERO_ADDRESS,
) =>
[
...voteKeys.proposal(proposalId, address),
"hasVotedOnProposal",
userAddress,
] as const,
canExecuteProposal: (proposalId = "-1", address = ZERO_ADDRESS) =>
[...voteKeys.proposal(proposalId, address), "canExecute"] as const,
};
@@ -2,7 +2,7 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { thirdwebClient } from "@/constants/client";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { getSDKTheme } from "app/components/sdk-component-theme";
import { CustomChainRenderer } from "components/selects/CustomChainRenderer";
import { mapV4ChainToV5Chain } from "contexts/map-chains";
@@ -32,6 +32,7 @@ export const CustomConnectWallet = (props: {
connectButtonClassName?: string;
detailsButtonClassName?: string;
}) => {
const thirdwebClient = useThirdwebClient();
const loginRequired =
props.loginRequired === undefined ? true : props.loginRequired;
const { theme } = useTheme();
@@ -222,6 +223,7 @@ function ConnectWalletWelcomeScreen(props: {
export function useCustomConnectModal() {
const { connect } = useConnectModal();
const { theme } = useTheme();
const thirdwebClient = useThirdwebClient();
return useCallback(
(options?: { chain?: Chain }) => {
@@ -244,7 +246,7 @@ export function useCustomConnectModal() {
theme: getSDKTheme(theme === "light" ? "light" : "dark"),
});
},
[connect, theme],
[connect, theme, thirdwebClient],
);
}
@@ -1,4 +1,4 @@
import { thirdwebClient } from "@/constants/client";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useMutation } from "@tanstack/react-query";
import { upload } from "thirdweb/storage";
@@ -8,6 +8,7 @@ type DashboardUploadOptions = {
};
export function useDashboardStorageUpload(options?: DashboardUploadOptions) {
const thirdwebClient = useThirdwebClient();
return useMutation({
mutationFn: async (files: Array<File | string>): Promise<string[]> => {
const uris = await upload({
@@ -3,7 +3,7 @@ import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useQuery } from "@tanstack/react-query";
import type { EnsureLoginResponse } from "app/api/auth/ensure-login/route";
import { usePathname } from "next/navigation";
import { useEffect, useRef } from "react";
import { useEffect, useState } from "react";
import {
useActiveAccount,
useActiveWalletConnectionStatus,
@@ -27,10 +27,16 @@ export function useLoggedInUser(): {
const connectionStatus = useActiveWalletConnectionStatus();
// this is to work around the fact that `connectionStatus` ends up as "disconnected"
// which we *do* care about, but only after we have been "connecting" at least once
const statusWasEverConnecting = useRef(false);
if (connectionStatus === "connecting" || connectionStatus === "connected") {
statusWasEverConnecting.current = true;
}
const [statusWasEverConnecting, setStatusWasEverConnecting] = useState(
connectionStatus === "connecting" || connectionStatus === "connected",
);
// needs to be a useEffect for now
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (connectionStatus === "connecting" || connectionStatus === "connected") {
setStatusWasEverConnecting(true);
}
}, [connectionStatus]);
const query = useQuery({
// enabled if:
@@ -40,7 +46,7 @@ export function useLoggedInUser(): {
enabled:
!!pathname &&
connectionStatus !== "connecting" &&
statusWasEverConnecting.current &&
statusWasEverConnecting &&
isLoginRequired(pathname),
// the last "persist", part of the queryKey, is to make sure that we do not cache this query in indexDB
// convention in v4 of the SDK that we are (ab)using here
@@ -71,14 +77,7 @@ export function useLoggedInUser(): {
if (query.data?.redirectTo) {
router.replace(query.data.redirectTo);
}
if (query.data?.jwt) {
// necessary for legacy things for now (SDK picks it up from there)
// eslint-disable-next-line react-compiler/react-compiler
window.TW_AUTH_TOKEN = query.data.jwt;
} else {
window.TW_AUTH_TOKEN = undefined;
}
}, [query.data?.redirectTo, query.data?.jwt, router]);
}, [query.data?.redirectTo, router]);
// if we are "disconnected" we are not logged in
if (connectionStatus === "disconnected") {
@@ -1,55 +1,62 @@
import {
queryOptions,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import type {
BalanceQueryRequest,
BalanceQueryResponse,
} from "pages/api/moralis/balances";
import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import { type ThirdwebContract, sendAndConfirmTransaction } from "thirdweb";
import { distribute, distributeByToken } from "thirdweb/extensions/split";
import { useSendAndConfirmTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import invariant from "tiny-invariant";
import { splitsKeys } from "..";
import {
useMutationWithInvalidate,
useQueryWithNetwork,
} from "./query/useQueryWithNetwork";
async function getSplitBalances(contract: ThirdwebContract) {
const query = await fetch("/api/moralis/balances", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
chainId: contract.chain.id,
address: contract.address,
} as BalanceQueryRequest),
});
if (query.status >= 400) {
throw new Error(await query.json().then((r) => r.error));
}
return query.json() as Promise<BalanceQueryResponse>;
}
function getQuery(contract: ThirdwebContract) {
return queryOptions({
queryKey: ["split-balances", contract.chain.id, contract.address],
queryFn: () => getSplitBalances(contract),
retry: false,
});
}
export function useSplitBalances(contract: ThirdwebContract) {
const chainId = contract.chain.id;
const contractAddress = contract.address;
const currencies = useQueryWithNetwork(
splitsKeys.currencies(contractAddress),
async () => {
const query = await fetch("/api/moralis/balances", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
chainId,
address: contractAddress,
} as BalanceQueryRequest),
});
if (query.status >= 400) {
throw new Error(await query.json().then((r) => r.error));
}
return query.json() as Promise<BalanceQueryResponse>;
},
{ enabled: !!chainId && !!contractAddress, retry: false },
);
return currencies;
return useQuery(getQuery(contract));
}
export function useSplitDistributeFunds(contract: ThirdwebContract) {
const contractAddress = contract.address;
const balances = useSplitBalances(contract);
const { mutateAsync } = useSendAndConfirmTransaction();
const account = useActiveAccount();
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async () => {
invariant(contract, "split contract is not ready");
invariant(balances.data, "No balances to distribute");
const distributions = (balances.data || [])
return useMutation({
mutationFn: async () => {
invariant(account, "No active account");
const balances =
// get the cached data if it exists, otherwise fetch it
queryClient.getQueryData(getQuery(contract).queryKey) ||
(await queryClient.fetchQuery(getQuery(contract)));
const distributions = balances
.filter((token) => token.display_balance !== "0.0")
.map(async (currency) => {
const transaction =
@@ -59,24 +66,21 @@ export function useSplitDistributeFunds(contract: ThirdwebContract) {
contract,
tokenAddress: currency.token_address,
});
const promise = mutateAsync(transaction);
const promise = sendAndConfirmTransaction({
transaction,
account,
});
toast.promise(promise, {
success: `Successfully distributed ${currency.name}`,
error: `Error distributing ${currency.name}`,
loading: "Distributing funds",
loading: `Distributing ${currency.name}`,
});
});
return await Promise.all(distributions);
},
{
onSuccess: (_data, _variables, _options, invalidate) => {
return invalidate([
splitsKeys.currencies(contractAddress),
splitsKeys.balances(contractAddress),
splitsKeys.list(contractAddress),
]);
},
onSettled: () => {
queryClient.invalidateQueries(getQuery(contract));
},
);
});
}
@@ -1,288 +1,103 @@
import { thirdwebClient } from "@/constants/client";
import { type ThirdwebContract, getContract, toTokens } from "thirdweb";
import { useMutation } from "@tanstack/react-query";
import {
balanceOf,
decimals,
delegate,
delegates,
} from "thirdweb/extensions/erc20";
import {
type VoteType,
canExecute,
castVoteWithReason,
executeProposal,
getAll,
hasVoted,
propose,
token,
} from "thirdweb/extensions/vote";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
type BaseTransactionOptions,
type ThirdwebContract,
getAddress,
getContract,
toTokens,
} from "thirdweb";
import * as ERC20Ext from "thirdweb/extensions/erc20";
import * as VoteExt from "thirdweb/extensions/vote";
import { useSendAndConfirmTransaction } from "thirdweb/react";
import type { Account } from "thirdweb/wallets";
import invariant from "tiny-invariant";
import { voteKeys } from "../cache-keys";
import {
useMutationWithInvalidate,
useQueryWithNetwork,
} from "./query/useQueryWithNetwork";
export function useVoteProposalList(contract?: ThirdwebContract) {
return useQueryWithNetwork(
voteKeys.proposals(contract?.address || ""),
async () => {
invariant(contract, "contract is required");
return await getAll({ contract });
},
{
enabled: !!contract,
},
);
export async function tokensDelegated(
options: BaseTransactionOptions<{ account: Account | undefined }>,
) {
if (!options.account) {
throw new Error("Expected an account to be passed in options");
}
const tokenAddress = await VoteExt.token(options);
if (!tokenAddress) {
throw new Error("Expected a delegated token address");
}
const tokenContract = getContract({
...options.contract,
address: tokenAddress,
});
const delegatedAddress = await ERC20Ext.delegates({
contract: tokenContract,
account: options.account.address,
});
return getAddress(delegatedAddress) === getAddress(options.account.address);
}
export function useHasVotedOnProposal(
contract: ThirdwebContract,
proposalId: bigint,
export async function voteTokenBalances(
options: BaseTransactionOptions<{ addresses: string[] }>,
) {
const userAddress = useActiveAccount()?.address;
return useQueryWithNetwork(
voteKeys.userHasVotedOnProposal(
proposalId.toString(),
contract.address,
userAddress,
invariant(options.addresses.length, "addresses are required");
const tokenAddress = await VoteExt.token({ contract: options.contract });
if (!tokenAddress) {
throw new Error("Expected a delegated token address");
}
const tokenContract = getContract({
...options.contract,
address: tokenAddress,
});
const [decimals, balanceData] = await Promise.all([
ERC20Ext.decimals({ contract: tokenContract }),
Promise.all(
options.addresses.map((address) =>
ERC20Ext.balanceOf({ contract: tokenContract, address }),
),
),
async () => {
invariant(userAddress, "address is required");
return await hasVoted({ contract, proposalId, account: userAddress });
},
{
enabled: !!contract,
},
);
}
export function useCanExecuteProposal(
contract: ThirdwebContract,
proposalId: bigint,
) {
return useQueryWithNetwork(
voteKeys.canExecuteProposal(proposalId.toString(), contract.address),
async () => await canExecute({ contract, proposalId }),
{
enabled: !!contract,
},
);
}
export function useTokensDelegated(contract: ThirdwebContract) {
const userAddress = useActiveAccount()?.address;
return useQueryWithNetwork(
voteKeys.delegation(contract.address, userAddress),
async () => {
invariant(userAddress, "wallet address is required");
const tokenAddress = await token({ contract });
if (!tokenAddress) {
throw new Error("Expected a delegated token address");
}
const tokenContract = getContract({
address: tokenAddress,
chain: contract.chain,
client: thirdwebClient,
});
const delegatedAddress = await delegates({
contract: tokenContract,
account: userAddress,
});
return delegatedAddress?.toLowerCase() === userAddress.toLowerCase();
},
{
enabled: !!contract && !!userAddress,
},
);
}
export function useVoteTokenBalances(
contract: ThirdwebContract,
addresses: string[],
) {
return useQueryWithNetwork(
voteKeys.balances(contract.address, addresses),
async (): Promise<
Array<{ address: string; balance: string; decimals: number }>
> => {
invariant(addresses.length, "addresses are required");
const tokenAddress = await token({ contract });
if (!tokenAddress) {
throw new Error("Expected a delegated token address");
}
const tokenContract = getContract({
address: tokenAddress,
chain: contract.chain,
client: thirdwebClient,
});
const _decimals = await decimals({ contract: tokenContract });
const balanceData = await Promise.all(
addresses.map((address) =>
balanceOf({ contract: tokenContract, address }),
),
);
const balances = addresses.map((address, index) => {
const balance = balanceData[index] || 0n;
return {
address,
balance: toTokens(balance, _decimals),
decimals: _decimals,
};
});
return await Promise.all(balances);
},
{
enabled: addresses.length > 0,
},
);
}
export interface IProposalInput {
description: string;
}
/**
* This hook is a simplified version meant to use on the Dashboard
* since _currently_ the dashboard only supports creating proposals with descriptions
*
* todo: make an extension in the SDK that is more robust and user-friendly ?
*/
export function useProposalCreateMutation(contract: ThirdwebContract) {
const { mutateAsync } = useSendAndConfirmTransaction();
return useMutationWithInvalidate(
async (proposal: IProposalInput) => {
const { description } = proposal;
const transaction = propose({
contract,
description,
targets: [contract.address],
values: [0n],
calldatas: ["0x"],
});
return await mutateAsync(transaction);
},
{
onSuccess: (_data, _options, _variables, invalidate) => {
return invalidate([voteKeys.proposals(contract.address)]);
},
},
);
}
export function useDelegateMutation(contract: ThirdwebContract) {
const userAddress = useActiveAccount()?.address;
const { mutateAsync } = useSendAndConfirmTransaction();
return useMutationWithInvalidate(
async () => {
invariant(userAddress, "address is required");
const tokenAddress = await token({ contract });
if (!tokenAddress) {
throw new Error("Expected a delegated token address");
}
const tokenContract = getContract({
address: tokenAddress,
chain: contract.chain,
client: thirdwebClient,
});
const transaction = delegate({
contract: tokenContract,
delegatee: userAddress,
});
return await mutateAsync(transaction);
},
{
onSuccess: (_data, _options, _variables, invalidate) => {
return invalidate([voteKeys.delegation(contract.address, userAddress)]);
},
},
);
]);
return options.addresses.map((address, index) => {
const balance = balanceData[index] || 0n;
return {
address,
balance: toTokens(balance, decimals),
decimals: decimals,
};
});
}
/**
* Get the decimals of the voting erc20 token
*
* TODO: move this into SDK extensions?
*/
export function useVotingTokenDecimals(contract: ThirdwebContract) {
return useMutationWithInvalidate(async () => {
const tokenAddress = await token({ contract });
if (!tokenAddress) {
throw new Error("Expected a delegated token address");
}
const tokenContract = getContract({
address: tokenAddress,
chain: contract.chain,
client: thirdwebClient,
});
const _decimals = await decimals({ contract: tokenContract });
return _decimals;
export async function votingTokenDecimals(options: BaseTransactionOptions) {
const tokenAddress = await VoteExt.token(options);
if (!tokenAddress) {
throw new Error("Expected a delegated token address");
}
const tokenContract = getContract({
...options.contract,
address: tokenAddress,
});
return await ERC20Ext.decimals({ contract: tokenContract });
}
interface IVoteCast {
voteType: VoteType;
reason?: string;
}
export function useCastVoteMutation(
contract: ThirdwebContract,
proposalId: bigint,
) {
const address = useActiveAccount()?.address;
const contractAddress = contract.address;
export function useDelegateMutation() {
const { mutateAsync } = useSendAndConfirmTransaction();
return useMutationWithInvalidate(
async (voteItem: IVoteCast) => {
invariant(contract, "contract is required");
invariant(address, "address is required");
const { voteType, reason } = voteItem;
const transaction = castVoteWithReason({
contract,
proposalId,
support: voteType,
reason: reason ?? "",
return useMutation({
mutationFn: async (contract: ThirdwebContract) => {
const tokenAddress = await VoteExt.token({ contract });
if (!tokenAddress) {
throw new Error("Expected a delegated token address");
}
const tokenContract = getContract({
...contract,
address: tokenAddress,
});
const transaction = ERC20Ext.delegate({
contract: tokenContract,
delegatee: contract.address,
});
return await mutateAsync(transaction);
},
{
onSuccess: (_data, _options, _variables, invalidate) => {
return invalidate([
voteKeys.proposals(contractAddress),
voteKeys.userHasVotedOnProposal(
proposalId.toString(),
contractAddress,
address,
),
voteKeys.canExecuteProposal(proposalId.toString(), contractAddress),
]);
},
},
);
}
/**
* This hook is a simplified version for the Dashboard
* It doesn't pass any extra info to the execute function
*/
export function useExecuteProposalMutation(
contract: ThirdwebContract,
proposalId: bigint,
) {
const contractAddress = contract.address;
const mutation = useSendAndConfirmTransaction();
return useMutationWithInvalidate(
async () => {
const transaction = executeProposal({ contract, proposalId });
return await mutation.mutateAsync(transaction);
},
{
onSuccess: (_data, _options, _variables, invalidate) => {
return invalidate([
voteKeys.proposals(contractAddress),
voteKeys.canExecuteProposal(proposalId.toString(), contractAddress),
]);
},
},
);
});
}
@@ -2,8 +2,8 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { thirdwebClient } from "@/constants/client";
import { THIRDWEB_ENGINE_FAUCET_WALLET } from "@/constants/env";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
@@ -36,6 +36,7 @@ export function FaucetButton({
chain: ChainMetadata;
amount: string;
}) {
const client = useThirdwebClient();
const address = useActiveAccount()?.address;
const chainId = chain.chainId;
// do not include local overrides for chain pages
@@ -44,7 +45,7 @@ export function FaucetButton({
const faucetWalletBalanceQuery = useWalletBalance({
address: THIRDWEB_ENGINE_FAUCET_WALLET,
chain: definedChain,
client: thirdwebClient,
client,
});
const trackEvent = useTrack();
@@ -1,7 +1,7 @@
"use client";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { thirdwebClient } from "@/constants/client";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useTrack } from "hooks/analytics/useTrack";
import { defineDashboardChain } from "lib/defineDashboardChain";
import { useTheme } from "next-themes";
@@ -10,6 +10,7 @@ import { getSDKTheme } from "../../../../../../components/sdk-component-theme";
export function PayModalButton(props: { chainId: number; label: string }) {
const { theme } = useTheme();
const client = useThirdwebClient();
const trackEvent = useTrack();
return (
<Dialog>
@@ -34,7 +35,7 @@ export function PayModalButton(props: { chainId: number; label: string }) {
dialogCloseClassName="focus:ring-0"
>
<PayEmbed
client={thirdwebClient}
client={client}
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
className="!w-auto"
payOptions={{
@@ -3,8 +3,7 @@
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Skeleton } from "@/components/ui/skeleton";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { thirdwebClient } from "@/constants/client";
import { isProd } from "@/constants/env";
import { DASHBOARD_THIRDWEB_CLIENT_ID, isProd } from "@/constants/env";
import { useQuery } from "@tanstack/react-query";
import { CircleCheck, XIcon } from "lucide-react";
import { hostnameEndsWith } from "utils/url";
@@ -14,7 +13,7 @@ function useChainStatswithRPC(_rpcUrl: string) {
let rpcUrl = _rpcUrl.replace(
// eslint-disable-next-line no-template-curly-in-string
"${THIRDWEB_API_KEY}",
thirdwebClient.clientId,
DASHBOARD_THIRDWEB_CLIENT_ID,
);
// based on the environment hit dev or production
@@ -1,5 +1,5 @@
/* eslint-disable @next/next/no-img-element */
import { thirdwebClient } from "@/constants/client";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { ImageResponse } from "next/og";
import { download } from "thirdweb/storage";
import { fetchChain } from "utils/fetchChain";
@@ -77,8 +77,8 @@ export default async function Image({
),
// download the chain icon if there is one
chain.icon?.url && hasWorkingChainIcon
? download({ uri: chain.icon.url, client: thirdwebClient }).then((res) =>
res.arrayBuffer(),
? download({ uri: chain.icon.url, client: getThirdwebClient() }).then(
(res) => res.arrayBuffer(),
)
: undefined,
// download the background image (based on chain)
@@ -1,6 +1,7 @@
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { thirdwebClient } from "@/constants/client";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { mapV4ChainToV5Chain } from "contexts/map-chains";
import { ZERO_ADDRESS, toTokens } from "thirdweb";
import {
eth_getBlockByHash,
@@ -9,16 +10,17 @@ import {
getRpcClient,
} from "thirdweb/rpc";
import { hexToNumber, shortenAddress, toEther } from "thirdweb/utils";
import { mapV4ChainToV5Chain } from "../../../../../../contexts/map-chains";
import { getChain } from "../../../utils";
export default async function Page(props: {
params: { chain_id: string; txHash: `0x${string}` };
}) {
// consider if we want to pass the JWT here, likely no need to do it but we could?
const client = getThirdwebClient();
const chain = await getChain(props.params.chain_id);
const rpcRequest = getRpcClient({
client: thirdwebClient,
client,
// Do not include chain overrides for chain pages
// eslint-disable-next-line no-restricted-syntax
chain: mapV4ChainToV5Chain(chain),
@@ -5,7 +5,7 @@ import { Input } from "@/components/ui/input";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { SearchIcon, XCircleIcon } from "lucide-react";
import { usePathname, useSearchParams } from "next/navigation";
import { useRef } from "react";
import { useEffect, useRef } from "react";
import { useDebouncedCallback } from "use-debounce";
function cleanUrl(url: string) {
@@ -22,10 +22,13 @@ export const SearchInput: React.FC = () => {
const inputRef = useRef<HTMLInputElement>(null);
// hah, no need for useEffect here this just works!
if (inputRef.current?.value && !searchParams?.get("query")) {
inputRef.current.value = "";
}
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
// reset the input if the query param is removed
if (inputRef.current?.value && !searchParams?.get("query")) {
inputRef.current.value = "";
}
}, [searchParams]);
const handleSearch = useDebouncedCallback((term: string) => {
const params = new URLSearchParams(searchParams ?? undefined);
@@ -1,8 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import "server-only";
import { thirdwebClient } from "@/constants/client";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/env";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { cn } from "@/lib/utils";
import { resolveScheme } from "thirdweb/storage";
@@ -15,7 +14,7 @@ export async function ChainIcon(props: {
}) {
if (props.iconUrl) {
const resolved = resolveScheme({
client: thirdwebClient,
client: getThirdwebClient(),
uri: props.iconUrl,
});
const res = await fetch(resolved, {
@@ -1,6 +1,6 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { setOverrides } from "lib/vercel-utils";
import { fetchDeployMetadata } from "thirdweb/contract";
import { thirdwebClient } from "../../../../../@/constants/client";
import { DeployContractInfo } from "../../../published-contract/components/contract-info";
import { DeployFormForUri } from "../../../published-contract/components/uri-based-deploy";
@@ -15,7 +15,7 @@ type DirectDeployPageProps = {
export default async function DirectDeployPage(props: DirectDeployPageProps) {
const parsedUri = decodeURIComponent(props.params.compiler_uri);
const metadata = await fetchDeployMetadata({
client: thirdwebClient,
client: getThirdwebClient(),
// force `ipfs://` prefix
uri: parsedUri.startsWith("ipfs://") ? parsedUri : `ipfs://${parsedUri}`,
});
@@ -27,7 +27,11 @@ export default async function DirectDeployPage(props: DirectDeployPageProps) {
description={metadata.description}
logo={metadata.logo}
/>
<DeployFormForUri contractMetadata={metadata} modules={null} />
<DeployFormForUri
contractMetadata={metadata}
modules={null}
pathname={`/contracts/deploy/${props.params.compiler_uri}`}
/>
</div>
);
}
@@ -1,8 +1,10 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { thirdwebClient } from "@/constants/client";
import { getActiveAccountCookie, getJWTCookie } from "@/constants/cookie";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { ContractPublishForm } from "components/contract-components/contract-publish-form";
import { setOverrides } from "lib/vercel-utils";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { fetchDeployMetadata } from "thirdweb/contract";
setOverrides();
@@ -23,13 +25,26 @@ export default async function PublishContractPage(
const publishMetadata = await fetchDeployMetadata({
uri: publishUri,
client: thirdwebClient,
client: getThirdwebClient(),
});
const pathname = `/contracts/publish/${props.params.publish_uri}`;
const address = getActiveAccountCookie();
if (!address) {
redirect(`/login?next=${encodeURIComponent(pathname)}`);
}
const token = getJWTCookie(address);
if (!token) {
redirect(`/login?next=${encodeURIComponent(pathname)}`);
}
return (
<div className="container py-8 flex flex-col gap-8">
<ChakraProviderSetup>
<ContractPublishForm
jwt={token}
publishMetadata={publishMetadata}
onPublishSuccess={async () => {
"use server";
@@ -13,7 +13,7 @@ import {
} from "@/components/ui/dropdown-menu";
import { Skeleton } from "@/components/ui/skeleton";
import { TabLinks } from "@/components/ui/tabs";
import { thirdwebClient } from "@/constants/client";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import {
AlertTriangleIcon,
CheckIcon,
@@ -123,6 +123,7 @@ export function EcosystemHeader(props: {
refetchOnWindowFocus: false,
initialData: props.ecosystem,
});
const client = useThirdwebClient();
const ecosystem = fetchedEcosystem ?? props.ecosystem;
@@ -140,7 +141,7 @@ export function EcosystemHeader(props: {
<Image
src={resolveScheme({
uri: ecosystem.imageUrl,
client: thirdwebClient,
client,
})}
sizes="100px"
alt={ecosystem.name}
@@ -1,4 +1,4 @@
import { thirdwebClient } from "@/constants/client";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
import {
type UseMutationOptions,
@@ -24,6 +24,8 @@ export function useCreateEcosystem(
const { isLoggedIn } = useLoggedInUser();
const queryClient = useQueryClient();
const client = useThirdwebClient();
return useMutation({
// Returns the created ecosystem slug
mutationFn: async (params: CreateEcosystemParams): Promise<string> => {
@@ -32,7 +34,7 @@ export function useCreateEcosystem(
}
const imageUri = await upload({
client: thirdwebClient,
client,
files: [params.logo],
});
@@ -0,0 +1,61 @@
import { TabPathLinks } from "@/components/ui/tabs";
import { redirect } from "next/navigation";
import { getApiKeys } from "../../../../../api/lib/getAPIKeys";
import { APIKeySelector } from "../components/APIKeySelector";
export default async function Layout(props: {
params: {
id: string;
};
children: React.ReactNode;
}) {
const apiKeys = await getApiKeys();
const firstKey = apiKeys[0];
if (!firstKey) {
redirect("/dashboard/connect/pay/no-keys");
}
const selectedKey = apiKeys.find((x) => x.id === props.params.id);
if (!selectedKey) {
redirect("/dashboard/connect/pay");
}
const layoutPath = `/dashboard/connect/pay/${selectedKey.id}`;
return (
<div>
<div className="flex-col gap-4 flex lg:flex-row w-full relative">
<div className="max-sm:w-full lg:min-w-[200px] lg:absolute right-0 bottom-3 z-10">
<APIKeySelector apiKeys={apiKeys} selectedApiKey={selectedKey} />
</div>
<TabPathLinks
className="w-full"
links={[
{
name: "Analytics",
path: layoutPath,
exactMatch: true,
},
{
name: "Webhooks",
path: `${layoutPath}/webhooks`,
},
{
name: "Settings",
path: `${layoutPath}/settings`,
},
]}
/>
</div>
<div className="h-4" />
{props.children}
</div>
);
}
export const dynamic = "force-dynamic";
@@ -0,0 +1,17 @@
import { notFound } from "next/navigation";
import { PayAnalytics } from "../../../../../../components/pay/PayAnalytics/PayAnalytics";
import { getAPIKey } from "../../../../../api/lib/getAPIKeys";
export default async function Page(props: {
params: {
id: string;
};
}) {
const apiKey = await getAPIKey(props.params.id);
if (!apiKey) {
notFound();
}
return <PayAnalytics clientId={apiKey.key} />;
}
@@ -0,0 +1,17 @@
import { notFound } from "next/navigation";
import { PayConfig } from "../../../../../../../components/pay/PayConfig";
import { getAPIKey } from "../../../../../../api/lib/getAPIKeys";
export default async function Page(props: {
params: {
id: string;
};
}) {
const apiKey = await getAPIKey(props.params.id);
if (!apiKey) {
notFound();
}
return <PayConfig apiKey={apiKey} />;
}
@@ -0,0 +1,17 @@
import { notFound } from "next/navigation";
import { getAPIKey } from "../../../../../../api/lib/getAPIKeys";
import { PayWebhooksPage } from "../../components/webhooks.client";
export default async function Page(props: {
params: {
id: string;
};
}) {
const apiKey = await getAPIKey(props.params.id);
if (!apiKey) {
notFound();
}
return <PayWebhooksPage clientId={apiKey.key} />;
}
@@ -0,0 +1,41 @@
"use client";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
export function APIKeySelector(props: {
apiKeys: ApiKey[];
selectedApiKey: ApiKey;
}) {
const router = useDashboardRouter();
return (
<Select
value={props.selectedApiKey.id}
onValueChange={(keyId) => {
const newKey = props.apiKeys.find((x) => x.id === keyId);
if (newKey) {
router.push(`/dashboard/connect/pay/${newKey.id}`);
}
}}
>
<SelectTrigger>
<SelectValue>{props.selectedApiKey.name}</SelectValue>
</SelectTrigger>
<SelectContent>
{props.apiKeys.map((key) => (
<SelectItem key={key.id} value={key.id}>
{key.name}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
@@ -1,81 +0,0 @@
"use client";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { TabButtons } from "@/components/ui/tabs";
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
import { PayAnalytics } from "components/pay/PayAnalytics/PayAnalytics";
import { PayConfig } from "components/pay/PayConfig";
import { useMemo, useState } from "react";
import { WebhooksPage } from "./webhooks.client";
export function PayUI(props: {
apiKeys: ApiKey[];
}) {
const [selectedKeyId, setSelectedKeyId] = useState<string>(
props.apiKeys[0].key,
);
const [activeTab, setActiveTab] = useState<
"settings" | "analytics" | "webhooks"
>("analytics");
const selectedKey = useMemo(() => {
// biome-ignore lint/style/noNonNullAssertion: This is a valid use case for non-null assertion
return props.apiKeys.find((key) => key.key === selectedKeyId)!;
}, [props.apiKeys, selectedKeyId]);
return (
<>
<div className="flex-col gap-4 flex lg:flex-row w-full relative">
<div className="max-sm:w-full lg:min-w-[200px] lg:absolute right-0 bottom-3 z-10">
<Select defaultValue={selectedKeyId} onValueChange={setSelectedKeyId}>
<SelectTrigger>
<SelectValue>{selectedKey?.name || selectedKeyId}</SelectValue>
</SelectTrigger>
<SelectContent>
{props.apiKeys.map((key) => (
<SelectItem key={key.key} value={key.key}>
{key.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<TabButtons
containerClassName="w-full"
tabs={[
{
name: "Analytics",
isActive: activeTab === "analytics",
onClick: () => setActiveTab("analytics"),
isEnabled: true,
},
{
name: "Webhooks",
isActive: activeTab === "webhooks",
onClick: () => setActiveTab("webhooks"),
isEnabled: true,
},
{
name: "Settings",
isActive: activeTab === "settings",
onClick: () => setActiveTab("settings"),
isEnabled: true,
},
]}
/>
</div>
{/* TODO: split this into sub-pages */}
{activeTab === "analytics" && <PayAnalytics clientId={selectedKey.key} />}
{activeTab === "settings" && <PayConfig apiKey={selectedKey} />}
{activeTab === "webhooks" && <WebhooksPage clientId={selectedKey.key} />}
</>
);
}
@@ -1,3 +1,5 @@
"use client";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
@@ -55,11 +57,11 @@ type Webhook = {
secret: string;
};
type WebhooksPageProps = {
type PayWebhooksPageProps = {
clientId: string;
};
export function WebhooksPage(props: WebhooksPageProps) {
export function PayWebhooksPage(props: PayWebhooksPageProps) {
const webhooksQuery = useQuery({
queryKey: ["webhooks", props.clientId],
queryFn: async () => {
@@ -154,7 +156,7 @@ const formSchema = z.object({
label: z.string().min(1, "Please enter a label."),
});
function CreateWebhookButton(props: PropsWithChildren<WebhooksPageProps>) {
function CreateWebhookButton(props: PropsWithChildren<PayWebhooksPageProps>) {
const [open, setOpen] = useState(false);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@@ -278,7 +280,7 @@ function CreateWebhookButton(props: PropsWithChildren<WebhooksPageProps>) {
}
function DeleteWebhookButton(
props: PropsWithChildren<WebhooksPageProps & { webhookId: string }>,
props: PropsWithChildren<PayWebhooksPageProps & { webhookId: string }>,
) {
const [open, setOpen] = useState(false);
const queryClient = useQueryClient();
@@ -0,0 +1,31 @@
import Link from "next/link";
export default async function Layout(props: {
children: React.ReactNode;
}) {
return (
<div className="flex flex-col gap-8 w-full">
<div className="flex flex-col lg:flex-row gap-6 justify-between items-start">
<div className="max-w-[800px]">
<h1 className="text-3xl lg:text-4xl tracking-tight font-bold mb-3">
Pay
</h1>
<p className="text-muted-foreground leading-7">
Pay allows your users to purchase cryptocurrencies and execute
transactions with their credit card or debit card, or with any token
via cross-chain routing.{" "}
<Link
target="_blank"
href="https://portal.thirdweb.com/connect/pay/overview"
className="!text-link-foreground"
>
Learn more
</Link>
</p>
</div>
</div>
{props.children}
</div>
);
}
@@ -0,0 +1,5 @@
import { NoApiKeys } from "../../../../../../components/settings/ApiKeys/NoApiKeys";
export default function Page() {
return <NoApiKeys service="Pay in Connect" />;
}
@@ -1,86 +1,13 @@
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
import { NoApiKeys } from "components/settings/ApiKeys/NoApiKeys";
import { cookies } from "next/headers";
import Link from "next/link";
import { redirect } from "next/navigation";
import { PayUI } from "./components/pay-ui.client";
import { getApiKeys } from "../../../../api/lib/getAPIKeys";
async function getApiKeys(authToken: string) {
const res = await fetch(
`${process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com"}/v1/keys`,
{
method: "GET",
export default async function Page() {
const apiKeys = await getApiKeys();
const firstKey = apiKeys[0];
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
},
);
const json = await res.json();
if (json.error) {
throw new Error(json.error.message);
}
return json.data as ApiKey[];
}
export default async function DashboardConnectPayPage() {
const cookiesManager = cookies();
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
const authToken = activeAccount
? cookiesManager.get(`${COOKIE_PREFIX_TOKEN}${activeAccount}`)?.value
: null;
if (!authToken) {
// redirect to login page
redirect(`/login?next=${encodeURIComponent("/dashboard/connect/pay")}`);
if (firstKey) {
redirect(`/dashboard/connect/pay/${firstKey.id}`);
}
const apiKeys = await getApiKeys(authToken).catch((err) => {
console.error("failed to load api keys", err);
return [];
});
return (
<div className="flex flex-col gap-8 w-full">
<div className="flex flex-col lg:flex-row gap-6 justify-between items-start">
<div className="max-w-[800px]">
<h1 className="text-3xl lg:text-4xl tracking-tight font-bold mb-3">
Pay
</h1>
<p className="text-muted-foreground leading-7">
Pay allows your users to purchase cryptocurrencies and execute
transactions with their credit card or debit card, or with any token
via cross-chain routing.{" "}
<Link
target="_blank"
href="https://portal.thirdweb.com/connect/pay/overview"
className="!text-link-foreground"
>
Learn more
</Link>
</p>
</div>
</div>
{apiKeys.length > 0 ? (
<PayUI apiKeys={apiKeys} />
) : (
<NoApiKeys
service="Pay in Connect"
buttonTextOverride={apiKeys.length ? "Enable Pay" : undefined}
copyOverride={
apiKeys.length
? "You'll need to enable pay as a service in an API Key to use Pay."
: undefined
}
/>
)}
</div>
);
redirect("/dashboard/connect/pay/no-keys");
}
// because cookies() is used
export const dynamic = "force-dynamic";
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

@@ -0,0 +1,134 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { ContractCard } from "components/explore/contract-card";
import { DeployUpsellCard } from "components/explore/upsells/deploy-your-own";
import { ALL_CATEGORIES, getCategory } from "data/explore";
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
type ExploreCategoryPageProps = {
params: {
category: string;
};
};
export const metadata: Metadata = {
title: "Explore | Smart Contracts",
description:
"Browse a large collection of ready-to-deploy contracts that have been built by thirdweb and other contract developers. Find a contract for your specific app's or game's needs.",
openGraph: {
title: "thirdweb Explore: Smart Contracts & Protocols",
},
};
export async function generateMetadatra(
props: ExploreCategoryPageProps,
): Promise<Metadata> {
const category = getCategory(props.params.category);
if (!category) {
notFound();
}
return {
title: `${category.name} Smart Contracts | Explore`,
description: `${category.description} Deploy with one click to Ethereum, Polygon, Optimism, and other EVM blockchains with thirdweb.`,
};
}
export default async function ExploreCategoryPage(
props: ExploreCategoryPageProps,
) {
const category = getCategory(props.params.category);
if (!category) {
notFound();
}
return (
<div className="flex flex-col">
<Breadcrumb className="py-4 px-6 border-b border-border">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/explore">Explore</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>
{category.displayName || category.name}
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="container py-8 flex flex-col gap-4">
<h1 className="mb-3 text-3xl lg:text-5xl font-bold tracking-tighter">
{category.displayName || category.name}
</h1>
<p className="text-base lg:text-lg text-muted-foreground max-w-screen-md">
{category.description}{" "}
{category.learnMore && (
<Link href={category.learnMore}>Learn more</Link>
)}
</p>
<div className="h-10" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
{category.contracts.map((publishedContractId, idx) => {
const publisher: string = Array.isArray(publishedContractId)
? publishedContractId[0].split("/")[0]
: publishedContractId.split("/")[0];
const contractId: string = Array.isArray(publishedContractId)
? publishedContractId[0].split("/")[1]
: publishedContractId.split("/")[1];
const modules = Array.isArray(publishedContractId)
? publishedContractId[1]
: undefined;
const overrides = Array.isArray(publishedContractId)
? publishedContractId[2]
: undefined;
return (
<ContractCard
key={publisher + contractId + overrides?.title}
publisher={publisher}
contractId={contractId}
titleOverride={overrides?.title}
descriptionOverride={overrides?.description}
tracking={{
source: category.id,
itemIndex: `${idx}`,
}}
isBeta={category.isBeta}
modules={
modules?.length
? modules.map((m) => ({
publisher: m.split("/")[0],
moduleId: m.split("/")[1],
}))
: undefined
}
/>
);
})}
</div>
<div className="h-16" />
{/* TODO: remove this once we update the deploy upsell card */}
<ChakraProviderSetup>
<DeployUpsellCard />
</ChakraProviderSetup>
</div>
</div>
);
}
export async function generateStaticParams() {
return ALL_CATEGORIES.map((category) => ({
params: { category },
}));
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

@@ -0,0 +1,63 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
} from "@/components/ui/breadcrumb";
import { ContractRow } from "components/explore/contract-row";
import { DeployUpsellCard } from "components/explore/upsells/deploy-your-own";
import { PublishUpsellCard } from "components/explore/upsells/publish-submit";
import { EXPLORE_PAGE_DATA } from "data/explore";
import type { Metadata } from "next";
import { Fragment } from "react";
export const metadata: Metadata = {
title: "Explore | Smart Contracts",
description:
"Browse a large collection of ready-to-deploy contracts that have been built by thirdweb and other contract developers. Find a contract for your specific app's or game's needs.",
openGraph: {
title: "thirdweb Explore: Smart Contracts & Protocols",
},
};
export default async function ExplorePage() {
return (
<div className="flex flex-col">
<Breadcrumb className="py-4 px-6 border-b border-border">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbPage>Explore</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="container py-8 flex flex-col gap-4">
<h1 className="mb-3 text-3xl lg:text-5xl font-bold tracking-tighter">
Explore
</h1>
<p className="text-base lg:text-lg text-muted-foreground max-w-screen-md">
The best place for web3 developers to explore smart contracts from
world-class web3 protocols & engineers — all deployable with one
click.
</p>
<div className="h-10" />
<div className="flex flex-col gap-14">
{EXPLORE_PAGE_DATA.map((category, idx) => (
<Fragment key={category.id}>
{Math.floor(EXPLORE_PAGE_DATA.length / 2) === idx && (
<PublishUpsellCard />
)}
<ContractRow category={category} />
</Fragment>
))}
</div>
<div className="h-16" />
{/* TODO: remove this once we update the deploy upsell card */}
<ChakraProviderSetup>
<DeployUpsellCard />
</ChakraProviderSetup>
</div>
</div>
);
}
@@ -1,12 +1,12 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { Separator } from "@/components/ui/separator";
import { thirdwebClient } from "@/constants/client";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { SimpleGrid } from "@chakra-ui/react";
import { fetchPublishedContractVersions } from "components/contract-components/fetch-contracts-with-versions";
import { PublishedContract } from "components/contract-components/published-contract";
import { setOverrides } from "lib/vercel-utils";
import { isAddress } from "thirdweb";
import { resolveAddress } from "thirdweb/extensions/ens";
import { ChakraProviderSetup } from "../../../../../../@/components/ChakraProviderSetup";
import { fetchPublishedContractVersions } from "../../../../../../components/contract-components/fetch-contracts-with-versions";
import { PublishedContract } from "../../../../../../components/contract-components/published-contract";
import { setOverrides } from "../../../../../../lib/vercel-utils";
import { PublishedActions } from "../../../components/contract-actions-published.client";
import { DeployContractHeader } from "../../../components/contract-header";
@@ -34,7 +34,7 @@ export default async function PublishedContractPage(
const publisherAddress = isAddress(props.params.publisher)
? props.params.publisher
: await resolveAddress({
client: thirdwebClient,
client: getThirdwebClient(),
name: mapThirdwebPublisher(props.params.publisher),
});
@@ -1,7 +1,7 @@
import { thirdwebClient } from "@/constants/client";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { fetchPublishedContractVersions } from "components/contract-components/fetch-contracts-with-versions";
import { isAddress } from "thirdweb";
import { resolveAddress } from "thirdweb/extensions/ens";
import { fetchPublishedContractVersions } from "../../../../../../components/contract-components/fetch-contracts-with-versions";
function mapThirdwebPublisher(publisher: string) {
if (publisher === "thirdweb.eth") {
@@ -20,7 +20,7 @@ export async function getPublishedContractsWithPublisherMapping(options: {
const publisherAddress = isAddress(publisher)
? publisher
: await resolveAddress({
client: thirdwebClient,
client: getThirdwebClient(),
name: mapThirdwebPublisher(publisher),
});
@@ -1,5 +1,5 @@
/* eslint-disable @next/next/no-img-element */
import { thirdwebClient } from "@/constants/client";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { ImageResponse } from "next/og";
import { download } from "thirdweb/storage";
@@ -174,6 +174,8 @@ export async function publishedContractOGImageTemplate(params: {
),
).then((res) => res.arrayBuffer());
const client = getThirdwebClient();
const [
inter400,
inter500,
@@ -195,13 +197,13 @@ export async function publishedContractOGImageTemplate(params: {
params.logo
? download({
uri: params.logo,
client: thirdwebClient,
client,
}).then((res) => res.arrayBuffer())
: undefined,
params.publisherAvatar
? download({
uri: params.publisherAvatar,
client: thirdwebClient,
client,
}).then((res) => res.arrayBuffer())
: undefined,
]);
@@ -1,4 +1,4 @@
import { thirdwebClient } from "@/constants/client";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { resolveScheme } from "thirdweb/storage";
export function DeployContractInfo(props: {
@@ -19,7 +19,7 @@ export function DeployContractInfo(props: {
className="size-12"
alt={props.name}
src={resolveScheme({
client: thirdwebClient,
client: getThirdwebClient(),
uri: props.logo,
})}
/>
@@ -1,12 +1,12 @@
import { Separator } from "@/components/ui/separator";
import { thirdwebClient } from "@/constants/client";
import { isAddress } from "thirdweb";
import { fetchDeployMetadata } from "thirdweb/contract";
import { resolveAddress } from "thirdweb/extensions/ens";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import {
fetchPublishedContractVersion,
fetchPublishedContractVersions,
} from "../../../../components/contract-components/fetch-contracts-with-versions";
} from "components/contract-components/fetch-contracts-with-versions";
import { isAddress } from "thirdweb";
import { fetchDeployMetadata } from "thirdweb/contract";
import { resolveAddress } from "thirdweb/extensions/ens";
import { DeployActions } from "./contract-actions-deploy.client";
import { DeployContractHeader } from "./contract-header";
import { DeployFormForUri } from "./uri-based-deploy";
@@ -26,11 +26,12 @@ function mapThirdwebPublisher(publisher: string) {
}
export async function DeployFormForPublishInfo(props: PublishBasedDeployProps) {
const client = getThirdwebClient();
// resolve ENS if required
const publisherAddress = isAddress(props.publisher)
? props.publisher
: await resolveAddress({
client: thirdwebClient,
client,
name: mapThirdwebPublisher(props.publisher),
});
@@ -52,7 +53,7 @@ export async function DeployFormForPublishInfo(props: PublishBasedDeployProps) {
.map((m) => m.publishMetadataUri);
const [contractMetadata, ...fetchedModules] = await Promise.all([
fetchDeployMetadata({
client: thirdwebClient,
client,
// force `ipfs://` prefix
uri: publishedContract.publishMetadataUri.startsWith("ipfs://")
? publishedContract.publishMetadataUri
@@ -60,7 +61,7 @@ export async function DeployFormForPublishInfo(props: PublishBasedDeployProps) {
}).catch(() => null),
...(moduleUris || []).map((uri) =>
fetchDeployMetadata({
client: thirdwebClient,
client,
// force `ipfs://` prefix
uri: uri.startsWith("ipfs://") ? uri : `ipfs://${uri}`,
}).catch(() => null),
@@ -83,6 +84,7 @@ export async function DeployFormForPublishInfo(props: PublishBasedDeployProps) {
<DeployFormForUri
contractMetadata={contractMetadata}
modules={fetchedModules.filter((m) => m !== null)}
pathname={`/${props.publisher}/${props.contract_id}${props.version ? `/${props.version}` : ""}/deploy`}
/>
</>
);
@@ -1,25 +1,42 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { CustomContractForm } from "components/contract-components/contract-deploy-form/custom-contract";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getAddress } from "thirdweb";
import type { FetchDeployMetadataResult } from "thirdweb/contract";
type DeployFormForUriProps = {
contractMetadata: FetchDeployMetadataResult | null;
modules: FetchDeployMetadataResult[] | null;
pathname: string;
};
export async function DeployFormForUri(props: DeployFormForUriProps) {
const { contractMetadata, modules } = props;
const { contractMetadata, modules, pathname } = props;
if (!contractMetadata) {
return <div>Could not fetch metadata</div>;
}
const cookieStore = cookies();
const address = cookieStore.get(COOKIE_ACTIVE_ACCOUNT)?.value;
if (!address) {
redirect(`/login?next=${encodeURIComponent(pathname)}`);
}
const authCookieName = COOKIE_PREFIX_TOKEN + getAddress(address);
const token = cookieStore.get(authCookieName)?.value;
if (!token) {
redirect(`/login?next=${encodeURIComponent(pathname)}`);
}
// TODO: remove the `ChakraProviderSetup` wrapper once the form is updated to no longer use chrakra
return (
<ChakraProviderSetup>
<CustomContractForm
metadata={contractMetadata}
modules={modules?.filter((m) => m !== null)}
jwt={token}
/>
</ChakraProviderSetup>
);
@@ -1,4 +1,4 @@
import { thirdwebClient } from "@/constants/client";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { redirect } from "next/navigation";
import { fetchDeployMetadata } from "thirdweb/contract";
@@ -13,7 +13,7 @@ export default async function Page(props: {
}
const contractMetadata = await fetchDeployMetadata({
client: thirdwebClient,
client: getThirdwebClient(),
// force `ipfs://` prefix
uri: props.searchParams.uri.startsWith("ipfs://")
? props.searchParams.uri
@@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { thirdwebClient } from "@/constants/client";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import type { Abi, AbiFunction } from "abitype";
import { useV5DashboardChain } from "lib/v5-adapter";
import { ArrowDown } from "lucide-react";
@@ -62,6 +62,7 @@ export const TransactionSimulator = (props: {
const chain = useV5DashboardChain(
Number.isInteger(Number(chainId)) ? chainId : undefined,
);
const client = useThirdwebClient();
async function handleSimulation(data: SimulateTransactionForm) {
try {
@@ -71,7 +72,7 @@ export const TransactionSimulator = (props: {
throw new Error("Invalid chainId");
}
const contract = getContract({
client: thirdwebClient,
client,
chain,
address: to,
});
@@ -85,6 +85,7 @@ export function AccountHeaderMobileUI(props: AccountHeaderCompProps) {
<TeamSelectorMobileMenuButton
currentTeam={undefined}
teamsAndProjects={props.teamsAndProjects}
upgradeTeamLink={undefined}
/>
</div>
</div>
@@ -1,5 +1,7 @@
"use client";
import { DangerSettingCard } from "@/components/blocks/DangerSettingCard";
import { SettingsCard } from "@/components/blocks/SettingsCard";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -32,8 +34,6 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { FileInput } from "../../../components/shared/FileInput";
import { DangerSettingCard } from "../../team/[team_slug]/(team)/~/settings/general/DangerSettingCard";
import { SettingsCard } from "../../team/[team_slug]/(team)/~/settings/general/SettingsCard";
type MinimalAccount = Pick<
Account,
@@ -0,0 +1,58 @@
import { COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { cookies } from "next/headers";
import { type NextRequest, NextResponse } from "next/server";
import { getAddress } from "thirdweb/utils";
const THIRDWEB_API_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
export type GetAuthTokenResponse = {
jwt: string | null;
};
function respond(jwt: string | null) {
const responseObj: GetAuthTokenResponse = {
jwt,
};
return NextResponse.json(responseObj);
}
export const GET = async (req: NextRequest) => {
const address = req.nextUrl.searchParams.get("address");
const cookieStore = cookies();
if (!address) {
return NextResponse.json(
{
error: "address is required",
},
{
status: 400,
},
);
}
const authCookieName = COOKIE_PREFIX_TOKEN + getAddress(address);
// check if we have a token for the address
const token = cookieStore.get(authCookieName)?.value;
// no auth token found for the address
if (!token) {
return respond(null);
}
// check token validity
const accountRes = await fetch(`${THIRDWEB_API_HOST}/v1/account/me`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (accountRes.status !== 200) {
return respond(null);
}
return respond(token);
};
+44 -14
View File
@@ -1,27 +1,57 @@
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
import { getAuthToken } from "./getAuthToken";
// TODO - Fix the `/v1/keys/${apiKeyId}` endpoint in API server
export async function getAPIKey(apiKeyId: string) {
// The `/v1/keys/${apiKeyId}`; does not return the "FULL" ApiKey object for some reason
// Until this is fixed in API server - we just use the getApiKeys() and find the key by id
const apiKeys = await getApiKeys();
return apiKeys.find((key) => key.id === apiKeyId);
// const authToken = getAuthToken();
// const apiServerURL = new URL(
// process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com",
// );
// apiServerURL.pathname = `/v1/keys/${apiKeyId}`;
// const res = await fetch(apiServerURL, {
// method: "GET",
// headers: {
// Authorization: `Bearer ${authToken}`,
// },
// });
// const json = await res.json();
// if (json.error) {
// console.error(json.error);
// return undefined;
// }
// return json.data as ApiKey;
}
export async function getApiKeys() {
const authToken = getAuthToken();
const apiServerURL = new URL(
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com",
);
const res = await fetch(
`${process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com"}/v1/keys`,
{
method: "GET",
apiServerURL.pathname = `/v1/keys/${apiKeyId}`;
const res = await fetch(apiServerURL, {
method: "GET",
headers: {
Authorization: `Bearer ${authToken}`,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
},
});
);
const json = await res.json();
if (json.error) {
console.error(json.error);
return undefined;
return [];
}
return json.data as ApiKey;
return json.data as ApiKey[];
}
@@ -3,9 +3,10 @@
// don't know why - but getting compilation error without adding "use client" even though it's already added
// where we import AutoConnect from in thirdweb/react
import { thirdwebClient } from "@/constants/client";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { AutoConnect } from "thirdweb/react";
export function TWAutoConnect() {
return <AutoConnect client={thirdwebClient} />;
const client = useThirdwebClient();
return <AutoConnect client={client} />;
}
@@ -2,7 +2,7 @@
"use client";
import { usePathname, useSearchParams } from "next/navigation";
import { usePostHog } from "posthog-js-opensource/react";
import { usePostHog } from "posthog-js/react";
import { useEffect } from "react";
export default function PostHogPageView(): null {
@@ -1,7 +1,7 @@
// app/providers.tsx
"use client";
import posthog from "posthog-js-opensource";
import { PostHogProvider as PHProvider } from "posthog-js-opensource/react";
import posthog from "posthog-js";
import { PostHogProvider as PHProvider } from "posthog-js/react";
if (typeof window !== "undefined") {
posthog.init(
+3 -2
View File
@@ -1,7 +1,7 @@
"use client";
import { ColorModeToggle } from "@/components/color-mode-toggle";
import { thirdwebClient } from "@/constants/client";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useTheme } from "next-themes";
import { useSearchParams } from "next/navigation";
@@ -42,6 +42,7 @@ function CustomConnectEmmbed() {
const router = useDashboardRouter();
const { theme } = useTheme();
const nextSearchParam = searchParams?.get("next");
const client = useThirdwebClient();
function onLoginSuccessful() {
if (nextSearchParam && isValidRedirectPath(nextSearchParam)) {
@@ -73,7 +74,7 @@ function CustomConnectEmmbed() {
return isLoggedInResult;
},
}}
client={thirdwebClient}
client={client}
modalSize={isLG ? "wide" : "compact"}
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
/>
@@ -1,6 +1,8 @@
"use client";
import type { Team } from "@/api/team";
import { DangerSettingCard } from "@/components/blocks/DangerSettingCard";
import { SettingsCard } from "@/components/blocks/SettingsCard";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Input } from "@/components/ui/input";
import { useDashboardRouter } from "@/lib/DashboardRouter";
@@ -8,8 +10,6 @@ import { useMutation } from "@tanstack/react-query";
import { FileInput } from "components/shared/FileInput";
import { useState } from "react";
import { toast } from "sonner";
import { DangerSettingCard } from "./DangerSettingCard";
import { SettingsCard } from "./SettingsCard";
export function GeneralSettingsPage(props: {
team: Team;
@@ -13,12 +13,14 @@ import { SmartWalletsBillingAlert } from "components/settings/ApiKeys/Alerts";
import { CircleAlertIcon } from "lucide-react";
import { useMemo } from "react";
import { useActiveWalletChain } from "thirdweb/react";
import { SmartWallets } from "../../../../../../components/smart-wallets";
import { AccountFactories } from "../../../../../../components/smart-wallets/AccountFactories";
import { AAFooterSection } from "./AAFooterSection";
import { isOpChainId } from "./isOpChain";
const TRACKING_CATEGORY = "smart-wallet";
// TODO - the factories shown on this page is not project specific, need to revamp this page
export function AccountAbstractionPage(props: {
apiKeyServices: ApiKeyService[];
}) {
@@ -87,10 +89,7 @@ export function AccountAbstractionPage(props: {
)
)}
<SmartWallets
apiKeyServices={apiKeyServices}
trackingCategory={TRACKING_CATEGORY}
/>
<AccountFactories trackingCategory={TRACKING_CATEGORY} />
</div>
)}
@@ -1,7 +1,7 @@
import { getProject } from "@/api/projects";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { notFound } from "next/navigation";
import { EmbeddedWallets } from "../../../../../../components/embedded-wallets";
import { InAppWalletUsersPageContent } from "../../../../../../components/embedded-wallets/Users";
import { AnalyticsCallout } from "./_components/AnalyticsCallout";
import { InAppWaletFooterSection } from "./_components/footer";
@@ -43,11 +43,8 @@ export default async function Page(props: {
</TrackedLinkTW>
</p>
<EmbeddedWallets
apiKey={{
...project,
key: project.publishableKey, // clientId
}}
<InAppWalletUsersPageContent
clientId={project.publishableKey}
trackingCategory={TRACKING_CATEGORY}
/>
@@ -1,64 +0,0 @@
"use client";
import {} from "@/components/ui/select";
import { TabButtons } from "@/components/ui/tabs";
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
import { PayAnalytics } from "components/pay/PayAnalytics/PayAnalytics";
import { PayConfig } from "components/pay/PayConfig";
import { useState } from "react";
import { WebhooksPage } from "../../../../../(dashboard)/dashboard/connect/pay/components/webhooks.client";
export function PayPageUI(props: {
apiKey: Pick<
ApiKey,
| "key"
| "services"
| "id"
| "name"
| "domains"
| "bundleIds"
| "services"
| "redirectUrls"
>;
}) {
const [activeTab, setActiveTab] = useState<
"settings" | "analytics" | "webhooks"
>("analytics");
return (
<>
<div className="flex-col gap-4 flex lg:flex-row w-full relative">
<TabButtons
containerClassName="w-full"
tabs={[
{
name: "Analytics",
isActive: activeTab === "analytics",
onClick: () => setActiveTab("analytics"),
isEnabled: true,
},
{
name: "Webhooks",
isActive: activeTab === "webhooks",
onClick: () => setActiveTab("webhooks"),
isEnabled: true,
},
{
name: "Settings",
isActive: activeTab === "settings",
onClick: () => setActiveTab("settings"),
isEnabled: true,
},
]}
/>
</div>
{/* TODO: split this into sub-pages */}
{activeTab === "analytics" && (
<PayAnalytics clientId={props.apiKey.key} />
)}
{activeTab === "settings" && <PayConfig apiKey={props.apiKey} />}
{activeTab === "webhooks" && <WebhooksPage clientId={props.apiKey.key} />}
</>
);
}
@@ -0,0 +1,64 @@
import { getProject } from "@/api/projects";
import { TabPathLinks } from "@/components/ui/tabs";
import Link from "next/link";
import { notFound } from "next/navigation";
export default async function Layout(props: {
params: {
team_slug: string;
project_slug: string;
};
children: React.ReactNode;
}) {
const project = await getProject(
props.params.team_slug,
props.params.project_slug,
);
if (!project) {
notFound();
}
const payLayoutPath = `/team/${props.params.team_slug}/${props.params.project_slug}/connect/pay`;
return (
<div className="flex flex-col gap-8 w-full">
<div className="flex flex-col lg:flex-row gap-6 justify-between items-start">
<div className="max-w-[700px]">
<h1 className="text-3xl md:text-4xl tracking-tight font-bold mb-3">
Pay
</h1>
<p className="text-sm md:text-base text-muted-foreground leading-relaxed">
Pay allows your users to purchase cryptocurrencies and execute
transactions with their credit card or debit card, or with any token
via cross-chain routing.{" "}
<Link
target="_blank"
href="https://portal.thirdweb.com/connect/pay/overview"
className="!text-link-foreground"
>
Learn more
</Link>
</p>
</div>
</div>
<TabPathLinks
className="w-full"
links={[
{
name: "Analytics",
path: payLayoutPath,
exactMatch: true,
},
{
name: "Webhooks",
path: `${payLayoutPath}/webhooks`,
},
]}
/>
{props.children}
</div>
);
}
@@ -1,7 +1,6 @@
import { getProject } from "@/api/projects";
import Link from "next/link";
import { notFound } from "next/navigation";
import { PayPageUI } from "./PayPageUI";
import { PayAnalytics } from "../../../../../../components/pay/PayAnalytics/PayAnalytics";
export default async function Page(props: {
params: {
@@ -18,34 +17,5 @@ export default async function Page(props: {
notFound();
}
return (
<div className="flex flex-col gap-8 w-full">
<div className="flex flex-col lg:flex-row gap-6 justify-between items-start">
<div className="max-w-[700px]">
<h1 className="text-3xl md:text-4xl tracking-tight font-bold mb-3">
Pay
</h1>
<p className="text-sm md:text-base text-muted-foreground leading-relaxed">
Pay allows your users to purchase cryptocurrencies and execute
transactions with their credit card or debit card, or with any token
via cross-chain routing.{" "}
<Link
target="_blank"
href="https://portal.thirdweb.com/connect/pay/overview"
className="!text-link-foreground"
>
Learn more
</Link>
</p>
</div>
</div>
<PayPageUI
apiKey={{
...project,
key: project.publishableKey, // clientId
}}
/>
</div>
);
return <PayAnalytics clientId={project.publishableKey} />;
}
@@ -0,0 +1,21 @@
import { getProject } from "@/api/projects";
import { notFound } from "next/navigation";
import { PayWebhooksPage } from "../../../../../../(dashboard)/dashboard/connect/pay/components/webhooks.client";
export default async function Page(props: {
params: {
team_slug: string;
project_slug: string;
};
}) {
const project = await getProject(
props.params.team_slug,
props.params.project_slug,
);
if (!project) {
notFound();
}
return <PayWebhooksPage clientId={project.publishableKey} />;
}
@@ -107,10 +107,9 @@ const DeployOptions = () => {
tabClassName="font-medium !text-sm"
/>
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
<div
<button
className="bg-muted/50 p-4 hover:bg-muted cursor-pointer mt-3 rounded-lg flex items-center gap-2 border border-border"
role="button"
type="button"
onClick={() => {
activeTabContent.onClick?.();
if (activeTabContent.href) {
@@ -142,7 +141,7 @@ const DeployOptions = () => {
{activeTabContent.description}
</p>
</div>
</div>
</button>
</>
);
};
@@ -20,7 +20,7 @@ export default function Page() {
}
return (
<div className="pb-10 pt-6">
<div className="pb-10 pt-10">
{!hasContracts ? (
<GetStartedWithContractsDeploy />
) : (
@@ -0,0 +1,90 @@
import type { UpdateKeyInput } from "@3rdweb-sdk/react/hooks/useApi";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { Toaster } from "sonner";
import { createApiKeyStub } from "../../../../../stories/stubs";
import { mobileViewport } from "../../../../../stories/utils";
import { ProjectGeneralSettingsPageUI } from "./ProjectGeneralSettingsPage";
const meta = {
title: "Project/Settings/General",
component: Story,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {
wording: "project",
},
};
export const Mobile: Story = {
args: {
wording: "project",
},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
export const DesktopAPIKeyWording: Story = {
args: {
wording: "api-key",
},
};
export const MobileAPIKeyWording: Story = {
args: {
wording: "api-key",
},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const apiKeyStub = createApiKeyStub();
apiKeyStub.secret = undefined;
function Story(props: {
wording: "project" | "api-key";
}) {
const updateMutation = useMutation({
mutationFn: async (inputs: UpdateKeyInput) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("update with", inputs);
},
});
const deleteMutation = useMutation({
mutationFn: async (id: string) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("delete with", id);
},
});
return (
<div className="max-w-[1100px] py-6 px-4 w-full mx-auto">
<ProjectGeneralSettingsPageUI
apiKey={apiKeyStub}
wording={props.wording}
updateMutation={updateMutation}
deleteMutation={deleteMutation}
paths={{
aaConfig: "/aaConfig",
afterDeleteRedirectTo: "/afterDeleteRedirectTo",
inAppConfig: "/inAppConfig",
payConfig: "/payConfig",
}}
onKeyUpdated={undefined}
/>
<Toaster richColors />
</div>
);
}
@@ -0,0 +1,746 @@
"use client";
import { DangerSettingCard } from "@/components/blocks/DangerSettingCard";
import { SettingsCard } from "@/components/blocks/SettingsCard";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { DynamicHeight } from "@/components/ui/DynamicHeight";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ApiKey, UpdateKeyInput } from "@3rdweb-sdk/react/hooks/useApi";
import {
useRevokeApiKey,
useUpdateApiKey,
} from "@3rdweb-sdk/react/hooks/useApi";
import { zodResolver } from "@hookform/resolvers/zod";
import type { UseMutationResult } from "@tanstack/react-query";
import { SERVICES } from "@thirdweb-dev/service-utils";
import {
type ServiceName,
getServiceByName,
} from "@thirdweb-dev/service-utils";
import { format } from "date-fns";
import { useTrack } from "hooks/analytics/useTrack";
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { type UseFormReturn, useForm } from "react-hook-form";
import { type FieldArrayWithId, useFieldArray } from "react-hook-form";
import { toast } from "sonner";
import { joinWithComma, toArrFromList } from "utils/string";
import {
type ApiKeyValidationSchema,
HIDDEN_SERVICES,
apiKeyValidationSchema,
} from "../../../../../components/settings/ApiKeys/validations";
type EditProjectUIPaths = {
inAppConfig: string;
aaConfig: string;
payConfig: string;
afterDeleteRedirectTo: string;
};
export function ProjectGeneralSettingsPage(props: {
apiKey: ApiKey;
paths: EditProjectUIPaths;
onKeyUpdated: (() => void) | undefined;
wording: "project" | "api-key";
}) {
const updateMutation = useUpdateApiKey();
const deleteMutation = useRevokeApiKey();
return (
<ProjectGeneralSettingsPageUI
apiKey={props.apiKey}
wording={props.wording}
updateMutation={updateMutation}
deleteMutation={deleteMutation}
paths={props.paths}
onKeyUpdated={props.onKeyUpdated}
/>
);
}
type UpdateMutation = UseMutationResult<
unknown,
unknown,
UpdateKeyInput,
unknown
>;
type DeleteMutation = UseMutationResult<unknown, unknown, string, unknown>;
interface EditApiKeyProps {
apiKey: ApiKey;
wording: "project" | "api-key";
updateMutation: UpdateMutation;
deleteMutation: DeleteMutation;
paths: EditProjectUIPaths;
onKeyUpdated: (() => void) | undefined;
}
type UpdateAPIForm = UseFormReturn<ApiKeyValidationSchema>;
export const ProjectGeneralSettingsPageUI: React.FC<EditApiKeyProps> = (
props,
) => {
const { apiKey, wording, updateMutation, deleteMutation } = props;
const trackEvent = useTrack();
const router = useDashboardRouter();
const form = useForm<ApiKeyValidationSchema>({
resolver: zodResolver(apiKeyValidationSchema),
defaultValues: {
name: apiKey.name,
domains: joinWithComma(apiKey.domains),
bundleIds: joinWithComma(apiKey.bundleIds),
redirectUrls: joinWithComma(apiKey.redirectUrls),
services: SERVICES.map((srv) => {
const existingService = (apiKey.services || []).find(
(s) => s.name === srv.name,
);
return {
name: srv.name,
targetAddresses: existingService
? joinWithComma(existingService.targetAddresses)
: "",
enabled: !!existingService,
actions: existingService?.actions || [],
recoveryShareManagement: existingService?.recoveryShareManagement,
customAuthentication: existingService?.customAuthentication,
customAuthEndpoint: existingService?.customAuthEndpoint,
applicationName: existingService?.applicationName,
applicationImageUrl: existingService?.applicationImageUrl,
};
}),
},
});
const handleSubmit = form.handleSubmit((values) => {
const enabledServices = (values.services || []).filter(
(srv) => !!srv.enabled,
);
if (enabledServices.length > 0) {
// validate embedded wallets custom auth
const embeddedWallets = enabledServices.find(
(s) => s.name === "embeddedWallets",
);
if (embeddedWallets) {
const { customAuthentication, recoveryShareManagement } =
embeddedWallets;
if (
recoveryShareManagement === "USER_MANAGED" &&
(!customAuthentication?.aud.length ||
!customAuthentication?.jwksUri.length)
) {
return toast.error("Custom JSON Web Token configuration is invalid", {
description:
"To use In-App Wallets with Custom JSON Web Token, provide JWKS URI and AUD.",
});
}
}
const formattedValues = {
id: apiKey.id,
name: values.name,
domains: toArrFromList(values.domains),
bundleIds: toArrFromList(values.bundleIds),
redirectUrls: toArrFromList(values.redirectUrls, true),
services: (values.services || [])
.filter((srv) => srv.enabled)
// FIXME: Not yet supported, add when it is
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.map(({ recoveryShareManagement, ...srv }) => ({
...srv,
targetAddresses: toArrFromList(srv.targetAddresses),
})),
};
trackEvent({
category: "api-keys",
action: "edit",
label: "attempt",
});
updateMutation.mutate(formattedValues, {
onSuccess: () => {
toast.success(
`${wording === "project" ? "Project" : "API Key"} updated successfully`,
);
trackEvent({
category: "api-keys",
action: "edit",
label: "success",
});
props.onKeyUpdated?.();
},
onError: (err) => {
toast.error(
`Failed to update ${wording === "project" ? "Project" : "API Key"}`,
);
trackEvent({
category: "api-keys",
action: "edit",
label: "error",
error: err,
});
},
});
} else {
toast.error("Service not selected", {
description: "Choose at least one service",
});
}
});
return (
<Form {...form}>
<form
onSubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
autoComplete="off"
>
<div className="flex flex-col gap-8">
<ProjectNameSetting
form={form}
updateMutation={updateMutation}
handleSubmit={handleSubmit}
wording={wording}
/>
<APIKeyDetails apiKey={apiKey} />
<AllowedDomainsSetting
form={form}
updateMutation={updateMutation}
handleSubmit={handleSubmit}
wording={wording}
/>
<AllowedBundleIDsSetting
form={form}
updateMutation={updateMutation}
handleSubmit={handleSubmit}
wording={wording}
/>
<EnabledServicesSetting
form={form}
apiKey={apiKey}
updateMutation={updateMutation}
handleSubmit={handleSubmit}
wording={wording}
paths={props.paths}
/>
<DeleteProject
id={apiKey.id}
name={apiKey.name}
deleteMutation={deleteMutation}
onDeleteSuccessful={() => {
router.replace(props.paths.afterDeleteRedirectTo);
}}
wording={wording}
/>
</div>
</form>
</Form>
);
};
function ProjectNameSetting(props: {
form: UpdateAPIForm;
updateMutation: UpdateMutation;
handleSubmit: () => void;
wording: "project" | "api-key";
}) {
const { form, updateMutation, handleSubmit, wording } = props;
const isNameDirty = form.getFieldState("name").isDirty;
return (
<SettingsCard
header={{
title: wording === "project" ? "Project Name" : "Display Name",
description: `Assign a name to identify your ${
wording === "project" ? "project" : "API Key"
} on thirdweb dashboard`,
}}
noPermissionText={undefined}
errorText={form.getFieldState("name").error?.message}
saveButton={{
onClick: handleSubmit,
disabled: !isNameDirty,
isLoading: updateMutation.isPending && isNameDirty,
}}
bottomText="Please use 64 characters at maximum"
>
<Input
autoFocus
placeholder={wording === "project" ? "My Project" : "My API Key"}
type="text"
{...form.register("name")}
className="bg-background max-w-[350px]"
/>
</SettingsCard>
);
}
function AllowedDomainsSetting(props: {
form: UpdateAPIForm;
updateMutation: UpdateMutation;
handleSubmit: () => void;
wording: "project" | "api-key";
}) {
const { form, handleSubmit, updateMutation } = props;
const isDomainsDirty = form.getFieldState("domains").isDirty;
const helperText = (
<ul className="list-disc text-sm text-muted-foreground pl-3 [&>li]:pl-1 flex flex-col gap-1.5 py-1">
<li>
Authorize all domains with{" "}
<span className="bg-muted text-xs font-mono inline-block px-2 rounded">
*
</span>
{". "}
<span>
Example:{" "}
<span className="bg-muted text-xs font-mono inline-block px-2 rounded">
*.thirdweb.com
</span>{" "}
accepts all{" "}
<span className="bg-muted text-xs font-mono inline-block px-2 rounded">
.thirdweb.com
</span>{" "}
sites
</span>
</li>
<li>
Authorize localhost URLs with{" "}
<span className="bg-muted text-xs font-mono inline-block px-2 rounded">
{"localhost:<port>"}
</span>
</li>
<li>Enter domains separated by commas or new lines</li>
</ul>
);
return (
<SettingsCard
header={{
title: "Domain Restrictions",
description:
"Only allow Client ID to be used on specific domains to prevent unauthorized use",
}}
noPermissionText={undefined}
errorText={form.getFieldState("domains", form.formState).error?.message}
saveButton={{
onClick: handleSubmit,
disabled: !isDomainsDirty,
isLoading: updateMutation.isPending && isDomainsDirty,
}}
bottomText={"This is only applicable for web applications"}
>
<div className="flex flex-col gap-6">
<div className="relative">
<Label htmlFor="domains" className="inline-block mb-2">
Allowed Domains
</Label>
<CheckboxWithLabel className="absolute top-0 right-0">
<Checkbox
checked={form.watch("domains") === "*"}
onCheckedChange={(v) => {
form.setValue("domains", v ? "*" : "", {
shouldDirty: true,
});
}}
/>
All Domains
</CheckboxWithLabel>
<Textarea
placeholder="thirdweb.com, rpc.example.com, localhost:3000"
{...form.register("domains")}
/>
</div>
{helperText}
{!form.watch("domains") && (
<Alert variant="warning">
<AlertTitle className="text-sm">No Domains Configured</AlertTitle>
<AlertDescription>
This will deny requests from all origins, rendering the key
unusable in frontend applications. <br /> Proceed only if you
intend to use this key in server or native apps environments
</AlertDescription>
</Alert>
)}
{form.watch("domains") === "*" && (
<Alert variant="warning">
<AlertTitle className="text-sm">Unrestricted Web Access</AlertTitle>
<AlertDescription>
Requests from all origins will be authorized. Your key can be
misused by other websites
</AlertDescription>
</Alert>
)}
</div>
</SettingsCard>
);
}
function AllowedBundleIDsSetting(props: {
form: UpdateAPIForm;
updateMutation: UpdateMutation;
handleSubmit: () => void;
wording: "project" | "api-key";
}) {
const { form, handleSubmit, updateMutation } = props;
const isBundleIdsDirty = form.getFieldState("bundleIds").isDirty;
return (
<SettingsCard
saveButton={{
onClick: handleSubmit,
disabled: !isBundleIdsDirty,
isLoading: updateMutation.isPending && isBundleIdsDirty,
}}
noPermissionText={undefined}
header={{
title: "Bundle ID Restrictions",
description:
"Only allow Client ID to be used on specific Bundle IDs to prevent unauthorized use",
}}
bottomText="This is only applicable for Native games or Native applications"
errorText={form.getFieldState("bundleIds", form.formState).error?.message}
>
<div className="flex flex-col gap-4">
<div className="relative ">
<CheckboxWithLabel className="absolute top-0 right-0">
<Checkbox
checked={form.watch("bundleIds") === "*"}
onCheckedChange={(checked) => {
form.setValue("bundleIds", checked ? "*" : "", {
shouldDirty: true,
});
}}
/>
All Bundle IDs
</CheckboxWithLabel>
<Label className="inline-block mb-2">Allowed Bundle IDs</Label>
<Textarea
placeholder="com.thirdweb.app"
{...form.register("bundleIds")}
/>
</div>
<p className="text-muted-foreground text-sm ">
Enter bundle ids separated by commas or new lines.
</p>
{!form.watch("bundleIds") && (
<Alert variant="warning">
<AlertTitle className="text-sm">
No Bundle IDs Configured
</AlertTitle>
<AlertDescription>
This will deny requests from all native applications, rendering
the key unusable. Proceed only if you intend to use this key in
server or frontend environments.
</AlertDescription>
</Alert>
)}
{form.watch("bundleIds") === "*" && (
<Alert variant="warning">
<AlertTitle className="text-sm">Unrestricted App Access</AlertTitle>
<AlertDescription>
Requests from all applications will be authorized. If your key is
leaked it could be misused.
</AlertDescription>
</Alert>
)}
</div>
</SettingsCard>
);
}
function EnabledServicesSetting(props: {
form: UpdateAPIForm;
updateMutation: UpdateMutation;
handleSubmit: () => void;
wording: "project" | "api-key";
apiKey: ApiKey;
paths: EditApiKeyProps["paths"];
}) {
const { form, handleSubmit, updateMutation, wording } = props;
const { fields, update } = useFieldArray({
control: form.control,
name: "services",
});
const handleAction = (
srvIdx: number,
srv: FieldArrayWithId<ApiKeyValidationSchema, "services", "id">,
actionName: string,
checked: boolean,
) => {
const actions = checked
? [...(srv.actions || []), actionName]
: (srv.actions || []).filter((a) => a !== actionName);
update(srvIdx, {
...srv,
actions,
});
};
return (
<SettingsCard
header={{
title: "Enabled Services",
description: `thirdweb services enabled for this ${
wording === "project" ? "project" : "API Key"
}`,
}}
noPermissionText={undefined}
errorText={undefined}
saveButton={{
onClick: handleSubmit,
disabled: !form.formState.isDirty,
isLoading: updateMutation.isPending,
}}
bottomText=""
>
<DynamicHeight>
<div className="flex flex-col">
{fields.map((srv, idx) => {
const service = getServiceByName(srv.name as ServiceName);
const hidden = HIDDEN_SERVICES.includes(service.name);
const serviceName = getServiceByName(service.name as ServiceName);
const shouldShow = !hidden && serviceName;
if (!shouldShow) {
return null;
}
let configurationLink: string | undefined;
if (service.name === "embeddedWallets" && srv.enabled) {
configurationLink = props.paths.inAppConfig;
} else if (service.name === "bundler" && srv.enabled) {
configurationLink = props.paths.aaConfig;
} else if (service.name === "pay") {
configurationLink = props.paths.payConfig;
}
return (
<div
key={srv.name}
className="border-t py-5 border-border flex items-start gap-6 justify-between"
>
{/* Left */}
<div className="flex flex-col gap-4">
<div>
<h4 className="text-base font-semibold">{service.title}</h4>
<p className="text-muted-foreground text-sm">
{service.description}
</p>
</div>
{configurationLink && (
<div>
<Button
asChild
size="sm"
variant="outline"
className="min-w-32 gap-2 justify-between"
>
<Link href={configurationLink}>
Configure
<ExternalLinkIcon className="size-3 text-muted-foreground" />
</Link>
</Button>
</div>
)}
{service.actions.length > 0 && (
<div className="flex gap-4">
{service.actions.map((sa) => (
<ToolTipLabel key={sa.name} label={sa.description}>
<div>
<CheckboxWithLabel>
<Checkbox
checked={srv.actions.includes(sa.name)}
onCheckedChange={(checked) =>
handleAction(idx, srv, sa.name, !!checked)
}
/>
{sa.title}
</CheckboxWithLabel>
</div>
</ToolTipLabel>
))}
</div>
)}
</div>
{/* Right */}
<Switch
checked={srv.enabled}
onCheckedChange={(v) =>
update(idx, {
...srv,
enabled: !!v,
})
}
/>
</div>
);
})}
</div>
</DynamicHeight>
</SettingsCard>
);
}
function APIKeyDetails({
apiKey,
}: {
apiKey: ApiKey;
}) {
const { createdAt, updatedAt, lastAccessedAt } = apiKey;
return (
<div className="border border-border bg-muted/50 rounded-lg py-6 px-4 lg:px-6 flex flex-col gap-6">
<div>
<h3>Client ID</h3>
<p className="text-muted-foreground text-sm mb-2">
Identifies your application.
</p>
<CopyTextButton
textToCopy={apiKey.key}
className="truncate w-full justify-between font-mono px-3 !h-auto py-3 bg-card max-w-[350px]"
textToShow={apiKey.key}
copyIconPosition="right"
tooltip="Copy Client ID"
/>
</div>
{/* NOTE: for very old api keys the secret might be `null`, if that's the case we skip it */}
{apiKey.secretMasked && (
<div>
<h3>Secret Key</h3>
<p className="text-muted-foreground text-sm mb-2">
Identifies and authenticates your application from a backend. <br />{" "}
This is not the full secret key, Refer to your saved secret key at
the time of creation for the full secret key.
</p>
<div className="px-4 py-3 border border-border rounded-lg font-mono text-sm bg-card max-w-[350px]">
{apiKey.secretMasked}
</div>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<TimeInfo label="Created" date={createdAt} />
<TimeInfo label="Last Updated" date={updatedAt} />
<TimeInfo label="Last Accessed" date={lastAccessedAt} />
</div>
</div>
);
}
function TimeInfo(props: {
label: string;
date: string | undefined;
}) {
return (
<div>
<p> {props.label}</p>
<p className="text-muted-foreground text-sm">
{props.date ? format(new Date(props.date), "MMMM dd, yyyy") : "Never"}
</p>
</div>
);
}
function DeleteProject(props: {
id: string;
name: string;
deleteMutation: UseMutationResult<unknown, unknown, string, unknown>;
wording: "project" | "api-key";
onDeleteSuccessful: () => void;
}) {
const { id, name, deleteMutation, wording, onDeleteSuccessful } = props;
const trackEvent = useTrack();
const handleRevoke = () => {
trackEvent({
category: "api-keys",
action: "revoke",
label: "attempt",
});
deleteMutation.mutate(id, {
onSuccess: () => {
toast.success(
`${wording === "api-key" ? "API Key" : "Project"} deleted successfully`,
);
onDeleteSuccessful();
trackEvent({
category: "api-keys",
action: "revoke",
label: "success",
});
},
onError: (err) => {
// onError(err);
toast.error(
`Failed to delete ${wording === "project" ? "Project" : "API Key"}`,
);
trackEvent({
category: "api-keys",
action: "revoke",
label: "error",
error: err,
});
},
});
};
const description =
"The associated Client ID and Secret Key will not able to access thirdweb services after deletion. This action is irreversible";
return (
<DangerSettingCard
buttonOnClick={() => handleRevoke()}
buttonLabel={wording === "project" ? "Delete project" : "Delete key"}
confirmationDialog={{
title: `Delete ${
wording === "api-key" ? "API Key" : "Project"
} "${name}"?`,
description: description,
}}
description={description}
isLoading={deleteMutation.isPending}
title={wording === "project" ? "Delete Project" : "Delete API Key"}
/>
);
}
@@ -0,0 +1,31 @@
"use client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
import { ProjectGeneralSettingsPage } from "./ProjectGeneralSettingsPage";
export function ProjectGeneralSettingsPageForTeams(props: {
team_slug: string;
project_slug: string;
apiKey: ApiKey;
}) {
const router = useDashboardRouter();
const { team_slug, project_slug, apiKey } = props;
const projectSettingsLayout = `/team/${team_slug}/${project_slug}/settings`;
return (
<ProjectGeneralSettingsPage
apiKey={apiKey}
paths={{
aaConfig: `${projectSettingsLayout}/account-abstraction`,
inAppConfig: `${projectSettingsLayout}/in-app-wallets`,
payConfig: `${projectSettingsLayout}/pay`,
afterDeleteRedirectTo: `/team/${team_slug}`,
}}
onKeyUpdated={() => {
router.refresh();
}}
wording="project"
/>
);
}
@@ -1,9 +1,32 @@
export default function Page() {
import { getProject } from "@/api/projects";
import { notFound } from "next/navigation";
import { ChakraProviderSetup } from "../../../../../../@/components/ChakraProviderSetup";
import { AccountAbstractionSettingsPage } from "../../../../../../components/smart-wallets/SponsorshipPolicies";
import { getAPIKey } from "../../../../../api/lib/getAPIKeys";
export default async function Page(props: {
params: { team_slug: string; project_slug: string };
}) {
const { team_slug, project_slug } = props.params;
const project = await getProject(team_slug, project_slug);
if (!project) {
notFound();
}
// THIS IS A WORKAROUND - project does not have `services` info - so we fetch APIKey object.
const apiKey = await getAPIKey(project.id);
if (!apiKey) {
notFound();
}
return (
<div className="grow flex flex-col items-center justify-center">
<h1 className="text-4xl tracking-tighter text-muted-foreground">
Account Abstraction Settings
</h1>
</div>
<ChakraProviderSetup>
<AccountAbstractionSettingsPage
apiKeyServices={apiKey.services || []}
trackingCategory="account-abstraction-project-settings"
/>
</ChakraProviderSetup>
);
}
@@ -1,9 +1,29 @@
export default function Page() {
import { getProject } from "@/api/projects";
import { notFound } from "next/navigation";
import { InAppWalletSettingsPage } from "../../../../../../components/embedded-wallets/Configure";
import { getAPIKey } from "../../../../../api/lib/getAPIKeys";
export default async function Page(props: {
params: { team_slug: string; project_slug: string };
}) {
const { team_slug, project_slug } = props.params;
const project = await getProject(team_slug, project_slug);
if (!project) {
notFound();
}
// THIS IS A WORKAROUND - project does not have `services` info - so we fetch APIKey object.
const apiKey = await getAPIKey(project.id);
if (!apiKey) {
notFound();
}
return (
<div className="grow flex flex-col items-center justify-center">
<h1 className="text-4xl tracking-tighter text-muted-foreground">
In-App wallet Settings
</h1>
</div>
<InAppWalletSettingsPage
apiKey={apiKey}
trackingCategory="in-app-wallet-project-settings"
/>
);
}
@@ -11,28 +11,37 @@ export default function Layout(props: {
const layoutPath = `/team/${team_slug}/${project_slug}/settings`;
return (
<SidebarLayout
sidebarLinks={[
{
label: "General",
href: `${layoutPath}`,
exactMatch: true,
},
{
label: "In-App Wallets",
href: `${layoutPath}/in-app-wallets`,
},
{
label: "Account Abstraction",
href: `${layoutPath}/account-abstraction`,
},
{
label: "Pay",
href: `${layoutPath}/pay`,
},
]}
>
{props.children}
</SidebarLayout>
<div className="grow flex flex-col">
<div className="py-10 border-b border-border">
<div className="container">
<h1 className="text-3xl tracking-tight font-semibold">
Project Settings
</h1>
</div>
</div>
<SidebarLayout
sidebarLinks={[
{
label: "General",
href: `${layoutPath}`,
exactMatch: true,
},
{
label: "In-App Wallets",
href: `${layoutPath}/in-app-wallets`,
},
{
label: "Account Abstraction",
href: `${layoutPath}/account-abstraction`,
},
{
label: "Pay",
href: `${layoutPath}/pay`,
},
]}
>
{props.children}
</SidebarLayout>
</div>
);
}
@@ -1,9 +1,30 @@
export default function Page() {
import { getProject } from "@/api/projects";
import { notFound } from "next/navigation";
import { getAPIKey } from "../../../../api/lib/getAPIKeys";
import { ProjectGeneralSettingsPageForTeams } from "./ProjectGeneralSettingsPageForTeams";
export default async function Page(props: {
params: { team_slug: string; project_slug: string };
}) {
const { team_slug, project_slug } = props.params;
const project = await getProject(team_slug, project_slug);
if (!project) {
notFound();
}
// THIS IS A WORKAROUND - project does not have `services` info - so we fetch APIKey object.
const apiKey = await getAPIKey(project.id);
if (!apiKey) {
notFound();
}
return (
<div className="grow flex flex-col items-center justify-center">
<h1 className="text-4xl tracking-tighter text-muted-foreground">
General Settings
</h1>
</div>
<ProjectGeneralSettingsPageForTeams
apiKey={apiKey}
project_slug={project_slug}
team_slug={team_slug}
/>
);
}
@@ -1,9 +1,27 @@
export default function Page() {
return (
<div className="grow flex flex-col items-center justify-center">
<h1 className="text-4xl tracking-tighter text-muted-foreground">
Pay Settings
</h1>
</div>
);
import { getProject } from "@/api/projects";
import { notFound } from "next/navigation";
import { PayConfig } from "../../../../../../components/pay/PayConfig";
import { getAPIKey } from "../../../../../api/lib/getAPIKeys";
export default async function Page(props: {
params: {
team_slug: string;
project_slug: string;
};
}) {
const { team_slug, project_slug } = props.params;
const project = await getProject(team_slug, project_slug);
if (!project) {
notFound();
}
// THIS IS A WORKAROUND - project does not have `services` info - so we fetch APIKey object.
const apiKey = await getAPIKey(project.id);
if (!apiKey) {
notFound();
}
return <PayConfig apiKey={apiKey} />;
}
@@ -20,7 +20,9 @@ export function ProjectSelectorUI(props: {
const { projects, currentProject, team } = props;
const [searchProjectTerm, setSearchProjectTerm] = useState("");
const filteredProjects = searchProjectTerm
? projects.filter((project) => project.name.includes(searchProjectTerm))
? projects.filter((project) =>
project.name.toLowerCase().includes(searchProjectTerm.toLowerCase()),
)
: projects;
return (
@@ -78,6 +78,11 @@ export function TeamAndProjectSelectorPopoverButton(props: TeamSwitcherProps) {
currentTeam={currentTeam}
setHoveredTeam={setHoveredTeam}
teamsAndProjects={teamsAndProjects}
upgradeTeamLink={
currentTeam
? `/team/${currentTeam.slug}/~/settings/billing`
: undefined
}
/>
{/* Right */}
@@ -129,6 +129,7 @@ export function TeamHeaderMobileUI(props: TeamHeaderCompProps) {
<TeamSelectorMobileMenuButton
currentTeam={props.currentTeam}
teamsAndProjects={props.teamsAndProjects}
upgradeTeamLink={`/team/${currentTeam.slug}/settings`}
/>
</div>
@@ -15,13 +15,16 @@ export function TeamSelectionUI(props: {
setHoveredTeam: (team: Team | undefined) => void;
currentTeam: Team | undefined;
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
upgradeTeamLink: string | undefined;
}) {
const { setHoveredTeam, currentTeam, teamsAndProjects } = props;
const teamPlan = currentTeam ? getValidTeamPlan(currentTeam) : undefined;
const teams = teamsAndProjects.map((x) => x.team);
const [searchTeamTerm, setSearchTeamTerm] = useState("");
const filteredTeams = searchTeamTerm
? teams.filter((team) => team.name.includes(searchTeamTerm))
? teams.filter((team) =>
team.name.toLowerCase().includes(searchTeamTerm.toLowerCase()),
)
: teams;
return (
@@ -106,16 +109,17 @@ export function TeamSelectionUI(props: {
</ul>
</div>
</ScrollShadow>
{/* TODO - what do we do on this button click? */}
{/* Bottom */}
{teamPlan && teamPlan !== "pro" && (
{teamPlan && teamPlan !== "pro" && props.upgradeTeamLink && (
<div className="p-2 border-t border-border">
<Button
asChild
variant="primary"
className="w-full"
onMouseEnter={() => setHoveredTeam(undefined)}
>
Upgrade Team
<Link href={props.upgradeTeamLink}>Upgrade Team</Link>
</Button>
</div>
)}
@@ -8,12 +8,15 @@ import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { ChevronsUpDownIcon } from "lucide-react";
import { TeamSelectionUI } from "./TeamSelectionUI";
type TeamSwitcherProps = {
type TeamSelectorMobileMenuButtonProps = {
currentTeam: Team | undefined;
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
upgradeTeamLink: string | undefined;
};
export function TeamSelectorMobileMenuButton(props: TeamSwitcherProps) {
export function TeamSelectorMobileMenuButton(
props: TeamSelectorMobileMenuButtonProps,
) {
const { currentTeam, teamsAndProjects } = props;
return (
@@ -39,6 +42,7 @@ export function TeamSelectorMobileMenuButton(props: TeamSwitcherProps) {
currentTeam={currentTeam}
setHoveredTeam={() => {}} // don't care on mobile
teamsAndProjects={teamsAndProjects}
upgradeTeamLink={props.upgradeTeamLink}
/>
</DynamicHeight>
</DialogContent>
@@ -24,9 +24,7 @@ export const CustomToolTip: React.FC<CustomToolTipProps> = ({
<div className="flex flex-col p-3 gap-2 border-none outline-none rounded-lg backdrop-blur-lg bg-[rgba(255,255,255,0.2)] dark:bg-[rgba(0,0,0,0.2)] ">
{payload[0]?.payload?.time && (
<div className="flex flex-col gap-0.5">
<label className="text-xs font-semibold text-foreground">
Date
</label>
<h5 className="text-xs font-semibold text-foreground">Date</h5>
<p className="text-xs">
{new Date(payload[0].payload.time).toLocaleDateString(
undefined,
@@ -36,9 +34,9 @@ export const CustomToolTip: React.FC<CustomToolTipProps> = ({
</div>
)}
<div className="flex flex-col gap-0.5">
<label className="text-xs font-semibold text-foreground">
<h5 className="text-xs font-semibold text-foreground">
{valueLabel}
</label>
</h5>
<p className="text-xs font-mono">
{valueFormatter
? valueFormatter(payload[0].value)
@@ -1,3 +1,5 @@
"use client";
import { DynamicHeight } from "@/components/ui/DynamicHeight";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button as ButtonShadcn } from "@/components/ui/button";
@@ -8,7 +10,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { thirdwebClient } from "@/constants/client";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { cn } from "@/lib/utils";
import { useDashboardEVMChainId } from "@3rdweb-sdk/react";
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
@@ -81,10 +83,11 @@ export const MismatchButton = forwardRef<HTMLButtonElement, ButtonProps>(
const activeWalletChain = useActiveWalletChain();
const [dialog, setDialog] = useState<undefined | "no-funds" | "pay">();
const { theme } = useTheme();
const client = useThirdwebClient();
const evmBalance = useWalletBalance({
address: account?.address,
chain: activeWalletChain,
client: thirdwebClient,
client,
});
const initialFocusRef = useRef<HTMLButtonElement>(null);
@@ -216,7 +219,7 @@ export const MismatchButton = forwardRef<HTMLButtonElement, ButtonProps>(
{dialog === "pay" && (
<PayEmbed
client={thirdwebClient}
client={client}
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
className="!w-auto"
payOptions={{
@@ -488,18 +491,19 @@ const MismatchNotice: React.FC<{
const GetLocalHostTestnetFunds: React.FC = () => {
const address = useActiveAccount()?.address;
const client = useThirdwebClient();
const requestFunds = async () => {
if (!address) {
return toast.error("No active account detected");
}
const faucet = privateKeyToAccount({
privateKey: LOCAL_NODE_PKEY,
client: thirdwebClient,
client,
});
const transaction = prepareTransaction({
to: address,
chain: localhost,
client: thirdwebClient,
client,
value: toWei("10"),
});
const promise = sendTransaction({
@@ -5,6 +5,8 @@ import { Button } from "@/components/ui/button";
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
import {
Accordion,
AccordionButton,
@@ -17,6 +19,8 @@ import {
import { useMutation, useQuery } from "@tanstack/react-query";
import { NetworkSelectorButton } from "components/selects/NetworkSelectorButton";
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 Link from "next/link";
@@ -33,9 +37,6 @@ import {
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
import { upload } from "thirdweb/storage";
import { FormHelperText, FormLabel, Heading, Text } from "tw-components";
import { thirdwebClient } from "../../../@/constants/client";
import { useLoggedInUser } from "../../../@3rdweb-sdk/react/hooks/useLoggedInUser";
import { useTxNotifications } from "../../../hooks/useTxNotifications";
import { useCustomFactoryAbi, useFunctionParamsFromABI } from "../hooks";
import { addContractToMultiChainRegistry } from "../utils";
import { Fieldset } from "./common";
@@ -60,6 +61,7 @@ import { TrustedForwardersFieldset } from "./trusted-forwarders-fieldset";
interface CustomContractFormProps {
metadata: FetchDeployMetadataResult;
jwt: string;
modules?: FetchDeployMetadataResult[];
}
@@ -109,10 +111,20 @@ function checkTwPublisher(publisher: string | undefined) {
}
}
function rewriteTwPublisher(publisher: string | undefined) {
if (checkTwPublisher(publisher)) {
return "deployer.thirdweb.eth";
}
return publisher;
}
export const CustomContractForm: React.FC<CustomContractFormProps> = ({
metadata,
modules,
jwt,
}) => {
const thirdwebClient = useThirdwebClient(jwt);
const activeAccount = useActiveAccount();
const walletChain = useActiveWalletChain();
useLoggedInUser();
@@ -120,6 +132,7 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
"Successfully deployed contract",
"Failed to deploy contract",
);
const trackEvent = useTrack();
const constructorParams =
metadata.abi.find((a) => a.type === "constructor")?.inputs || [];
@@ -182,6 +195,18 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
chainId: walletChain?.id,
},
);
// if _defaultAdmin is not prefilled with activeAccount address with the replaceTemplateValues, do it here
// because _defaultAdmin is hidden in the form
if (
param.name === "_defaultAdmin" &&
param.type === "address" &&
!acc[param.name] &&
activeAccount
) {
acc[param.name] = activeAccount.address;
}
return acc;
},
{} as Record<string, string>,
@@ -454,7 +479,7 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
id="custom-contract-form"
as="form"
onSubmit={form.handleSubmit(async (formData) => {
if (!walletChain?.id || !activeAccount) {
if (!walletChain?.id || !activeAccount || !jwt) {
return;
}
@@ -475,11 +500,37 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
},
];
}
const publisherAnalyticsData = metadata.publisher
? {
publisherAndContractName: `${rewriteTwPublisher(metadata.publisher)}/${metadata.name}`,
}
: {};
trackEvent({
category: "custom-contract",
action: "deploy",
label: "attempt",
...publisherAnalyticsData,
chainId: walletChain.id,
metadataUri: metadata.metadataUri,
});
deployStatusModal.setViewContractLink("");
deployStatusModal.open(steps);
try {
// do the actual deployment
const contractAddr = await deployMutation.mutateAsync(formData);
trackEvent({
category: "custom-contract",
action: "deploy",
label: "success",
...publisherAnalyticsData,
contractAddress: contractAddr,
chainId: walletChain.id,
metadataUri: metadata.metadataUri,
});
deployStatusModal.nextStep();
// if add to dashboard is checked, add the contract to the dashboard
if (formData.addToDashboard) {
@@ -492,14 +543,33 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
activeAccount,
300000n,
);
trackEvent({
category: "custom-contract",
action: "add-to-dashboard",
label: "success",
...publisherAnalyticsData,
contractAddress: contractAddr,
chainId: walletChain.id,
metadataUri: metadata.metadataUri,
});
deployStatusModal.nextStep();
}
deployStatusModal.setViewContractLink(
`/${walletChain.id}/${contractAddr}`,
);
} catch (e) {
onError(e);
console.error("failed to deploy contract", e);
trackEvent({
category: "custom-contract",
action: "error",
label: "success",
...publisherAnalyticsData,
chainId: walletChain.id,
metadataUri: metadata.metadataUri,
error: e,
});
deployStatusModal.close();
}
})}
@@ -672,21 +742,18 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
);
})}
{isModular && (
<>
{modules?.length ? (
<ModularContractDefaultModulesFieldset
form={form}
modules={modules}
isTWPublisher={isTWPublisher}
/>
) : (
<div className="min-h-[250px] flex justify-center items-center">
<Spinner className="size-8" />
</div>
)}
</>
)}
{isModular &&
(modules?.length ? (
<ModularContractDefaultModulesFieldset
form={form}
modules={modules}
isTWPublisher={isTWPublisher}
/>
) : (
<div className="min-h-[250px] flex justify-center items-center">
<Spinner className="size-8" />
</div>
))}
{advancedParams.length > 0 && (
<Accordion allowToggle>
@@ -1,6 +1,5 @@
"use client";
import { thirdwebClient } from "@/constants/client";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect";
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
@@ -32,7 +31,9 @@ import { NetworksFieldset } from "./networks-fieldset";
export function ContractPublishForm(props: {
publishMetadata: FetchDeployMetadataResult;
onPublishSuccess: () => Promise<void>;
jwt: string;
}) {
const client = getThirdwebClient(props.jwt);
useLoggedInUser();
const [customFactoryAbi, setCustomFactoryAbi] = useState<Abi>([]);
const [fieldsetToShow, setFieldsetToShow] = useState<
@@ -254,7 +255,7 @@ export function ContractPublishForm(props: {
const tx = publishContract({
account,
contract: getContractPublisher(thirdwebClient),
contract: getContractPublisher(client),
metadata,
previousMetadata: props.publishMetadata,
});
@@ -1,10 +1,10 @@
import { thirdwebClient } from "@/constants/client";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { ProfileMetadata } from "constants/schemas";
import { type ThirdwebContract, getContract, isAddress } from "thirdweb";
import { polygon } from "thirdweb/chains";
import { isAddress } from "thirdweb";
import { fetchDeployMetadata } from "thirdweb/contract";
import { resolveAddress } from "thirdweb/extensions/ens";
import {
getContractPublisher,
getPublishedContractVersions,
getPublisherProfileUri,
} from "thirdweb/extensions/thirdweb";
@@ -17,25 +17,14 @@ function mapThirdwebPublisher(publisher: string) {
return publisher;
}
let publisherContract: ThirdwebContract;
function getPublisherContract() {
if (!publisherContract) {
publisherContract = getContract({
client: thirdwebClient,
address: "0xf5b896Ddb5146D5dA77efF4efBb3Eae36E300808",
chain: polygon,
});
}
return publisherContract;
}
export async function fetchPublisherProfile(publisherAddress: string) {
const client = getThirdwebClient();
const profileUri = await getPublisherProfileUri({
contract: getPublisherContract(),
contract: getContractPublisher(client),
publisher: isAddress(publisherAddress)
? publisherAddress
: await resolveAddress({
client: thirdwebClient,
client,
name: mapThirdwebPublisher(publisherAddress),
}),
});
@@ -44,7 +33,7 @@ export async function fetchPublisherProfile(publisherAddress: string) {
}
try {
const res = await download({
client: thirdwebClient,
client,
uri: profileUri,
});
return res.json() as Promise<ProfileMetadata>;
@@ -57,12 +46,13 @@ export async function fetchPublishedContractVersions(
publisherAddress: string,
contractId: string,
) {
const client = getThirdwebClient();
const allVersions = await getPublishedContractVersions({
contract: getPublisherContract(),
contract: getContractPublisher(client),
publisher: isAddress(publisherAddress)
? publisherAddress
: await resolveAddress({
client: thirdwebClient,
client,
name: mapThirdwebPublisher(publisherAddress),
}),
contractId: contractId,
@@ -78,7 +68,7 @@ export async function fetchPublishedContractVersions(
const responses = await Promise.allSettled(
sortedVersions.map((v) =>
fetchDeployMetadata({
client: thirdwebClient,
client,
uri: v.publishMetadataUri,
}).then((m) => ({ ...m, ...v })),
),
@@ -1,7 +1,9 @@
import { thirdwebClient } from "@/constants/client";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { queryOptions, useQuery } from "@tanstack/react-query";
import type { Abi } from "abitype";
import { isEnsName, resolveEns } from "lib/ens";
import { useV5DashboardChain } from "lib/v5-adapter";
import { useMemo } from "react";
import { type ThirdwebContract, getContract } from "thirdweb";
import {
@@ -16,7 +18,6 @@ import {
} from "thirdweb/extensions/thirdweb";
import { extractIPFSUri, isAddress } from "thirdweb/utils";
import invariant from "tiny-invariant";
import { useV5DashboardChain } from "../../lib/v5-adapter";
import {
type PublishedContractWithVersion,
fetchPublishedContractVersions,
@@ -48,7 +49,7 @@ async function fetchDeployMetadata(contractId: string) {
return removeUndefinedFromObjectDeep(
await sdkFetchDeployMetadata({
client: thirdwebClient,
client: getThirdwebClient(),
uri: contractIdIpfsHash,
}),
);
@@ -102,6 +103,7 @@ export function useAllVersions(
}
export function usePublishedContractsFromDeploy(contract: ThirdwebContract) {
const client = useThirdwebClient();
return useQuery({
queryKey: [
"published-contracts-from-deploy",
@@ -116,7 +118,7 @@ export function usePublishedContractsFromDeploy(contract: ThirdwebContract) {
}
const publishURIs = await getPublishedUriFromCompilerUri({
contract: getContractPublisher(thirdwebClient),
contract: getContractPublisher(client),
compilerMetadataUri: contractUri,
});
@@ -188,7 +190,7 @@ async function fetchPublishedContracts(address?: string | null) {
invariant(address, "address is not defined");
const tempResult = (
(await getAllPublishedContracts({
contract: getContractPublisher(thirdwebClient),
contract: getContractPublisher(getThirdwebClient()),
publisher: address,
})) || []
).filter((c) => c.contractId);
@@ -286,13 +288,14 @@ export function useContractEvents(abi: Abi) {
export function useCustomFactoryAbi(contractAddress: string, chainId: number) {
const chain = useV5DashboardChain(chainId);
const client = useThirdwebClient();
const contract = useMemo(() => {
return getContract({
client: thirdwebClient,
client,
address: contractAddress,
chain,
});
}, [contractAddress, chain]);
}, [contractAddress, chain, client]);
return useQuery({
queryKey: ["custom-factory-abi", contract],
queryFn: () => resolveContractAbi<Abi>(contract),
@@ -1,5 +1,6 @@
"use client";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import {
Divider,
Flex,
@@ -17,11 +18,11 @@ import { useMemo } from "react";
import { BiPencil } from "react-icons/bi";
import { BsShieldCheck } from "react-icons/bs";
import { VscBook, VscCalendar, VscServer } from "react-icons/vsc";
import type { ThirdwebClient } from "thirdweb";
import { useActiveAccount } from "thirdweb/react";
import { download } from "thirdweb/storage";
import invariant from "tiny-invariant";
import { Card, Heading, Link, LinkButton, Text } from "tw-components";
import { thirdwebClient } from "../../../@/constants/client";
import type { PublishedContractWithVersion } from "../fetch-contracts-with-versions";
import {
usePublishedContractEvents,
@@ -52,6 +53,7 @@ export const PublishedContract: React.FC<PublishedContractProps> = ({
walletOrEns,
}) => {
const address = useActiveAccount()?.address;
const client = useThirdwebClient();
const contractFunctions = usePublishedContractFunctions(publishedContract);
const contractEvents = usePublishedContractEvents(publishedContract);
@@ -66,51 +68,6 @@ export const PublishedContract: React.FC<PublishedContractProps> = ({
const licenses = correctAndUniqueLicenses(publishedContract?.licenses || []);
// const publishedContractName =
// publishedContract?.displayName || publishedContract?.name;
// const extensionNames = useMemo(() => {
// return enabledExtensions.map((ext) => ext.name);
// }, [enabledExtensions]);
// const ogImageUrl = useMemo(
// () =>
// PublishedContractOG.toUrl({
// name: publishedContractName,
// description: contract.description,
// version: contract.version || "latest",
// publisher: publisherEnsOrAddress,
// extension: extensionNames,
// license: licenses,
// publishDate,
// publisherAvatar: publisherProfile.data?.avatar || undefined,
// logo: contract.logo,
// }),
// [
// extensionNames,
// licenses,
// contract.description,
// contract.logo,
// publishedContractName,
// contract.version,
// publishDate,
// publisherEnsOrAddress,
// publisherProfile.data?.avatar,
// ],
// );
// const twitterIntentUrl = useMemo(() => {
// const url = new URL("https://twitter.com/intent/tweet");
// url.searchParams.append(
// "text",
// `Check out this ${publishedContractName} contract on @thirdweb
// Deploy it in one click`,
// );
// url.searchParams.append("url", currentRoute);
// return url.href;
// }, [publishedContractName, currentRoute]);
const sources = useQuery({
queryKey: ["sources", publishedContract.publishMetadataUri],
queryFn: async () => {
@@ -118,7 +75,7 @@ export const PublishedContract: React.FC<PublishedContractProps> = ({
publishedContract.metadata.sources,
"no compilerMetadata sources available",
);
return (await fetchSourceFilesFromMetadata(publishedContract))
return (await fetchSourceFilesFromMetadata(publishedContract, client))
.map((source) => {
return {
...source,
@@ -131,22 +88,6 @@ export const PublishedContract: React.FC<PublishedContractProps> = ({
enabled: !!publishedContract.metadata.sources,
});
// const title = useMemo(() => {
// let clearType = "";
// if (extensionNames.includes("ERC721")) {
// clearType = "ERC721";
// } else if (extensionNames.includes("ERC20")) {
// clearType = "ERC20";
// } else if (extensionNames.includes("ERC1155")) {
// clearType = "ERC1155";
// }
// if (clearType) {
// return `${publishedContractName} - ${clearType} | Published Smart Contract`;
// }
// return `${publishedContractName} | Published Smart Contract`;
// }, [extensionNames, publishedContractName]);
const implementationAddresses =
publishedContract.factoryDeploymentData?.implementationAddresses;
@@ -165,37 +106,6 @@ export const PublishedContract: React.FC<PublishedContractProps> = ({
return (
<>
{/* <NextSeo
title={title}
description={`${contract.description}${
contract.description ? ". " : ""
}Deploy ${publishedContractName} in one click with thirdweb.`}
openGraph={{
title,
url: currentRoute,
images: [
{
url: ogImageUrl.toString(),
width: 1200,
height: 630,
alt: `${publishedContractName} contract on thirdweb`,
},
],
}}
/> */}
{/* Farcaster frames headers */}
{/* <Head>
<meta property="fc:frame" content="vNext" />
<meta property="fc:frame:image" content={ogImageUrl.toString()} />
<meta
property="fc:frame:post_url"
content={`${getAbsoluteUrl()}/api/frame/redirect`}
/>
<meta property="fc:frame:button:1" content="Deploy now" />
<meta name="fc:frame:button:1:action" content="post_redirect" />
</Head> */}
<GridItem colSpan={{ base: 12, md: 9 }}>
<Flex flexDir="column" gap={6}>
{address === publishedContract.publisher && (
@@ -376,6 +286,7 @@ type ContractSource = {
};
async function fetchSourceFilesFromMetadata(
publishedMetadata: ExtendedPublishedContract,
client: ThirdwebClient,
): Promise<ContractSource[]> {
return await Promise.all(
Object.entries(publishedMetadata.metadata.sources).map(
@@ -394,7 +305,7 @@ async function fetchSourceFilesFromMetadata(
(
await download({
uri: `ipfs://${ipfsHash}`,
client: thirdwebClient,
client,
})
).text(),
timeout,
@@ -1,4 +1,6 @@
import { thirdwebClient } from "@/constants/client";
"use client";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import {
Box,
Flex,
@@ -50,6 +52,7 @@ export const EditProfile: React.FC<EditProfileProps> = ({
}) => {
const FORM_ID = useId();
const { isOpen, onOpen, onClose } = useDisclosure();
const client = useThirdwebClient();
const {
register,
@@ -105,13 +108,13 @@ export const EditProfile: React.FC<EditProfileProps> = ({
label: "attempt",
});
const tx = setPublisherProfileUri({
contract: getContractPublisher(thirdwebClient),
contract: getContractPublisher(client),
asyncParams: async () => {
return {
publisher: address,
uri: await upload({
files: [d],
client: thirdwebClient,
client,
}),
};
},
@@ -187,7 +190,7 @@ export const EditProfile: React.FC<EditProfileProps> = ({
setValue={(file) => setValue("avatar", file)}
className="border border-border rounded transition-all"
renderPreview={(fileUrl) => (
<MaskedAvatar w="100%" h="100%" src={fileUrl} />
<MaskedAvatar className="w-full h-full" src={fileUrl} />
)}
/>
</Box>
@@ -1,4 +1,6 @@
import { thirdwebClient } from "@/constants/client";
"use client";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { resolveScheme } from "thirdweb/storage";
import {
MaskedAvatar,
@@ -15,6 +17,7 @@ export const PublisherAvatar: React.FC<PublisherAvatarProps> = ({
isLoading,
...restProps
}) => {
const client = useThirdwebClient();
const ensQuery = useEns(address);
const publisherProfile = usePublisherProfile(
ensQuery.data?.address || undefined,
@@ -26,7 +29,7 @@ export const PublisherAvatar: React.FC<PublisherAvatarProps> = ({
publisherProfile.data?.avatar
? resolveScheme({
uri: publisherProfile.data.avatar,
client: thirdwebClient,
client,
})
: ""
}
@@ -1,6 +1,8 @@
"use client";
import { Badge } from "@/components/ui/badge";
import { SkeletonContainer } from "@/components/ui/skeleton";
import { thirdwebClient } from "@/constants/client";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useDashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
import type { BasicContract } from "contract-ui/types/types";
import { useChainSlug } from "hooks/chains/chainSlug";
@@ -22,9 +24,10 @@ export const AsyncContractNameCell = memo(
({ cell }: AsyncContractNameCellProps) => {
const chainSlug = useChainSlug(cell.chainId);
const chain = useV5DashboardChain(cell.chainId);
const client = useThirdwebClient();
const contract = getContract({
client: thirdwebClient,
client,
address: cell.address,
chain,
});
@@ -61,9 +64,10 @@ interface AsyncContractTypeCellProps {
export const AsyncContractTypeCell = memo(
({ cell }: AsyncContractTypeCellProps) => {
const client = useThirdwebClient();
const chain = useV5DashboardChain(cell.chainId);
const contract = getContract({
client: thirdwebClient,
client,
address: cell.address,
chain,
});
@@ -1,4 +1,6 @@
import { thirdwebClient } from "@/constants/client";
"use client";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { Center, Flex, Skeleton, useBreakpointValue } from "@chakra-ui/react";
import { ChainIcon } from "components/icons/ChainIcon";
import Link from "next/link";
@@ -33,6 +35,7 @@ export const MetadataHeader: React.FC<MetadataHeaderProps> = ({
chain,
externalLinks,
}) => {
const client = useThirdwebClient();
const isMobile = useBreakpointValue({ base: true, md: false });
const cleanedChainName = chain?.name?.replace("Mainnet", "").trim();
const validBlockExplorers = chain?.explorers
@@ -68,7 +71,7 @@ export const MetadataHeader: React.FC<MetadataHeaderProps> = ({
{data?.image ? (
<MediaRenderer
src={data.image}
client={thirdwebClient}
client={client}
alt={data?.name?.toString() || ""}
style={{
position: "absolute",
@@ -3,7 +3,7 @@ import { BadgeContainer, mobileViewport } from "../../stories/utils";
import { StepsCard } from "./StepsCard";
const meta = {
title: "common blocks/StepsCard",
title: "blocks/StepsCard",
component: Component,
parameters: {
layout: "centered",
@@ -38,13 +38,13 @@ import { type UseFormReturn, useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { toArrFromList } from "utils/string";
interface ConfigureProps {
type InAppWalletSettingsPageProps = {
apiKey: Pick<
ApiKey,
"id" | "name" | "domains" | "bundleIds" | "services" | "redirectUrls"
>;
trackingCategory: string;
}
};
const TRACKING_CATEGORY = "embedded-wallet";
@@ -54,7 +54,7 @@ type UpdateAPIKeyTrackingData = {
hasCustomAuthEndpoint: boolean;
};
export const Configure: React.FC<ConfigureProps> = (props) => {
export function InAppWalletSettingsPage(props: InAppWalletSettingsPageProps) {
const { data: dashboardAccount } = useAccount();
const mutation = useUpdateApiKey();
const { trackingCategory } = props;
@@ -109,10 +109,10 @@ export const Configure: React.FC<ConfigureProps> = (props) => {
isUpdating={mutation.isPending}
/>
);
};
}
export const InAppWalletSettingsUI: React.FC<
ConfigureProps & {
InAppWalletSettingsPageProps & {
canEditAdvancedFeatures: boolean;
updateApiKey: (
apiKey: UpdateKeyInput,

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