Compare commits

...
Author SHA1 Message Date
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
e0c641c9cd Version Packages (#5352)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-14 05:57:00 +13:00
a1fc436a92 [SDK] Feature: Adds EIP-1193 Adapter (#5354)
Co-authored-by: gregfromstl <[email protected]>
2024-11-14 04:36:11 +13:00
kien-ngo b30177c518 [Dashboard] Remove tw-components <Badge /> (#5370)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the usage of the `Badge` component across various files, simplifying its props and styling. It also removes the `Badge` component from some files and updates imports accordingly.

### Detailed summary
- Deleted `badge.tsx` file.
- Removed `Badge` imports and usage in several components, replacing them with simpler implementations.
- Updated `Badge` usage to use class names for styling instead of props in multiple components.
- Adjusted the `Badge` component's props to reduce complexity and improve readability.

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

<!-- end pr-codex -->
2024-11-13 02:15:42 +00:00
iuwqyir 040e478ad6 add nebula service definition (#5401)
## Problem solved

https://linear.app/thirdweb/issue/BLOCK-412/add-api-key-scope

Added Nebula service scope to the service definitions and included it in the hidden services list.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces the `nebula` service scope to the project and updates the related configurations and validations accordingly. It also modifies the pull request body check to include the `BLOCK` keyword.

### Detailed summary
- Added `nebula` service scope in `packages/service-utils/src/core/services.ts` with properties: `name`, `title`, and `description`.
- Updated `HIDDEN_SERVICES` in `apps/dashboard/src/components/settings/ApiKeys/validations.ts` to include `nebula`.
- Modified the pull request body check in `.github/workflows/linear.yml` to include `BLOCK`.

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

<!-- end pr-codex -->
2024-11-12 22:51:51 +00:00
ElasticBottle 6771cfed62 Winston/cnct 2311 pro cannot sign transaction with in app wallet (#5390)
## Problem solved

https://linear.app/thirdweb/issue/CNCT-2311/pro-cannot-sign-transaction-with-in-app-wallet

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving the transaction signing process in the `enclave-wallet` by ensuring proper type checks for various transaction properties, specifically handling cases for `bigint` and `number` types.

### Detailed summary
- Updated the handling of `tx.value`, `tx.gas`, `tx.nonce`, `tx.maxPriorityFeePerGas`, and `tx.gasPrice` to check their types before processing.
- Ensured that `tx.value`, `tx.gas`, and `tx.maxPriorityFeePerGas` are only processed if they are of type `bigint`.
- Adjusted the handling of `tx.nonce` to support both `number` and fallback to `eth_getTransactionCount`.
- Maintained a 10% buffer for `tx.gas` calculations.
- Updated the assignment of `transaction.maxFeePerGas` and `transaction.type` based on the presence of `tx.maxFeePerGas`.

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

<!-- end pr-codex -->
2024-11-12 22:15:04 +00:00
MananTank ff188fd49d Replace prism+chakra Code component with shiki+tailwind (#5393)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring code related to code blocks in the UI by replacing the `CodeBlock` component with a new `CodeClient` component and introducing a `PlainTextCodeBlock` for plain text display. It also updates styles and dependencies.

### Detailed summary
- Removed `CodeBlock` component from various files.
- Introduced `CodeClient` component for enhanced code rendering.
- Added `PlainTextCodeBlock` for displaying plain text.
- Updated styles in `globals.css` for dark mode support.
- Added new utility functions for code formatting.
- Updated dependencies in `package.json` and `pnpm-lock.yaml`.

> 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-11-12 21:58:05 +00:00
gregfromstl 512eeb5cf0 fix: unstoppable domains test (#5389)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR modifies the test suite for resolving addresses with `Unstoppable Domain`. It updates the test structure to conditionally run based on the presence of `TW_SECRET_KEY` and retains the existing test logic.

### Detailed summary
- Changed `describe` to conditionally run with `describe.runIf(process.env.TW_SECRET_KEY)`.
- Retained the test for resolving a valid address.
- Retained the test for handling a non-existent domain name.

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

<!-- end pr-codex -->
2024-11-12 20:57:25 +00:00
gregfromstl a1707f66f3 [SDK] Fix: Add JSDoc documentation for getSocialIcon function (#5395)
CNCT-2324

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the documentation for the `getSocialIcon` function, providing a detailed JSDoc comment that explains its purpose, parameters, return value, and includes usage examples.

### Detailed summary
- Added a JSDoc comment for the `getSocialIcon` function.
- Described the function's purpose, parameters, return value, and provided usage examples.
- Changed the return type of `getSocialIcon` to explicitly specify it returns a `string`.

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

<!-- end pr-codex -->
2024-11-12 20:13:54 +00:00
MananTank f31801e790 ignore invalid module search param in deploy page (#5396)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the handling of the `module` parameter in the `searchParams` to support both an array and a string. It ensures that the code correctly processes the `module` regardless of its type.

### Detailed summary
- Updated `module` type in `searchParams` to allow both `string[]` and `string`.
- Modified the logic to handle `searchParams.module` as a string or an array.
- Ensured that `modules` is derived correctly from `moduleParam`, defaulting to an empty array if no valid modules are found.

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

<!-- end pr-codex -->
2024-11-12 19:52:35 +00:00
gregfromstl ef56304195 [SDK] Feature: Adds Ox for Hex and Bytes Utilities (#5366)
CNCT-2296

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the `thirdweb` package to replace internal utility functions with a new library called `ox`, enhancing code efficiency and maintainability.

### Detailed summary
- Removed several utility files related to encoding and hex operations.
- Integrated `ox` library for hex and byte operations.
- Updated tests to reflect changes in utility functions.
- Increased size limits in `.size-limit.json`.
- Modified hash verification logic in `verify-hash.ts`.

> 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-11-12 19:40:31 +00:00
MananTank daae1dce85 Update condition for showing verify email button in faucet claim section (#5394)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the email verification logic for claiming from the faucet in the `FaucetButton` component. It changes the condition to check for both unconfirmed email and the absence of a confirmed email.

### Detailed summary
- Modified the condition to check if `emailConfirmedAt` is falsy and if `unconfirmedEmail` is truthy.
- Removed the previous check for `accountQuery.data.status === "noCustomer"`.

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

<!-- end pr-codex -->
2024-11-12 19:29:20 +00:00
Joaquim VergesandGitHub afb6343198 feat: add lens sepolia, zero testnet, and treasure topaz chains (#5391) 2024-11-13 04:32:55 +13:00
kien-ngo 7d34a8f50c Add Drop page for minting NFTs with partners (#5307)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `Dashboard` layout and functionality for NFT drops, including improved metadata handling, wallet connection, and minting features.

### Detailed summary
- Added `AppFooter` and `ErrorProvider` in `DashboardLayout`.
- Introduced `DropPageData` type in `data.ts`.
- Enhanced `NftMint` component with quantity selection and custom address options.
- Integrated OpenGraph image generation for NFT drops.
- Updated `CustomConnectWallet` to accept `chain` prop.

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

<!-- end pr-codex -->
2024-11-12 14:39:19 +00:00
MananTank 9584885f42 Updated 404 page in dashboard, Add to App router (#5387)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/yNOf1svJ8o3zjO7zQouZ/cb7fcb59-0d12-4072-a426-3c9863302d1f.png)

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on creating a new `NotFound` page component and refactoring the existing 404 page to utilize this new component, improving code organization and reusability.

### Detailed summary
- Added `NotFoundPage` component in `apps/dashboard/src/components/not-found-page.tsx`.
- Created a structured layout for the 404 error message with links.
- Refactored the `PageNotFound` component in `apps/dashboard/src/pages/404.tsx` to use the new `NotFoundPage`.
- Removed unused imports and components related to the old 404 page design.

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

<!-- end pr-codex -->
2024-11-11 22:46:19 +00:00
MananTank c6310e60d5 Move deploy multiple and publish multiple pages to app router + migrate to shadcn/tailwind (#5381)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on the removal of several components and hooks related to contract deployment and publishing, while introducing new pages and components for handling these processes more effectively, including improved loading states and contract metadata handling.

### Detailed summary
- Deleted several files related to contract hooks and components.
- Introduced `DeployMultipleContractsPage` and `PublishMultipleContractsPage` for contract deployment and publishing.
- Added `SanctionedAddressesChecker` to handle blocked wallet addresses.
- Updated `DeployableContractTable` to fetch and display contract metadata.
- Refactored `ContractIdImage` to utilize fetched metadata directly.
- Changed table structure in `DeployableContractTable` to use new UI components.

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

<!-- end pr-codex -->
2024-11-11 20:42:42 +00:00
MananTank dc0ea8a286 Catch fetchDeployMetadata error and redirect to 404 (#5380)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances error handling in the `page.tsx` files for the `publish` and `deploy` routes. It introduces checks to call `notFound()` when metadata cannot be fetched, improving user experience by handling cases where the requested data is unavailable.

### Detailed summary
- In `apps/dashboard/src/app/(dashboard)/contracts/publish/[publish_uri]/page.tsx`:
  - Added a check for `publishMetadataFromUri` and called `notFound()` if it is not present.

- In `apps/dashboard/src/app/(dashboard)/contracts/deploy/[compiler_uri]/page.tsx`:
  - Added a check for `metadata` and called `notFound()` if it is not present.

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

<!-- end pr-codex -->
2024-11-11 18:28:30 +00:00
MananTank e4b40080f4 Remove invalid modules loading spinner in contract deploy page (#5379)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on simplifying the conditional rendering of the `ModularContractDefaultModulesFieldset` component by removing unnecessary nested ternary operators and improving code readability.

### Detailed summary
- Removed nested ternary operator for rendering `ModularContractDefaultModulesFieldset`.
- Simplified the condition to check if `modules` exists and has length greater than 0.
- Streamlined the rendering logic for better readability.

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

<!-- end pr-codex -->
2024-11-11 16:13:04 +00:00
kien-ngo a1b518c676 [Dashboard] Remove tw-components Button from contract action-buttons (#5369)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the button components across various files to standardize the usage of the `Button` component from the `@/components/ui/button` module, updating properties for consistency, and improving the UI elements.

### Detailed summary
- Replaced `Button` imports from `tw-components` with `@/components/ui/button`.
- Changed `colorScheme` to `variant` for button styling.
- Updated `isDisabled` to `disabled` for button state management.
- Added `className="gap-2"` for consistent spacing.
- Modified button content to include icons before text.

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

<!-- end pr-codex -->
2024-11-11 00:09:22 +00:00
kien-ngo 8c1fa1b7b4 [Dashboard] Remove tw-components from claim condition UI (#5368)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving the user interface and functionality of the `claim-conditions` components in the dashboard application. It enhances accessibility, updates styling, and refines the input handling for claim conditions.

### Detailed summary
- Updated `MaxClaimablePerWalletInput` to use `className` for styling and added a `target` attribute to the `Link`.
- Simplified `CustomFormControl` by replacing `Heading` with `FormLabel`.
- Changed `Text` components to `p` tags in `PricePreview` for better semantic structure.
- Refactored `QuantityInputWithUnlimited` to use new UI components and improved props handling.
- Enhanced `ClaimConditionsPhase` layout with Tailwind CSS classes, replacing Chakra UI components.
- Updated button styles and functionalities, including the removal button and editing toggle.
- Improved readability and structure of the phase details display.

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

<!-- end pr-codex -->
2024-11-11 00:09:20 +00:00
kien-ngo 453e45bd52 [Dashboard] Remove tw-components from marketplace components (#5367)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on replacing the `tw-components` library's `Text` and `Heading` components with standard HTML elements like `<p>` and `<div>` to simplify the code structure and improve styling consistency across various components in the dashboard.

### Detailed summary
- Replaced `Text` components with `<p>` in `csv-data-table.tsx`, `ContractDirectListingsPage.tsx`, and `ContractEnglishAuctionsPage.tsx`.
- Changed `Heading` components to `<p>` in multiple files for consistency.
- Updated button variants from `solid` to `default` in `marketplace-table.tsx`.
- Removed unnecessary imports of `Text` and `Heading` from `tw-components` in several files.
- Adjusted styling classes for better alignment and consistency.
- Enhanced layout structure in the `ListingDrawer` and `CreateListingsForm` components.

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

<!-- end pr-codex -->
2024-11-11 00:09:18 +00:00
MananTank 07af647067 Migrate profile page to tailwind and Move to app router and use RSCs (#5364)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on updating the profile management features within the application. It introduces new functionalities for resolving addresses and fetching published contracts while enhancing the user interface for profile editing.

### Detailed summary
- Removed unused files from the project.
- Updated `replaceDeployerAddress` to include a specific address replacement.
- Changed `PlaygroundMenu` to use `useDashboardRouter`.
- Enhanced `getSortedDeployedContracts` to handle mainnet filtering.
- Made `mapThirdwebPublisher` an exportable function.
- Added `resolveAddressAndEns` for address resolution.
- Introduced `PublishedContracts` component to display published contracts.
- Modified `DeployedContracts` to simplify contract listing logic.
- Updated `ProfileUI` to fetch and display publisher profiles.
- Enhanced `EditProfile` component for better profile editing experience.
- Improved `PublisherSocials` to streamline social media links.
- Added loading states and error handling in various components.

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

<!-- end pr-codex -->
2024-11-09 01:44:53 +00:00
gregfromstl 535e1df9a3 [Dashboard] Fix: More Analytics Cleanup (#5365)
CNCT-2294

https://github.com/user-attachments/assets/29e5e8e7-ff03-46ff-aa68-4f8e13fe323f

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refactoring components and improving the organization of analytics-related files. It updates titles in story files, introduces new components, and modifies existing ones to enhance functionality and maintainability.

### Detailed summary
- Updated story titles in `Stat`, `BarChart`, `PieChart`, `EmptyState`, and `CombinedBarChartCard` components.
- Added `InAppWalletStats`, `EcosystemWalletStats`, and `UserOpStats` types for better analytics data handling.
- Refactored `createUserOpStatsStub` to return `UserOpStats[]`.
- Enhanced `BarChart` and `CombinedBarChartCard` to support currency formatting.
- Introduced `EmptyStateCard` component for better UI handling of no data scenarios.
- Removed deprecated `UserOpStatsByChain` type and updated related functions.
- Improved data fetching functions to align with new analytics structure.

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

<!-- end pr-codex -->
2024-11-09 00:54:41 +00:00
jnsdls a9375ce593 update homepage routing and redirects (#5362) 2024-11-08 21:55:41 +00:00
60eec0344d feat: use paymaster for all zk testnet chains (#5358)
Co-authored-by: 0xFirekeeper <[email protected]>
2024-11-09 09:59:39 +13:00
MananTank 2e641f816a Migrate usage page to tailwind, render on server + Improved UI (#5361)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/yNOf1svJ8o3zjO7zQouZ/e0bad265-504c-4eab-811c-ece0607a6852.png)
2024-11-08 20:21:50 +00:00
MananTank c8ad066cf2 Dashboard storybook minor cleanup + fix crashing stories (#5359)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on updating the titles in various story files to follow a consistent naming convention, improving UI component integration, and enhancing the use of the `getThirdwebClient()` function across multiple components.

### Detailed summary
- Updated titles in story files for consistency.
- Modified `StatBreakdownCard` to use a max width.
- Integrated `getThirdwebClient()` in multiple components.
- Adjusted `AccountTeamsUI` and `TeamRow` to accept `client` as a prop.
- Enhanced theme handling in the Storybook preview.

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

<!-- end pr-codex -->
2024-11-08 18:07:30 +00:00
kien-ngo 0bba41446b [Dashboard] Fix padding issue on NFT tab buttons (#5357)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the environment configuration and modifies a component in the dashboard application to enhance functionality and maintainability.

### Detailed summary
- Added `ANALYTICS_SERVICE_URL` to the `.env.example` file.
- Updated `REDIS_URL` in the `.env.example` file.
- Refactored the `ButtonGroup` component in `token-id.tsx` to streamline the JSX structure by removing unnecessary line breaks and attributes.

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

<!-- end pr-codex -->
2024-11-08 09:39:12 +00:00
kien-ngo 6dda1c242e [Dashboard] Fix overflow on team overview page (#5356)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on modifying the layout of the `PieChartCard` component in the `Analytics` section of the dashboard. It changes a `div` class to improve the responsiveness and wrapping behavior of the child elements.

### Detailed summary
- Changed the class of a `div` from `flex-nowrap` to `flex-wrap` to allow child elements to wrap in the `PieChartCard`.

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

<!-- end pr-codex -->
2024-11-08 09:01:21 +00:00
jnsdls 6b1f4184a4 update deps and fix turbopack build? (#5345)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on updating various package dependencies and adjusting import paths in multiple files, ensuring compatibility and improvements in the application.

### Detailed summary
- Updated `@cloudflare/workers-types` to version `4.20241106.0`.
- Adjusted import paths in `engine-list.tsx` and `publish-submit.tsx`.
- Updated several dependencies in `package.json` files across multiple apps:
  - `@aws-sdk` packages to version `3.687.0`.
  - `@tanstack/react-query` to version `5.59.20`.
  - `next` to version `15.0.3`.
  - `eslint-config-next` to version `15.0.3`.
- Modified scripts in `dashboard/package.json` for build optimizations.
- Updated `@next/mdx` to version `15.0.3`.
- Various other minor dependency updates across packages.

> 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-11-08 06:03:18 +00:00
gregfromstl 9910a0230a [Dashboard] Feature: Team Analytics Page (#5353)
CNCT-2291

https://github.com/user-attachments/assets/163c5eb5-c4dc-4d0f-a6aa-099a321148e8

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refactoring components related to analytics in the dashboard. It replaces the `ProjectOverviewHeader` with an `AnalyticsHeader`, updates various components to enhance functionality, and introduces new analytics-related features.

### Detailed summary
- Deleted `Steps.tsx` and `HomeProductCard.tsx`.
- Replaced `ProjectOverviewHeader` with `AnalyticsHeader` in various files.
- Updated `projectStub` function to remove the export.
- Introduced new components for analytics: `UsersChartCard`, `WalletDistributionCard`, `AuthMethodDistributionCard`, `TotalSponsoredCard`, and `UserOpUsageCard`.
- Enhanced `EmptyState` component styling and functionality.
- Adjusted imports to reflect new component paths.
- Added new fetching functions for analytics data: `getWalletConnections`, `getInAppWalletUsage`, `getUserOpUsage`, and `getWalletUsers`.

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

<!-- end pr-codex -->
2024-11-08 05:42:02 +00:00
ElasticBottle 9572db007a [Dashboard]: fix client ID not being passed correctly to in app wallet user content (#5355)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the `Page` component in the `in-app-wallets/users` directory by adding error handling for project retrieval and updating the `clientId` property passed to `InAppWalletUsersPageContent`.

### Detailed summary
- Imported `notFound` from `next/navigation`.
- Fetched the `project` using `getProject` based on `team_slug` and `project_slug`.
- Added a check to call `notFound()` if the `project` is not found.
- Updated `clientId` prop in `InAppWalletUsersPageContent` to use `project.publishableKey`.

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

<!-- end pr-codex -->
2024-11-08 04:15:45 +00:00
gregfromstl 5de541878c [SDK] Feature: Include chain ID in analytics events (#5343)
CNCT-2138

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on adding the `chainId` to various components and functions within the `thirdweb` package, enhancing analytics tracking and wallet creation processes by including chain-specific information.

### Detailed summary
- Added `chainId` to `TransactionModal.tsx` for analytics.
- Updated `ConfirmationScreen.tsx` to use `fromToken.chainId`.
- Included `chainId` in wallet creation functions across multiple wallet types.
- Modified `trackConnect` to accept `chainId` in its arguments.

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

<!-- end pr-codex -->
2024-11-08 01:44:33 +00:00
Joaquim VergesandGitHub 46d0b4bdc0 fix: use maxFeePerGas for Pay gas estimations (#5349) 2024-11-08 14:26:54 +13:00
Joaquim VergesandGitHub 1e7e32f2cb feat: Support sophon testnet (#5335) 2024-11-08 14:10:52 +13:00
gregfromstl d34357e0a9 [Dashboard] Fix: Cleanup dashboard UI (#5344)
CNCT-2282

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refactoring components related to analytics and charts in the dashboard, removing unused components, improving data handling, and enhancing UI elements for better responsiveness and clarity.

### Detailed summary
- Deleted `ConnectSDKCard.tsx` and `ConnectAnalyticsDashboard.tsx` files.
- Removed the `Analytics` link from the sidebar.
- Enhanced `PieChartCard` and `BarChart` components with better data mapping and UI adjustments.
- Updated `RangeSelector` to handle local state for range and interval.
- Removed unused API functions related to wallet usage.
- Improved data filtering logic in `WalletDistributionCard` and `AuthMethodDistributionCard`.
- Adjusted image rendering for better layout consistency in charts.

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

<!-- end pr-codex -->
2024-11-08 00:32:04 +00:00
arcoraven 03ce8816ce fix: Engine wallet selector dropdown (#5347)
https://linear.app/thirdweb/issue/INFRA-409/fix-engine-wallet-dropdown-bug-in-dashboard

<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the backend wallet management by refining wallet type options and improving the user interface for wallet actions. It introduces restrictions on importing smart backend wallets and updates the icons in the wallet management table.

### Detailed summary
- Added a comment to disallow importing smart backend wallets.
- Filtered `EngineBackendWalletOptions` to exclude keys starting with "smart:".
- Replaced `Trash2Icon` with `PencilIcon` for the edit action in the `BackendWalletsTable`.
- Updated logic for checking wallet configuration to simplify conditions.
- Improved `onValueChange` handler in `CreateBackendWalletButton`.

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

<!-- end pr-codex -->
2024-11-07 23:22:49 +00:00
gregfromstl 2d99708c45 fix: flaky test (#5346)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on modifying the test for switching chains in the `smart-wallet-integration.test.ts` file. It simplifies the test by removing the transaction execution and balance verification, only asserting that the wallet's chain ID matches the expected value.

### Detailed summary
- Removed the creation of the `baseSepoliaEdition` contract instance.
- Eliminated the transaction sending and confirmation logic.
- Removed balance checking for the `baseSepoliaEdition` contract.
- Added an assertion to check if the wallet's current chain ID matches `baseSepolia.id`.

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

<!-- end pr-codex -->
2024-11-07 21:45:29 +00:00
MananTank aa46e27d7c Replace legacy dashboard with Teams and Projects dashboard (#5327)
## Changes Summary

### Huge changes
* Delete all `/dashboard/**` pages and make the `/team` the default
* Rename all the links `/dashboard/**` to the new equivalent + Setup redirects

### New pages Added in team and account layouts
* `/account/contracts`
* `/team/<team-slug>/~/contracts`
* `/account/devices`
* new team overview page
* `/team/<team-slug>/~/usage/storage`

### Pages Moved
* Engine pages moved from project to team
* Ecosystem pages moved from project to team
* Team's projects page moved to /projects ( `/team/<team-slug>/~/projects` )

### Links to these pages removed for now (page not deleted) in new dashboard because they are not yet ready
* `/account/wallets`, `/account/settings`

### Minor changes
* Add the `AutoConnect` component directly where we render `ThirdwebProvider` so it is always triggered and not only when certain components are rendered, and remove AutoConnect at component level
* Move `/chain/validate` to app router inside (dashboard) layout
* Remove unnecessary breadcrumb on /explore base page
* Various UI adjustments / improvements
* Add billing alerts on top of all team, project, account pages

### Fixes
* Fix theme switcher not working sometimes in app router
* Engine pages crashing in team dashboard
* Revert color changes done in a recent a PR, fix colors in project header, app footer done.
* Fix Team avatar not resolving IPFS
* Fix various react warnings
* Fix missing Overview tab on the ecosystem page

### Improvements
* Reduced redirects on in-app-wallets and connect analytics page by making the landing page the default. For example: render analytics on `/connect/in-app-wallets` instead of landing on `/connect/in-app-wallets` and then redirecting to `/connect/in-app-wallets/analytics` and then rendering the page )
2024-11-07 20:26:29 +00:00
gregfromstl 9f9410a77a [Dashboard] Feature: Project Overview (Analytics) Page (#5340)
CNCT-2182
CNCT-2180
CNCT-2177
CNCT-2179
CNCT-2178
CNCT-2176

https://github.com/user-attachments/assets/5c676ec4-ca2f-4796-880a-944636f9f0d5

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the analytics components in the dashboard by introducing new stats interfaces, updating existing components to utilize these stats, and improving the layout and functionality of various charts and cards.

### Detailed summary
- Added `WalletUserStats` interface.
- Updated `ProjectOverviewHeader` to accept `interval` and `range`.
- Enhanced `Stat` component to display trends with badges.
- Introduced `PieChart`, `BarChart`, and `CombinedBarChartCard` components.
- Updated `EmptyState` messaging.
- Improved layout in several components for better responsiveness.
- Added new stories for `PieChartCard`, `StatBreakdownCard`, and others in Storybook.
- Refactored data fetching functions to support new stats.

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

<!-- end pr-codex -->
2024-11-07 18:33:42 +00:00
737 changed files with 10817 additions and 11338 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
echo "Pull Request Body: $pr_body"
if echo "$pr_body" | grep -iE "CNCT|DASH"; then
if echo "$pr_body" | grep -iE "CNCT|DASH|BLOCK"; then
echo "Linked issue found in the pull request body."
else
echo "No linked issue found in the pull request body."
+3
View File
@@ -24,5 +24,8 @@
},
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[css]": {
"editor.defaultFormatter": "biomejs.biome"
}
}
+3 -1
View File
@@ -92,4 +92,6 @@ API_SERVER_SECRET=""
# Used for the Faucet page (/<chain_id>)
NEXT_PUBLIC_TURNSTILE_SITE_KEY=""
TURNSTILE_SECRET_KEY=""
REDIS_URL=""
REDIS_URL=""
ANALYTICS_SERVICE_URL=""
+24 -17
View File
@@ -2,15 +2,13 @@ import type { Preview } from "@storybook/react";
import "@/styles/globals.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MoonIcon, SunIcon } from "lucide-react";
import { ThemeProvider, useTheme } from "next-themes";
import { Inter as interFont } from "next/font/google";
// biome-ignore lint/style/useImportType: <explanation>
import React, { useState } from "react";
import React from "react";
import { useEffect } from "react";
import { Button } from "../src/@/components/ui/button";
// Note: Wrapping the Stoy with AppRouterProviders makes the storybook server time SUPER SLOW
// so let's just not have it there - all stories should be independent from context anyway and just rely on props
const queryClient = new QueryClient();
const fontSans = interFont({
@@ -31,9 +29,16 @@ const preview: Preview = {
decorators: [
(Story) => {
return (
<StoryLayout>
<Story />
</StoryLayout>
<ThemeProvider
attribute="class"
disableTransitionOnChange
enableSystem={false}
defaultTheme="dark"
>
<StoryLayout>
<Story />
</StoryLayout>
</ThemeProvider>
);
},
],
@@ -44,34 +49,36 @@ export default preview;
function StoryLayout(props: {
children: React.ReactNode;
}) {
const [theme, setTheme] = useState<"light" | "dark">("dark");
const { setTheme, theme } = useTheme();
useEffect(() => {
document.body.className = `font-sans antialiased ${fontSans.variable} ${theme === "dark" ? " dark" : ""}`;
}, [theme]);
document.body.className = `font-sans antialiased ${fontSans.variable}`;
}, []);
return (
<QueryClientProvider client={queryClient}>
<div className="min-h-screen flex flex-col bg-background text-foreground min-w-0">
<div className="flex justify-end p-4 border-b gap-2">
<div className="flex min-h-screen min-w-0 flex-col bg-background text-foreground">
<div className="flex justify-end gap-2 border-b p-4">
<Button
onClick={() => setTheme("dark")}
size="sm"
variant={theme === "dark" ? "default" : "ghost"}
variant={theme === "dark" ? "default" : "outline"}
className="h-auto w-auto rounded-full p-2"
>
<MoonIcon className="size-5" />
<MoonIcon className="size-4" />
</Button>
<Button
onClick={() => setTheme("light")}
size="sm"
variant={theme === "light" ? "default" : "ghost"}
variant={theme === "light" ? "default" : "outline"}
className="h-auto w-auto rounded-full p-2"
>
<SunIcon className="size-5" />
<SunIcon className="size-4" />
</Button>
</div>
<div className="grow flex flex-col min-w-0">{props.children}</div>
<div className="flex min-w-0 grow flex-col">{props.children}</div>
</div>
</QueryClientProvider>
);
@@ -1,4 +1,8 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import type { SentryBuildOptions } from "@sentry/nextjs";
import type { NextConfig } from "next";
import type { RemotePattern } from "next/dist/shared/lib/image-config";
import FRAMER_PATHS from "./framer-rewrites";
import getRedirects from "./redirects";
const ContentSecurityPolicy = `
default-src 'self';
@@ -37,15 +41,9 @@ const securityHeaders = [
},
];
const redirects = require("./redirects");
const FRAMER_PATHS = require("./framer-rewrites");
/**
* @returns {import('next').RemotePattern[]}
*/
function determineIpfsGateways() {
// add the clientId ipfs gateways
const remotePatterns = [];
const remotePatterns: RemotePattern[] = [];
if (process.env.API_ROUTES_CLIENT_ID) {
remotePatterns.push({
protocol: "https",
@@ -84,23 +82,39 @@ function determineIpfsGateways() {
return remotePatterns;
}
/** @type {import('next').NextConfig} */
const moduleExports = {
webpack: (config, { dev }) => {
if (config.cache && !dev) {
config.cache = Object.freeze({
type: "memory",
});
config.cache.maxMemoryGenerations = 0;
}
config.externals.push("pino-pretty");
config.module = {
...config.module,
exprContextCritical: false,
};
// Important: return the modified config
return config;
},
const SENTRY_OPTIONS: SentryBuildOptions = {
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options
org: "thirdweb-dev",
project: "dashboard",
// An auth token is required for uploading source maps.
authToken: process.env.SENTRY_AUTH_TOKEN,
// Suppresses source map uploading logs during build
silent: true,
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
tunnelRoute: "/err",
// Hides source maps from generated client bundles
hideSourceMaps: true,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// Enables automatic instrumentation of Vercel Cron Monitors.
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: false,
};
const baseNextConfig: NextConfig = {
async headers() {
return [
{
@@ -117,7 +131,7 @@ const moduleExports = {
];
},
async redirects() {
return redirects();
return getRedirects();
},
async rewrites() {
return [
@@ -151,74 +165,57 @@ const moduleExports = {
...determineIpfsGateways(),
],
},
reactStrictMode: true,
experimental: {
scrollRestoration: true,
webpackBuildWorker: true,
serverSourceMaps: false,
},
cacheMaxMemorySize: 0,
compiler: {
emotion: true,
},
productionBrowserSourceMaps: false,
reactStrictMode: true,
};
const { withSentryConfig } = require("@sentry/nextjs");
const { withPlausibleProxy } = require("next-plausible");
function getConfig(): NextConfig {
if (process.env.NODE_ENV === "production") {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { withPlausibleProxy } = require("next-plausible");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { withSentryConfig } = require("@sentry/nextjs");
return withBundleAnalyzer(
withPlausibleProxy({
customDomain: "https://pl.thirdweb.com",
scriptName: "pl",
})(
withSentryConfig(
{
...baseNextConfig,
experimental: {
webpackBuildWorker: true,
},
// @ts-expect-error - this is a valid option
webpack: (config, { dev }) => {
if (config.cache && !dev) {
config.cache = Object.freeze({
type: "filesystem",
maxMemoryGenerations: 0,
});
}
config.externals.push("pino-pretty");
config.module = {
...config.module,
exprContextCritical: false,
};
// Important: return the modified config
return config;
},
},
SENTRY_OPTIONS,
),
),
);
}
// otherwise return the base
return baseNextConfig;
}
// we only want sentry on production environments
const wSentry =
process.env.NODE_ENV === "production" ? withSentryConfig : (x) => x;
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
module.exports = withBundleAnalyzer(
withPlausibleProxy({
customDomain: "https://pl.thirdweb.com",
scriptName: "pl",
})(
wSentry(moduleExports, {
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options
org: "thirdweb-dev",
project: "dashboard",
// An auth token is required for uploading source maps.
authToken: process.env.SENTRY_AUTH_TOKEN,
// Suppresses source map uploading logs during build
silent: true,
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Transpiles SDK to be compatible with IE11 (increases bundle size)
transpileClientSDK: false,
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
tunnelRoute: "/err",
// Hides source maps from generated client bundles
hideSourceMaps: true,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// Enables automatic instrumentation of Vercel Cron Monitors.
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: false,
/**
* Disables the Sentry Webpack plugin on the server.
* See: https://github.com/getsentry/sentry-javascript/issues/10468#issuecomment-2004710692
*/
disableServerWebpackPlugin: true,
}),
),
);
export default getConfig();
+7 -8
View File
@@ -5,7 +5,7 @@
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "next dev",
"build": "next build",
"build": "NODE_OPTIONS=--max-old-space-size=6144 next build",
"start": "next start",
"format": "biome format ./src --write",
"lint": "biome check ./src && knip && eslint ./src",
@@ -50,7 +50,7 @@
"@shazow/whatsabi": "^0.16.0",
"@stripe/react-stripe-js": "^2.8.1",
"@stripe/stripe-js": "^3.5.0",
"@tanstack/react-query": "5.59.19",
"@tanstack/react-query": "5.59.20",
"@tanstack/react-table": "^8.17.3",
"@thirdweb-dev/service-utils": "workspace:*",
"@vercel/functions": "^1.4.2",
@@ -70,7 +70,7 @@
"ipaddr.js": "^2.2.0",
"lottie-react": "^2.4.0",
"lucide-react": "0.454.0",
"next": "15.0.2",
"next": "15.0.3",
"next-plausible": "^3.12.3",
"next-seo": "^6.5.0",
"next-themes": "^0.4.3",
@@ -78,8 +78,6 @@
"papaparse": "^5.4.1",
"pluralize": "^8.0.0",
"posthog-js": "1.67.1",
"prism-react-renderer": "^2.3.1",
"prismjs": "^1.29.0",
"qrcode": "^1.5.3",
"react": "19.0.0-rc-69d4b800-20241021",
"react-children-utilities": "^2.10.0",
@@ -94,6 +92,7 @@
"recharts": "^2.13.3",
"remark-gfm": "^4.0.0",
"server-only": "^0.0.1",
"shiki": "1.22.2",
"sonner": "^1.7.0",
"spdx-correct": "^3.2.0",
"swagger-ui-react": "^5.17.14",
@@ -107,8 +106,8 @@
"devDependencies": {
"@chakra-ui/cli": "^2.4.1",
"@chromatic-com/storybook": "3.2.2",
"@next/bundle-analyzer": "15.0.2",
"@next/eslint-plugin-next": "15.0.2",
"@next/bundle-analyzer": "15.0.3",
"@next/eslint-plugin-next": "15.0.3",
"@playwright/test": "1.48.2",
"@storybook/addon-essentials": "8.4.2",
"@storybook/addon-interactions": "8.4.2",
@@ -137,7 +136,7 @@
"eslint-config-biome": "1.9.3",
"eslint-plugin-react-compiler": "19.0.0-beta-8a03594-20241020",
"eslint-plugin-storybook": "^0.9.0",
"knip": "5.36.2",
"knip": "5.36.3",
"next-sitemap": "^4.2.3",
"postcss": "8.4.47",
"storybook": "8.4.2",
Binary file not shown.
+69 -124
View File
@@ -1,5 +1,69 @@
const legacyDashboardToTeamRedirects = [
{
source: "/dashboard",
destination: "/team",
permanent: false,
},
{
source: "/dashboard/contracts/:path*",
destination: "/team/~/~/contracts",
permanent: false,
},
{
source: "/dashboard/connect/ecosystem/:path*",
destination: "/team/~/~/ecosystem/:path*",
permanent: false,
},
{
source: "/dashboard/engine/:path*",
destination: "/team/~/~/engine/:path*",
permanent: false,
},
{
source: "/dashboard/settings/api-keys",
destination: "/team/~/~/projects",
permanent: false,
},
{
source: "/dashboard/settings/devices",
destination: "/account/devices",
permanent: false,
},
{
source: "/dashboard/settings/billing",
destination: "/team/~/~/settings/billing",
permanent: false,
},
{
source: "/dashboard/settings/gas-credits",
destination: "/team/~/~/settings/credits",
permanent: false,
},
{
source: "/dashboard/settings/usage",
destination: "/team/~/~/usage",
permanent: false,
},
{
source: "/dashboard/settings/storage",
destination: "/team/~/~/usage/storage",
permanent: false,
},
{
source: "/dashboard/settings/notifications",
destination: "/team/~/~/settings/notifications",
permanent: false,
},
// rest of the /dashboard/* routes
{
source: "/dashboard/:path*",
destination: "/team",
permanent: false,
},
];
/** @type {import('next').NextConfig['redirects']} */
function redirects() {
async function redirects() {
return [
{
source: "/portal/:match*",
@@ -11,16 +75,6 @@ function redirects() {
destination: "/solutions/chains",
permanent: true,
},
{
source: "/dashboard/publish/:path*",
destination: "/contracts/publish/:path*",
permanent: false,
},
{
source: "/dashboard/mumbai/publish/:path*",
destination: "/contracts/publish/:path*",
permanent: false,
},
{
source: "/contracts/release",
destination: "/contracts/publish",
@@ -97,23 +151,8 @@ function redirects() {
destination: "/polygon-zkevm",
permanent: false,
},
{
source: "/settings",
destination: "/dashboard/settings/api-keys",
permanent: false,
},
// backwards compat: page moved to pages/settings/api-key
{
source: "/dashboard/api-keys",
destination: "/dashboard/settings/api-keys",
permanent: false,
},
// backwards compat: page moved to pages/settings/devices
{
source: "/dashboard/settings/account",
destination: "/dashboard/settings/devices",
permanent: false,
},
{
source: "/template/nft-drop",
destination: "/template/erc721",
@@ -121,17 +160,12 @@ function redirects() {
},
{
source: "/create-api-key",
destination: "/dashboard/settings/api-keys",
destination: "/team/~/~/projects",
permanent: false,
},
{
source: "/dashboard/settings",
destination: "/dashboard/settings/api-keys",
permanent: false,
},
{
source: "/dashboard/connect",
destination: "/dashboard/connect/analytics",
destination: "/team/~/~/projects",
permanent: false,
},
{
@@ -139,61 +173,6 @@ function redirects() {
destination: "https://playground.thirdweb.com/connect/sign-in/button",
permanent: false,
},
{
source: "/dashboard/wallet",
destination: "https://playground.thirdweb.com/connect/sign-in/button",
permanent: false,
},
{
source: "/dashboard/rpc",
destination: "/dashboard/infrastructure/rpc-edge",
permanent: false,
},
{
source: "/dashboard/storage",
destination: "/dashboard/infrastructure/storage",
permanent: false,
},
{
source: "/smart-wallet",
destination: "/account-abstraction",
permanent: false,
},
{
source: "/dashboard/published",
destination: "/dashboard/publish",
permanent: false,
},
{
source: "/dashboard/wallets",
destination: "https://playground.thirdweb.com/connect/sign-in/button",
permanent: false,
},
{
source: "/dashboard/wallets/analytics",
destination: "/dashboard/connect/analytics",
permanent: false,
},
{
source: "/dashboard/wallets/embedded",
destination: "/dashboard/connect/embedded-wallets",
permanent: false,
},
{
source: "/dashboard/wallets/smart-wallet",
destination: "/dashboard/connect/account-abstraction",
permanent: false,
},
{
source: "/dashboard/wallets/connect",
destination: "https://playground.thirdweb.com/connect/sign-in/button",
permanent: false,
},
{
source: "/dashboard/infrastructure",
destination: "/dashboard/settings/storage",
permanent: false,
},
{
source: "/dashboard/infrastructure/storage",
destination: "/dashboard/settings/storage",
@@ -204,26 +183,6 @@ function redirects() {
destination: "/chainlist",
permanent: false,
},
{
source: "/dashboard/contracts",
destination: "/dashboard/contracts/deploy",
permanent: false,
},
{
source: "/solidity-sdk",
destination: "/build",
permanent: false,
},
{
source: "/connect-wallet",
destination: "/connect",
permanent: false,
},
{
source: "/wallet-sdk",
destination: "https://portal.thirdweb.com/wallet-sdk/latest",
permanent: false,
},
{
source: "/solutions/commerce",
destination: "/solutions/loyalty",
@@ -244,27 +203,12 @@ function redirects() {
destination: "/optimism",
permanent: false,
},
{
source: "/dashboard/payments/settings",
destination: "/dashboard/connect/pay",
permanent: false,
},
{
source: "/dashboard/payments/contracts",
destination: "/dashboard/connect/pay",
permanent: false,
},
// Redirecting as ambassadors lives in community now
{
source: "/ambassadors",
destination: "/community/ambassadors",
permanent: false,
},
{
source: "/dashboard/connect/embedded-wallets",
destination: "/dashboard/connect/in-app-wallets",
permanent: false,
},
{
source: "/embedded-wallets",
destination: "/in-app-wallets",
@@ -321,6 +265,7 @@ function redirects() {
destination: "/account-abstraction",
permanent: false,
},
...legacyDashboardToTeamRedirects,
];
}
+137
View File
@@ -0,0 +1,137 @@
import { fetchAnalytics } from "data/analytics/fetch-analytics";
export interface WalletStats {
date: string;
uniqueWalletsConnected: number;
totalConnections: number;
walletType: string;
}
export interface WalletUserStats {
date: string;
newUsers: number;
returningUsers: number;
totalUsers: number;
}
export interface InAppWalletStats {
date: string;
authenticationMethod: string;
uniqueWalletsConnected: number;
}
export interface EcosystemWalletStats extends InAppWalletStats {}
export interface UserOpStats {
date: string;
successful: number;
failed: number;
sponsoredUsd: number;
chainId?: string;
}
interface AnalyticsQueryParams {
clientId?: string;
accountId?: string;
from?: Date;
to?: Date;
period?: "day" | "week" | "month" | "year" | "all";
}
function buildSearchParams(params: AnalyticsQueryParams): URLSearchParams {
const searchParams = new URLSearchParams();
if (params.clientId) {
searchParams.append("clientId", params.clientId);
}
if (params.accountId) {
searchParams.append("accountId", params.accountId);
}
if (params.from) {
searchParams.append("from", params.from.toISOString());
}
if (params.to) {
searchParams.append("to", params.to.toISOString());
}
if (params.period) {
searchParams.append("period", params.period);
}
return searchParams;
}
export async function getWalletConnections(
params: AnalyticsQueryParams,
): Promise<WalletStats[]> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(`v1/wallets?${searchParams.toString()}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (res?.status !== 200) {
console.error("Failed to fetch wallet connections");
return [];
}
const json = await res.json();
return json.data as WalletStats[];
}
export async function getInAppWalletUsage(
params: AnalyticsQueryParams,
): Promise<InAppWalletStats[]> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v1/wallets/in-app?${searchParams.toString()}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
if (res?.status !== 200) {
console.error("Failed to fetch in-app wallet usage");
return [];
}
const json = await res.json();
return json.data as InAppWalletStats[];
}
export async function getUserOpUsage(
params: AnalyticsQueryParams,
): Promise<UserOpStats[]> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(`v1/user-ops?${searchParams.toString()}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (res?.status !== 200) {
console.error("Failed to fetch user ops usage");
return [];
}
const json = await res.json();
return json.data as UserOpStats[];
}
export async function getWalletUsers(
params: AnalyticsQueryParams,
): Promise<WalletUserStats[]> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v1/wallets/users?${searchParams.toString()}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
if (res?.status !== 200) {
console.error("Failed to fetch wallet user stats");
return [];
}
const json = await res.json();
return json.data as WalletUserStats[];
}
+2 -7
View File
@@ -13,13 +13,8 @@ export type Team = {
bannedAt?: string;
image?: string;
billingPlan: "pro" | "growth" | "free";
billingStatus: "validPayment" | (string & {}); // what's the other value?
billingEmail: string;
// billingExternalId: string;
// billingType: "STRIPE" | ??
// billingCustomerPayload: ?? | null
// paymentAttemptCount: number
// trialPeriodEndedAt: string | null;
billingStatus: "validPayment" | (string & {}) | null;
billingEmail: string | null;
};
export async function getTeamBySlug(slug: string) {
@@ -1,15 +0,0 @@
"use client";
import { useEffect } from "react";
import { setCookie } from "../../stores/SyncStoreToCookies";
export function DashboardTypeCookieSetter(props: {
type: "team" | "old";
}) {
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
setCookie("x-dashboard-type", props.type);
}, [props.type]);
return null;
}
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer } from "../../../../stories/utils";
import { getThirdwebClient } from "../../../constants/thirdweb.server";
import { Button } from "../../ui/button";
import { GradientAvatar } from "./GradientAvatar";
@@ -17,29 +18,74 @@ export const Desktop: Story = {
args: {},
};
const client = getThirdwebClient();
function Story() {
return (
<div className="flex flex-col gap-10 p-10">
<p> All images below are set with size-6 className </p>
<BadgeContainer label="No Src, No id - Skeleton">
<GradientAvatar id={undefined} src={undefined} className="size-6" />
<GradientAvatar
id={undefined}
src={undefined}
className="size-6"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="No Src, id=foo - Skeleton">
<GradientAvatar id={"foo"} src={undefined} className="size-6" />
<GradientAvatar
id={"foo"}
src={undefined}
className="size-6"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="Invalid/Empty Src, id=foo - Gradient">
<GradientAvatar id={"foo"} src={""} className="size-6" />
<BadgeContainer label="Invalid Src, id=foo - Gradient">
<GradientAvatar
id={"foo"}
src={""}
className="size-6"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="Invalid/Empty Src, id=bar - Gradient">
<GradientAvatar id={"bar"} src={""} className="size-6" />
<BadgeContainer label="Invalid Src, id=bar - Gradient">
<GradientAvatar
id={"bar"}
src={""}
className="size-6"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="Empty/Invalid Src, No id - default fallback">
<GradientAvatar src="invalid-src" id={undefined} className="size-6" />
<GradientAvatar
src="invalid-src"
id={undefined}
className="size-6"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="Valid Src, https">
<GradientAvatar
src="https://picsum.photos/200/300"
id={undefined}
className="size-6"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="Valid Src, ipfs">
<GradientAvatar
src="ipfs://QmZbeJYEs7kCJHyQxjxU2SJUtjSAr4m87wzJFJUyWomKdj/Smily.svg"
id={undefined}
className="size-6"
client={client}
/>
</BadgeContainer>
<ToggleTest />
@@ -74,7 +120,12 @@ function ToggleTest() {
<p> Src+ID is: {data ? "set" : "not set"} </p>
<BadgeContainer label="Valid Src">
<GradientAvatar src={data?.src} id={data?.id} className="size-6" />
<GradientAvatar
src={data?.src}
id={data?.id}
className="size-6"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="invalid Src">
@@ -82,6 +133,7 @@ function ToggleTest() {
className="size-6"
src={data ? "invalid-src" : undefined}
id={undefined}
client={client}
/>
</BadgeContainer>
</div>
@@ -1,5 +1,7 @@
import { Img } from "@/components/blocks/Img";
import { useMemo } from "react";
import type { ThirdwebClient } from "thirdweb";
import { resolveSchemeWithErrorHandler } from "../../../lib/resolveSchemeWithErrorHandler";
import { cn } from "../../../lib/utils";
const gradients = [
@@ -32,6 +34,7 @@ export function GradientAvatar(props: {
src: string | undefined;
id: string | undefined;
className: string;
client: ThirdwebClient;
}) {
const gradient = useMemo(() => {
if (!props.id) {
@@ -40,9 +43,16 @@ export function GradientAvatar(props: {
return getGradientForString(props.id);
}, [props.id]);
const resolvedSrc = props.src
? resolveSchemeWithErrorHandler({
client: props.client,
uri: props.src,
})
: props.src;
return (
<Img
src={props.src}
src={resolvedSrc}
className={cn("rounded-full", props.className)}
fallback={
gradient ? (
@@ -3,7 +3,7 @@ import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { DangerSettingCard } from "./DangerSettingCard";
const meta = {
title: "blocks/DangerSettingCard",
title: "blocks/Cards/DangerSettingCard",
component: Story,
parameters: {
nextjs: {
@@ -3,7 +3,7 @@ import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { SettingsCard } from "./SettingsCard";
const meta = {
title: "blocks/SettingsCard",
title: "Blocks/Cards/SettingsCard",
component: Story,
parameters: {
nextjs: {
@@ -51,7 +51,8 @@ export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
{props.links.map((link, i) => {
if ("group" in link) {
return (
<div className={cn({ "mt-6": i !== 0 })}>
// biome-ignore lint/suspicious/noArrayIndexKey: items won't be reordered
<div className={cn({ "mt-6": i !== 0 })} key={i}>
<p className={cn("px-3 py-2 font-medium text-foreground")}>
{link.group}
</p>
@@ -63,7 +64,8 @@ export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
const isExternal = link.href.startsWith("http");
return (
<NavLink
key={link.href}
// biome-ignore lint/suspicious/noArrayIndexKey: items won't be reordered
key={i}
href={link.href}
className="flex items-center gap-2 rounded-md px-3 py-2 text-muted-foreground text-sm hover:bg-muted"
activeClassName="text-foreground"
@@ -19,7 +19,7 @@ export function AppFooter(props: AppFooterProps) {
return (
<footer
className={cn(
"w-full border-border border-t bg-white py-6 md:py-8 dark:bg-muted/50",
"w-full border-border border-t bg-muted/50 py-6 md:py-8",
props.className,
)}
>
@@ -1,46 +0,0 @@
import { Highlight } from "prism-react-renderer";
import { cn } from "../../lib/utils";
import { CopyButton } from "./CopyButton";
import { Card } from "./card";
interface CodeBlockProps {
code: string;
language?: string;
className?: string;
canCopy?: boolean;
}
export const CodeBlock = ({
code,
language = "sh",
canCopy = true,
}: CodeBlockProps) => {
return (
<Highlight code={code} language={language}>
{({ className, tokens, getLineProps, getTokenProps }) => (
<Card className={className}>
{canCopy && (
<CopyButton text={code} className="float-right m-2 p-2" />
)}
<div
className={cn(
"block overflow-auto whitespace-pre-wrap p-4 font-mono text-sm",
className,
)}
>
{tokens.map((line, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: index IS the key here
<div {...getLineProps({ line, key: i })} key={i}>
{line.map((token, key) => (
// biome-ignore lint/suspicious/noArrayIndexKey: index IS the key here
<span {...getTokenProps({ token, key })} key={key} />
))}
</div>
))}
</div>
</Card>
)}
</Highlight>
);
};
@@ -50,7 +50,7 @@ export function DatePickerWithRange(props: {
id="date"
variant="outline"
className={cn(
"justify-start gap-2 bg-card text-left font-normal",
"justify-start gap-2 text-left font-normal",
props.className,
)}
>
@@ -1,36 +0,0 @@
"use client";
import { useClipboard } from "hooks/useClipboard";
import { CheckIcon, CopyIcon } from "lucide-react";
import { cn } from "../../lib/utils";
import { Button } from "./button";
export function PlainTextCodeBlock(props: {
code: string;
copyButtonClassName?: string;
}) {
const { hasCopied, onCopy } = useClipboard(props.code);
return (
<div className="relative">
<code className="block whitespace-pre rounded-lg border border-border bg-muted/50 p-4">
{props.code}
</code>
<Button
size="sm"
variant="outline"
onClick={onCopy}
className={cn(
"absolute top-3.5 right-3.5 h-auto p-2",
props.copyButtonClassName,
)}
>
{hasCopied ? (
<CheckIcon className="size-3 text-green-500" />
) : (
<CopyIcon className="size-3 text-muted-foreground" />
)}
</Button>
</div>
);
}
@@ -0,0 +1,70 @@
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
import { Button } from "@/components/ui/button";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { CheckIcon, CopyIcon } from "lucide-react";
import { useState } from "react";
export function RenderCode(props: {
code: string;
html: string;
className?: string;
scrollableClassName?: string;
}) {
return (
<div
className={cn(
"group relative max-w-full overflow-hidden rounded-lg border border-border bg-background",
props.className,
)}
>
<ScrollShadow
scrollableClassName={cn("p-4", props.scrollableClassName)}
className="text-xs md:text-sm [&_*]:leading-relaxed"
shadowColor="hsl(var(--muted))"
>
<div
// biome-ignore lint/security/noDangerouslySetInnerHtml: we know what we're doing here
dangerouslySetInnerHTML={{ __html: props.html }}
/>
</ScrollShadow>
<CopyButton
text={props.code}
iconClassName="size-3"
className="absolute top-4 right-4 z-10 border border-border bg-background p-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
/>
</div>
);
}
function CopyButton(props: {
text: string;
className?: string;
iconClassName?: string;
}) {
const [isCopied, setIsCopied] = useState(false);
return (
<ToolTipLabel label="Copy">
<Button
variant="ghost"
aria-label="Copy"
className={cn("h-auto w-auto p-1", props.className)}
onClick={() => {
navigator.clipboard.writeText(props.text);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 1000);
}}
>
{isCopied ? (
<CheckIcon
className={cn("size-4 text-green-500", props.iconClassName)}
/>
) : (
<CopyIcon
className={cn("size-4 text-muted-foreground", props.iconClassName)}
/>
)}
</Button>
</ToolTipLabel>
);
}
@@ -0,0 +1,57 @@
"use client";
import { cn } from "@/lib/utils";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import type { BundledLanguage } from "shiki";
import { Spinner } from "../../ui/Spinner/Spinner";
import { RenderCode } from "./RenderCode";
import { getCodeHtml } from "./getCodeHtml";
export type CodeProps = {
code: string;
lang: BundledLanguage;
className?: string;
scrollableClassName?: string;
loadingClassName?: string;
keepPreviousDataOnCodeChange?: boolean;
};
export const CodeClient: React.FC<CodeProps> = ({
code,
lang,
className,
scrollableClassName,
loadingClassName,
keepPreviousDataOnCodeChange = false,
}) => {
const codeQuery = useQuery({
queryKey: ["html", code],
queryFn: () => getCodeHtml(code, lang),
placeholderData: keepPreviousDataOnCodeChange
? keepPreviousData
: undefined,
retry: false,
});
if (!codeQuery.data) {
return (
<div
className={cn(
"flex min-h-[200px] items-center justify-center rounded-lg border border-border",
loadingClassName,
)}
>
<Spinner className="size-8" />
</div>
);
}
return (
<RenderCode
code={codeQuery.data.formattedCode}
html={codeQuery.data.html}
className={className}
scrollableClassName={scrollableClassName}
/>
);
};
@@ -0,0 +1,18 @@
import type { BundledLanguage } from "shiki";
import { RenderCode } from "./RenderCode";
import { getCodeHtml } from "./getCodeHtml";
export type CodeProps = {
code: string;
lang: BundledLanguage;
className?: string;
};
export const CodeServer: React.FC<CodeProps> = async ({
code,
lang,
className,
}) => {
const { html, formattedCode } = await getCodeHtml(code, lang);
return <RenderCode code={formattedCode} html={html} className={className} />;
};
@@ -0,0 +1,115 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { CodeClient } from "./code.client";
const meta = {
title: "code/lang",
component: Component,
parameters: {},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const tsCode = `\
type User = {
name: string;
age: number;
}
function logUser(user: User) {
console.log(user)
}
`;
const jsCode = `\
import { getContract } from "thirdweb";
import { sepolia } from "thirdweb/chains";
import { getOwnedNFTs } from "thirdweb/extensions/erc1155";
const contract = getContract({
client,
address: "0x1234...",
chain: sepolia,
});
`;
const jsxCode = `\
import { ThirdwebProvider } from "thirdweb/react";
function Main() {
return (
<ThirdwebProvider>
<App />
</ThirdwebProvider>
);
}`;
const tsxCode = `\
type User = {
name: string;
age: number;
}
function UserInfo(props: { user: User }) {
return <div> {props.user.name} </div>
}
`;
const tsxCodeWithFormError = `\
// This piece of code has invalid syntax and can't be formatted by prettier
// this should not crash the page - and be rendered as is without formatting
// the format error is logged in console
type User = // missing { here
name: string;
age: number;
}
function UserInfo(props: { user: User }) {
return <div> {props.user.name} </div>
}
`;
const bashCode = "pnpm i thirdweb";
function Component() {
return (
<div className="container flex max-w-[600px] flex-col gap-10 py-10">
<BadgeContainer label="ts">
<CodeClient code={tsCode} lang="ts" />
</BadgeContainer>
<BadgeContainer label="js">
<CodeClient code={jsCode} lang="js" />
</BadgeContainer>
<BadgeContainer label="bash">
<CodeClient code={bashCode} lang="bash" />
</BadgeContainer>
<BadgeContainer label="jsx">
<CodeClient code={jsxCode} lang="jsx" />
</BadgeContainer>
<BadgeContainer label="tsx">
<CodeClient code={tsxCode} lang="tsx" />
</BadgeContainer>
<BadgeContainer label="tsx">
<CodeClient code={tsxCodeWithFormError} lang="tsx" />
</BadgeContainer>
</div>
);
}
@@ -0,0 +1,45 @@
import * as parserBabel from "prettier/plugins/babel";
import * as estree from "prettier/plugins/estree";
import { format } from "prettier/standalone";
import { type BundledLanguage, codeToHtml } from "shiki";
function isPrettierSupportedLang(lang: BundledLanguage) {
return (
lang === "js" ||
lang === "jsx" ||
lang === "ts" ||
lang === "tsx" ||
lang === "javascript" ||
lang === "typescript" ||
lang === "css" ||
lang === "json"
);
}
export async function getCodeHtml(code: string, lang: BundledLanguage) {
const formattedCode = isPrettierSupportedLang(lang)
? await format(code, {
parser: "babel-ts",
plugins: [parserBabel, estree],
printWidth: 60,
}).catch((e) => {
console.error(e);
console.error("Failed to format code");
console.log({
code,
lang,
});
return code;
})
: code;
const html = await codeToHtml(formattedCode, {
lang: lang,
themes: {
light: "github-light",
dark: "github-dark-default",
},
});
return { html, formattedCode };
}
@@ -0,0 +1,52 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { PlainTextCodeBlock } from "./plaintext-code";
const meta = {
title: "code/plaintext",
component: Component,
parameters: {},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const jsCode = `\
import { getContract } from "thirdweb";
import { sepolia } from "thirdweb/chains";
import { getOwnedNFTs } from "thirdweb/extensions/erc1155";
const contract = getContract({
client,
address: "0x1234...",
chain: sepolia,
});
`;
const overflowText =
"123456789abcdefghijklmnopqrstuvwxyz123456789abcdefghijklmnopqrstuvwxyz123456789abcdefghijklmnopqrstuvwxyz123456789abcdefghijklmnopqrstuvwxyz123456789abcdefghijklmnopqrstuvwxyz123456789abcdefghijklmnopqrstuvwxyz";
function Component() {
return (
<div className="container flex max-w-[600px] flex-col gap-10 py-10">
<BadgeContainer label="large chunk of code">
<PlainTextCodeBlock code={jsCode} />
</BadgeContainer>
<BadgeContainer label="overflow test">
<PlainTextCodeBlock code={overflowText} />
</BadgeContainer>
</div>
);
}
@@ -0,0 +1,52 @@
"use client";
import { cn } from "@/lib/utils";
import { useClipboard } from "hooks/useClipboard";
import { CheckIcon, CopyIcon } from "lucide-react";
import { ScrollShadow } from "../ScrollShadow/ScrollShadow";
import { Button } from "../button";
export function PlainTextCodeBlock(props: {
code: string;
copyButtonClassName?: string;
className?: string;
scrollableClassName?: string;
codeClassName?: string;
}) {
const { hasCopied, onCopy } = useClipboard(props.code);
return (
<div
className={cn(
"group relative max-w-full overflow-hidden rounded-lg border border-border bg-background",
props.className,
)}
>
<ScrollShadow
scrollableClassName={cn("p-4", props.scrollableClassName)}
className="text-xs md:text-sm [&_*]:leading-relaxed"
shadowColor="hsl(var(--muted))"
>
<code className={cn("block whitespace-pre", props.codeClassName)}>
{props.code}
</code>
</ScrollShadow>
<Button
size="sm"
variant="outline"
onClick={onCopy}
className={cn(
"absolute top-3.5 right-3.5 h-auto bg-background p-2",
props.copyButtonClassName,
)}
>
{hasCopied ? (
<CheckIcon className="size-3 text-green-500" />
) : (
<CopyIcon className="size-3 text-muted-foreground" />
)}
</Button>
</div>
);
}
+34 -31
View File
@@ -35,40 +35,43 @@ const DialogContent = React.forwardRef<
dialogOverlayClassName?: string;
dialogCloseClassName?: string;
}
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay className={props.dialogOverlayClassName} />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-lg",
// on mobile - put the dialog at the bottom of the screen, animate - slide up and fade in
"right-0 bottom-0 left-0",
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
// on desktop - vertically and horizontally center the dialog, animate - zoom and fade in
"md:top-[50%] md:bottom-auto md:left-[50%] md:translate-x-[-50%] md:translate-y-[-50%] ",
"md:data-[state=closed]:zoom-out-95 md:data-[state=open]:zoom-in-95 ",
"md:data-[state=closed]:slide-out-to-left-1/2 md:data-[state=closed]:slide-out-to-top-[48%] ",
"md:data-[state=open]:slide-in-from-left-1/2 md:data-[state=open]:slide-in-from-top-[48%] ",
"data-[state=closed]:animate-out data-[state=open]:animate-in",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close
>(({ className, children, ...props }, ref) => {
const { dialogOverlayClassName, dialogCloseClassName, ...restProps } = props;
return (
<DialogPortal>
<DialogOverlay className={dialogOverlayClassName} />
<DialogPrimitive.Content
ref={ref}
className={cn(
"absolute top-4 right-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-accent data-[state=open]:text-muted-foreground",
props.dialogCloseClassName,
"fixed grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-lg",
// on mobile - put the dialog at the bottom of the screen, animate - slide up and fade in
"right-0 bottom-0 left-0",
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
// on desktop - vertically and horizontally center the dialog, animate - zoom and fade in
"md:top-[50%] md:bottom-auto md:left-[50%] md:translate-x-[-50%] md:translate-y-[-50%] ",
"md:data-[state=closed]:zoom-out-95 md:data-[state=open]:zoom-in-95 ",
"md:data-[state=closed]:slide-out-to-left-1/2 md:data-[state=closed]:slide-out-to-top-[48%] ",
"md:data-[state=open]:slide-in-from-left-1/2 md:data-[state=open]:slide-in-from-top-[48%] ",
"data-[state=closed]:animate-out data-[state=open]:animate-in",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...restProps}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
{children}
<DialogPrimitive.Close
className={cn(
"absolute top-4 right-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-accent data-[state=open]:text-muted-foreground",
dialogCloseClassName,
)}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
);
});
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
@@ -11,7 +11,7 @@ import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer } from "../../../stories/utils";
const meta = {
title: "Shadcn/select",
title: "Shadcn/Select",
component: Component,
parameters: {
layout: "centered",
@@ -13,7 +13,7 @@ import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { Badge } from "./badge";
const meta = {
title: "Shadcn/table",
title: "Shadcn/Table",
component: Component,
parameters: {
layout: "centered",
@@ -197,6 +197,7 @@ export function TabPathLinks(props: {
isActive: l.exactMatch
? pathname === l.path
: pathname.startsWith(l.path),
isDisabled: l.isDisabled,
}))}
/>
);
+2 -2
View File
@@ -2,8 +2,8 @@ export function isLoginRequired(pathname: string) {
// remove '/' in front and then split by '/'
const paths = pathname.slice(1).split("/");
// /dashboard, /team, /cli
if (paths[0] === "dashboard" || paths[0] === "team" || paths[0] === "cli") {
// /team, /cli
if (paths[0] === "team" || paths[0] === "cli") {
return true;
}
@@ -4,6 +4,7 @@ import {
IPFS_GATEWAY_URL,
} from "@/constants/env";
import {
THIRDWEB_BUNDLER_DOMAIN,
THIRDWEB_INAPP_WALLET_DOMAIN,
THIRDWEB_PAY_DOMAIN,
THIRDWEB_RPC_DOMAIN,
@@ -11,7 +12,15 @@ import {
THIRDWEB_STORAGE_DOMAIN,
} from "constants/urls";
import { createThirdwebClient } from "thirdweb";
import { setThirdwebDomains } from "thirdweb/utils";
import { getChainMetadata } from "thirdweb/chains";
import { populateEip712Transaction } from "thirdweb/transaction";
import {
getTransactionDecorator,
isZkSyncChain,
setThirdwebDomains,
setTransactionDecorator,
} from "thirdweb/utils";
import { getZkPaymasterData } from "thirdweb/wallets/smart";
import { getVercelEnv } from "../../lib/vercel-utils";
// returns a thirdweb client with optional JWT passed in
@@ -24,8 +33,54 @@ export function getThirdwebClient(jwt?: string) {
pay: THIRDWEB_PAY_DOMAIN,
storage: THIRDWEB_STORAGE_DOMAIN,
social: THIRDWEB_SOCIAL_API_DOMAIN,
bundler: THIRDWEB_BUNDLER_DOMAIN,
});
}
if (!getTransactionDecorator()) {
setTransactionDecorator(async ({ account, transaction }) => {
// use paymaster for zk chains on testnets
const chainMeta = await getChainMetadata(transaction.chain);
if (chainMeta.testnet) {
const isZkChain = await isZkSyncChain(transaction.chain);
if (isZkChain) {
const serializedTx = await populateEip712Transaction({
transaction,
account,
});
const pmData = await getZkPaymasterData({
options: {
client: transaction.client,
chain: transaction.chain,
},
transaction: serializedTx,
}).catch((e) => {
console.warn(
"No zk paymaster data available on chain ",
transaction.chain.id,
e,
);
return undefined;
});
return {
account,
transaction: {
...transaction,
eip712: pmData
? {
...transaction.eip712,
paymaster: pmData.paymaster,
paymasterInput: pmData.paymasterInput,
}
: transaction.eip712,
},
};
}
}
return { account, transaction };
});
}
return createThirdwebClient({
secretKey: jwt ? jwt : DASHBOARD_THIRDWEB_SECRET_KEY,
clientId: DASHBOARD_THIRDWEB_CLIENT_ID,
+12 -2
View File
@@ -6,7 +6,7 @@
:root,
[data-theme="light"] {
/* bg - neutral */
--background: 0 0% 98%;
--background: 0 0% 100%;
--popover: 0 0% 100%;
--card: 0 0% 100%;
--secondary: 0 0% 92%;
@@ -46,7 +46,7 @@
[data-theme="dark"] {
/* bg - neutral */
--background: 0 0% 0%;
--card: 0 0% 5%;
--card: 0 0% 0%;
--popover: 0 0% 0%;
--secondary: 0 0% 10%;
--muted: 0 0% 9%;
@@ -156,3 +156,13 @@ input:-webkit-autofill:active {
/* Revert background color */
transition: background-color 5000s ease-in-out 0s;
}
.dark .shiki,
.dark .shiki span {
color: var(--shiki-dark) !important;
background-color: var(--background) !important;
/* Optional, if you also want font styles */
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
@@ -15,7 +15,6 @@ import { usePathname } from "next/navigation";
import { useCallback, useMemo, useState } from "react";
import type { Chain } from "thirdweb";
import {
AutoConnect,
ConnectButton,
type NetworkSelectorProps,
useConnectModal,
@@ -36,6 +35,7 @@ export const CustomConnectWallet = (props: {
connectButtonClassName?: string;
signInLinkButtonClassName?: string;
detailsButtonClassName?: string;
chain?: Chain;
}) => {
const thirdwebClient = useThirdwebClient();
const loginRequired =
@@ -126,8 +126,6 @@ export const CustomConnectWallet = (props: {
<div className="flex h-[48px] w-[144px] items-center justify-center rounded-lg border border-border bg-muted">
<Spinner className="size-4" />
</div>
{/* need auto connect here so that we actually connect */}
<AutoConnect client={thirdwebClient} />
</>
);
}
@@ -147,8 +145,6 @@ export const CustomConnectWallet = (props: {
Sign In
</Link>
</Button>
{/* need auto connect here so that we actually connect */}
<AutoConnect client={thirdwebClient} />
</>
);
}
@@ -209,6 +205,7 @@ export const CustomConnectWallet = (props: {
},
},
}}
chain={props.chain}
/>
<LazyConfigureNetworkModal
@@ -1,3 +1,4 @@
import type { UserOpStats } from "@/api/analytics";
import {
type Query,
useMutation,
@@ -231,36 +232,6 @@ export interface UsageBillableByService {
};
}
export interface WalletStats {
date: string;
uniqueWalletsConnected: number;
totalConnections: number;
walletType: string;
}
export interface InAppWalletStats {
date: string;
authenticationMethod: string;
uniqueWalletsConnected: number;
}
export interface EcosystemWalletStats extends InAppWalletStats {}
export interface UserOpStats {
date: string;
successful: number;
failed: number;
sponsoredUsd: number;
}
export interface UserOpStatsByChain {
date: string;
successful: number;
failed: number;
sponsoredUsd: number;
chainId?: string;
}
interface BillingProduct {
name: string;
id: string;
@@ -373,43 +344,6 @@ export function useAccountCredits() {
});
}
async function getWalletUsage(args: {
clientId: string;
from?: Date;
to?: Date;
period?: "day" | "week" | "month" | "year" | "all";
}) {
const { clientId, from, to, period } = args;
const searchParams = new URLSearchParams();
searchParams.append("clientId", clientId);
if (from) {
searchParams.append("from", from.toISOString());
}
if (to) {
searchParams.append("to", to.toISOString());
}
if (period) {
searchParams.append("period", period);
}
const res = await fetch(
`${THIRDWEB_ANALYTICS_API_HOST}/v1/wallets?${searchParams.toString()}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
},
);
const json = await res.json();
if (res.status !== 200) {
throw new Error(json.message);
}
return json.data;
}
async function getUserOpUsage(args: {
clientId: string;
from?: Date;
@@ -447,43 +381,6 @@ async function getUserOpUsage(args: {
return json.data;
}
export async function getInAppWalletUsage(args: {
clientId: string;
from?: Date;
to?: Date;
period?: "day" | "week" | "month" | "year" | "all";
}) {
const { clientId, from, to, period } = args;
const searchParams = new URLSearchParams();
searchParams.append("clientId", clientId);
if (from) {
searchParams.append("from", from.toISOString());
}
if (to) {
searchParams.append("to", to.toISOString());
}
if (period) {
searchParams.append("period", period);
}
const res = await fetch(
`${THIRDWEB_ANALYTICS_API_HOST}/v1/wallets/in-app?${searchParams.toString()}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
},
);
const json = await res?.json();
if (!res || res.status !== 200) {
throw new Error(json.message);
}
return json.data;
}
export function useUserOpUsageAggregate(args: {
clientId: string;
from?: Date;
@@ -567,63 +464,6 @@ export function useUserOpUsagePeriod(args: {
});
}
export function useWalletUsageAggregate(args: {
clientId: string;
from?: Date;
to?: Date;
}) {
const { clientId, from, to } = args;
const { user, isLoggedIn } = useLoggedInUser();
return useQuery({
queryKey: accountKeys.walletStats(
user?.address as string,
clientId as string,
from?.toISOString() || "",
to?.toISOString() || "",
"all",
),
queryFn: async () => {
return getWalletUsage({
clientId,
from,
to,
period: "all",
});
},
enabled: !!clientId && !!user?.address && isLoggedIn,
});
}
export function useWalletUsagePeriod(args: {
clientId: string;
from?: Date;
to?: Date;
period: "day" | "week" | "month" | "year";
}) {
const { clientId, from, to, period } = args;
const { user, isLoggedIn } = useLoggedInUser();
return useQuery({
queryKey: accountKeys.walletStats(
user?.address as string,
clientId as string,
from?.toISOString() || "",
to?.toISOString() || "",
period,
),
queryFn: async () => {
return getWalletUsage({
clientId,
from,
to,
period,
});
},
enabled: !!clientId && !!user?.address && isLoggedIn,
});
}
export function useUpdateAccount() {
const { user } = useLoggedInUser();
const queryClient = useQueryClient();
@@ -1,7 +1,7 @@
"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { ResultItem } from "app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/metrics/components/StatusCodes";
import type { ResultItem } from "app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/metrics/components/StatusCodes";
import { THIRDWEB_API_HOST } from "constants/urls";
import type { EngineBackendWalletType } from "lib/engine";
import { useState } from "react";
@@ -1,6 +1,4 @@
"use client";
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
@@ -204,7 +202,10 @@ export function FaucetButton({
}
// Email verification is required to claim from the faucet
if (accountQuery.data.status === "noCustomer") {
if (
!accountQuery.data.emailConfirmedAt &&
!accountQuery.data.unconfirmedEmail
) {
return (
<>
<Button
@@ -215,11 +216,7 @@ export function FaucetButton({
Verify your Email
</Button>
{/* We will show the modal only if the user click on it, because this is a public page */}
{showOnboarding && (
<ChakraProviderSetup>
<Onboarding onOpenChange={setShowOnBoarding} />
</ChakraProviderSetup>
)}
{showOnboarding && <Onboarding onOpenChange={setShowOnBoarding} />}
</>
);
}
@@ -58,7 +58,7 @@ export function ChainHeader(props: ChainHeaderProps) {
}
/>
<Button variant="default" asChild>
<Link href="https://thirdweb.com/dashboard" target="_blank">
<Link href="/team" target="_blank">
Get started with thirdweb
</Link>
</Button>
@@ -205,7 +205,7 @@ export default async function ChainPageLayout(props: {
}
/>
<Button variant="primary">
<Link href="https://thirdweb.com/dashboard" target="_blank">
<Link href="/team" target="_blank">
Get started
</Link>
</Button>
@@ -22,6 +22,7 @@ import { isMoralisSupported } from "lib/wallet/nfts/moralis";
import { isSimpleHashSupported } from "lib/wallet/nfts/simpleHash";
import type { WalletNFT } from "lib/wallet/nfts/types";
import { CircleAlertIcon, InfoIcon } from "lucide-react";
import Link from "next/link";
import { type Dispatch, type SetStateAction, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -47,14 +48,7 @@ import type {
CreateListingParams,
} from "thirdweb/extensions/marketplace";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import {
FormErrorMessage,
FormHelperText,
FormLabel,
Heading,
Link,
Text,
} from "tw-components";
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
import { shortenIfAddress } from "utils/usedapp-external";
@@ -357,9 +351,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
})}
>
<FormControl>
<Heading as={FormLabel} size="label.lg">
Select NFT
</Heading>
<FormLabel>Select NFT</FormLabel>
<FormHelperText mb="8px">
Select the NFT you want to list for sale
</FormHelperText>
@@ -367,24 +359,22 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
<Flex flexDir="column" gap={4} mb={4}>
<div className="flex flex-row items-center gap-3 rounded-md border border-border border-orange-100 bg-orange-50 p-[10px] dark:border-orange-300 dark:bg-orange-300">
<InfoIcon className="size-6 text-orange-400 dark:text-orange-900" />
<Text color="orange.800" _dark={{ color: "orange.900" }}>
<p className="text-orange-800 dark:text-orange-900">
This chain is not supported by our NFT API yet, please enter the
contract address of the NFT you want to list.
</Text>
</p>
</div>
<FormControl
isInvalid={!!form.formState.errors.selected?.contractAddress}
>
<Heading as={FormLabel} size="label.lg">
Contract address
</Heading>
<FormLabel>Contract address</FormLabel>
<SolidityInput
solidityType="address"
formContext={form}
{...form.register("selected.contractAddress", {
required: "Contract address is required",
})}
placeholder=""
placeholder="0x..."
/>
<FormErrorMessage>
{form.formState.errors.selected?.contractAddress?.message}
@@ -458,21 +448,19 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
) : nfts && nfts.length === 0 ? (
<div className="flex flex-row items-center gap-3 rounded-md border border-border border-orange-100 bg-orange-50 p-[10px] dark:border-orange-300 dark:bg-orange-300">
<InfoIcon className="size-6 text-orange-400 dark:text-orange-900" />
<Text color="orange.800" _dark={{ color: "orange.900" }}>
<p className="text-orange-800 dark:text-orange-900">
There are no NFTs owned by this wallet. You need NFTs to create a
listing. You can create NFTs with thirdweb.{" "}
<Link href="/explore/nft" color="blue.600" isExternal>
<Link href="/explore/nft" color="blue.600" target="_blank">
Explore NFT contracts
</Link>
.
</Text>
</p>
</div>
) : null}
</FormControl>
<FormControl isRequired isDisabled={noNfts}>
<Heading as={FormLabel} size="label.lg">
Listing Currency
</Heading>
<FormLabel>Listing Currency</FormLabel>
<CurrencySelector
contractChainId={chainId}
value={form.watch("currencyContractAddress")}
@@ -485,11 +473,11 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
</FormHelperText>
</FormControl>
<FormControl isRequired isDisabled={noNfts}>
<Heading as={FormLabel} size="label.lg">
<FormLabel>
{form.watch("listingType") === "auction"
? "Buyout Price Per Token"
: "Listing Price"}
</Heading>
</FormLabel>
<Input {...form.register("pricePerToken")} />
<FormHelperText>
{form.watch("listingType") === "auction"
@@ -500,9 +488,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
{form.watch("selected")?.type?.toLowerCase() !== "erc721" && (
<FormControl isRequired isDisabled={noNfts}>
<div className="flex flex-row justify-between gap-2">
<Heading as={FormLabel} size="label.lg">
Quantity
</Heading>
<FormLabel>Quantity</FormLabel>
</div>
<Input {...form.register("quantity")} />
<FormHelperText>
@@ -513,18 +499,14 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
{form.watch("listingType") === "auction" && (
<>
<FormControl isRequired isDisabled={noNfts}>
<Heading as={FormLabel} size="label.lg">
Reserve Price Per Token
</Heading>
<FormLabel>Reserve Price Per Token</FormLabel>
<Input {...form.register("reservePricePerToken")} />
<FormHelperText>
The minimum price per token necessary to bid on this auction
</FormHelperText>
</FormControl>
<FormControl isRequired>
<Heading as={FormLabel} size="label.lg">
Auction Duration
</Heading>
<FormLabel>Auction Duration</FormLabel>
<Select {...form.register("listingDurationInSeconds")}>
{auctionTimes.map((time) => (
<option key={time.value} value={time.value}>
@@ -1,6 +1,8 @@
import { WalletAddress } from "@/components/blocks/wallet-address";
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import { Flex, GridItem, SimpleGrid, usePrevious } from "@chakra-ui/react";
import type { ThirdwebContract } from "thirdweb";
@@ -9,7 +11,8 @@ import type {
EnglishAuction,
} from "thirdweb/extensions/marketplace";
import { useActiveAccount } from "thirdweb/react";
import { Badge, Card, CodeBlock, Heading, Text } from "tw-components";
import { CodeClient } from "@/components/ui/code/code.client";
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
import { CancelTab } from "./cancel-tab";
import { LISTING_STATUS } from "./types";
@@ -51,18 +54,20 @@ export const ListingDrawer: React.FC<NFTDrawerProps> = ({
height="150px"
/>
<Flex flexDir="column" gap={2} w="70%">
<Heading size="title.lg">{renderData.asset.metadata.name}</Heading>
<Text size="label.md" noOfLines={6}>
<p className="font-bold text-lg">
{renderData.asset.metadata.name}
</Text>
</p>
<p className="line-clamp-6 truncate text-base leading-normal">
{renderData.asset.metadata.name}
</p>
</Flex>
</div>
<Flex flexDir="column" gap={4}>
<Card as={Flex} flexDir="column" gap={3}>
<Card className="flex flex-col gap-3 p-4">
<SimpleGrid rowGap={3} columns={12} placeItems="center left">
<GridItem colSpan={3}>
<Heading size="label.md">Asset contract address</Heading>
<p className="font-bold">Asset contract address</p>
</GridItem>
<GridItem colSpan={9}>
<CopyAddressButton
@@ -71,7 +76,7 @@ export const ListingDrawer: React.FC<NFTDrawerProps> = ({
/>
</GridItem>
<GridItem colSpan={3}>
<Heading size="label.md">Token ID</Heading>
<p className="font-bold">Token ID</p>
</GridItem>
<GridItem colSpan={9}>
<CopyTextButton
@@ -82,13 +87,13 @@ export const ListingDrawer: React.FC<NFTDrawerProps> = ({
/>
</GridItem>
<GridItem colSpan={3}>
<Heading size="label.md">Seller</Heading>
<p className="font-bold">Seller</p>
</GridItem>
<GridItem colSpan={9}>
<WalletAddress address={renderData.creatorAddress} />
</GridItem>
<GridItem colSpan={3}>
<Heading size="label.md">Listing ID</Heading>
<p className="font-bold">Listing ID</p>
</GridItem>
<GridItem colSpan={9}>
<CopyTextButton
@@ -99,49 +104,37 @@ export const ListingDrawer: React.FC<NFTDrawerProps> = ({
/>
</GridItem>
<GridItem colSpan={3}>
<Heading size="label.md">Type</Heading>
</GridItem>
<GridItem colSpan={9}>
<Text fontFamily="mono" size="body.md">
{renderData.asset.type}
</Text>
<p className="font-bold">Type</p>
</GridItem>
<GridItem colSpan={9}>{renderData.asset.type}</GridItem>
<GridItem colSpan={3}>
<Heading size="label.md">Status</Heading>
<p className="font-bold">Status</p>
</GridItem>
<GridItem colSpan={9}>
<Badge
size="label.sm"
variant="subtle"
textTransform="capitalize"
>
<Badge className="uppercase">
{LISTING_STATUS[renderData.status]}
</Badge>
</GridItem>
<GridItem colSpan={3}>
<Heading size="label.md">Quantity</Heading>
<p className="font-bold">Quantity</p>
</GridItem>
<GridItem colSpan={9}>
<Text fontFamily="mono" size="body.md">
{(renderData.quantity || 0n).toString()}{" "}
{/* For listings that are completed, the `quantity` would be `0`
{(renderData.quantity || 0n).toString()}{" "}
{/* For listings that are completed, the `quantity` would be `0`
So we show this text to make it clear */}
{LISTING_STATUS[renderData.status] === "Completed"
? "(Sold out)"
: ""}
</Text>
{LISTING_STATUS[renderData.status] === "Completed"
? "(Sold out)"
: ""}
</GridItem>
{renderData.type === "direct-listing" && (
<>
<GridItem colSpan={3}>
<Heading size="label.md">Price</Heading>
<p className="font-bold">Price</p>
</GridItem>
<GridItem colSpan={9}>
<Text fontFamily="mono" size="body.md">
{renderData.currencyValuePerToken.displayValue}{" "}
{renderData.currencyValuePerToken.symbol}
</Text>
{renderData.currencyValuePerToken.displayValue}{" "}
{renderData.currencyValuePerToken.symbol}
</GridItem>
</>
)}
@@ -153,16 +146,13 @@ export const ListingDrawer: React.FC<NFTDrawerProps> = ({
</SimpleGrid>
</Card>
{data?.asset.metadata.properties ? (
<Card as={Flex} flexDir="column" gap={4}>
<Heading size="label.md">Attributes</Heading>
<CodeBlock
<Card className="flex flex-col gap-3 p-4">
<p className="font-bold">Attributes</p>
<CodeClient
code={
JSON.stringify(data.asset.metadata.properties, null, 2) || ""
}
language="json"
canCopy={false}
wrap={false}
overflow="auto"
lang="json"
/>
</Card>
) : null}
@@ -1,7 +1,6 @@
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Button } from "@/components/ui/button";
import {
ButtonGroup,
Flex,
IconButton,
Select,
Skeleton,
@@ -38,7 +37,6 @@ import type {
EnglishAuction,
} from "thirdweb/extensions/marketplace";
import { min } from "thirdweb/utils";
import { Button, Text } from "tw-components";
import { ListingDrawer } from "./listing-drawer";
import { LISTING_STATUS } from "./types";
@@ -73,9 +71,9 @@ const tableColumns: Column<DirectListing | EnglishAuction>[] = [
// biome-ignore lint/suspicious/noExplicitAny: FIXME
Cell: ({ cell }: { cell: Cell<any, any> }) => {
return (
<Text size="label.md" whiteSpace="nowrap">
<p className="whitespace-nowrap text-muted-foreground">
{cell.value.displayValue} {cell.value.symbol}
</Text>
</p>
);
},
},
@@ -182,21 +180,23 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
>(null);
return (
<Flex gap={4} direction="column">
<ButtonGroup size="sm" variant="outline" isAttached>
<div className="flex flex-col gap-4">
<div className="flex flex-row">
<Button
onClick={() => setListingsToShow("all")}
variant={listingsToShow === "all" ? "solid" : "outline"}
variant={listingsToShow === "all" ? "default" : "outline"}
className="w-18 rounded-r-none"
>
All
</Button>
<Button
onClick={() => setListingsToShow("valid")}
variant={listingsToShow === "valid" ? "solid" : "outline"}
variant={listingsToShow === "valid" ? "default" : "outline"}
className="rounded-l-none"
>
Valid
</Button>
</ButtonGroup>
</div>
<TableContainer maxW="100%" className="relative">
{((listingsToShow === "all" && getAllQueryResult.isFetching) ||
(listingsToShow === "valid" && getValidQueryResult.isFetching)) && (
@@ -226,9 +226,9 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
key={columnIndex}
>
<Text as="label" size="label.sm" color="faded">
<p className="text-muted-foreground">
{column.render("Header")}
</Text>
</p>
</Th>
))}
{/* // Need to add an empty header for the drawer button */}
@@ -273,7 +273,7 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
</Table>
</TableContainer>
<div className="flex w-full items-center justify-center">
<Flex gap={2} direction="row" align="center">
<div className="flex flex-row items-center gap-2">
<IconButton
isDisabled={!canPreviousPage || totalCountQuery.isPending}
aria-label="first page"
@@ -286,7 +286,7 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
icon={<ChevronLeftIcon className="size-4" />}
onClick={() => previousPage()}
/>
<Text whiteSpace="nowrap">
<p className="whitespace-nowrap">
Page <strong>{pageIndex + 1}</strong> of{" "}
<Skeleton
as="span"
@@ -295,7 +295,7 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
>
<strong>{pageCount}</strong>
</Skeleton>
</Text>
</p>
<IconButton
isDisabled={!canNextPage || totalCountQuery.isPending}
aria-label="next page"
@@ -322,8 +322,8 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
<option value="250">250</option>
<option value="500">500</option>
</Select>
</Flex>
</div>
</div>
</Flex>
</div>
);
};
@@ -1,7 +1,6 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { Heading } from "tw-components";
import { CreateListingButton } from "../components/list-button";
import { DirectListingsTable } from "./components/table";
@@ -15,7 +14,7 @@ export const ContractDirectListingsPage: React.FC<
return (
<div className="flex flex-col gap-6">
<div className="flex flex-row items-center justify-between">
<Heading size="title.sm">Contract Listings</Heading>
<p className="text-lg">Contract Listings</p>
<div className="flex flex-row gap-4">
<CreateListingButton
contract={contract}
@@ -1,7 +1,6 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { Heading } from "tw-components";
import { CreateListingButton } from "../components/list-button";
import { EnglishAuctionsTable } from "./components/table";
@@ -15,7 +14,7 @@ export const ContractEnglishAuctionsPage: React.FC<
return (
<div className="flex flex-col gap-6">
<div className="flex flex-row items-center justify-between">
<Heading size="title.sm">Contract Auctions</Heading>
<p className="text-lg">Contract Auctions</p>
<div className="flex flex-row gap-4">
<CreateListingButton
contract={contract}
@@ -1,4 +1,4 @@
import { Link } from "tw-components";
import Link from "next/link";
import { QuantityInputWithUnlimited } from "../../quantity-input-with-unlimited";
import { CustomFormControl } from "../common";
import { useClaimConditionsFormContext } from "../index";
@@ -41,8 +41,8 @@ export const MaxClaimablePerWalletInput: React.FC = () => {
Limits are set per wallets and not per user, sophisticated actors
could get around wallet restrictions.{" "}
<Link
isExternal
color="blue.500"
className="text-blue-500"
target="_blank"
href="https://portal.thirdweb.com/contracts/design/Drop#sybil-attacks"
>
Learn more
@@ -1,11 +1,6 @@
import { Flex, FormControl } from "@chakra-ui/react";
import type { FieldError } from "react-hook-form";
import {
FormErrorMessage,
FormHelperText,
FormLabel,
Heading,
} from "tw-components";
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
import type { ComponentWithChildren } from "types/component-with-children";
import { useClaimConditionsFormContext } from ".";
@@ -22,9 +17,7 @@ export const CustomFormControl: ComponentWithChildren<
return (
<FormControl isDisabled={props.disabled} isInvalid={!!props.error}>
{/* label */}
<Heading as={FormLabel} size="label.md">
{props.label}
</Heading>
<FormLabel className="font-bold">{props.label}</FormLabel>
{/* input */}
{props.children}
@@ -1,8 +1,9 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { AdminOnly } from "@3rdweb-sdk/react/components/roles/admin-only";
import { Flex, SimpleGrid } from "@chakra-ui/react";
import { ChevronDownIcon, ChevronUpIcon, XIcon } from "lucide-react";
import type { ThirdwebContract } from "thirdweb";
import { Badge, Button, Card, Heading, Text } from "tw-components";
import { ClaimConditionTypeData, useClaimConditionsFormContext } from ".";
import { PricePreview } from "../price-preview";
import { ClaimPriceInput } from "./Inputs/ClaimPriceInput";
@@ -41,117 +42,114 @@ export const ClaimConditionsPhase: React.FC<ClaimConditionsPhaseProps> = ({
};
return (
<Card position="relative" p={8}>
<Flex direction="column" gap={8}>
<Flex
align="flex-start"
justify="space-between"
position="absolute"
top="10px"
right="10px"
gap={1}
<Card className="relative flex flex-col gap-8 p-8">
<div className="absolute top-3 right-3 flex flex-row items-start justify-between gap-1">
<Button
variant="ghost"
onClick={toggleEditing}
size="sm"
className="gap-2"
>
{field.isEditing ? "Collapse" : isAdmin ? "Edit" : "See Phase"}
{field.isEditing ? (
<ChevronUpIcon className="size-4" />
) : (
<ChevronDownIcon className="size-4" />
)}
</Button>
<AdminOnly contract={contract}>
<Button
variant="ghost"
onClick={toggleEditing}
onClick={onRemove}
disabled={isPending}
size="sm"
rightIcon={
field.isEditing ? (
<ChevronUpIcon className="size-4" />
) : (
<ChevronDownIcon className="size-4" />
)
}
className="gap-2 text-red-300"
>
{field.isEditing ? "Collapse" : isAdmin ? "Edit" : "See Phase"}
Remove <XIcon className="size-4" />
</Button>
<AdminOnly contract={contract}>
<Button
variant="ghost"
onClick={onRemove}
isDisabled={isPending}
colorScheme="red"
size="sm"
rightIcon={<XIcon className="size-4" />}
>
Remove
</Button>
</AdminOnly>
</Flex>
</AdminOnly>
</div>
<Flex flexDir="column" gap={2} mt={{ base: 4, md: 0 }}>
<Flex gap={3} alignItems="center">
<Heading>{ClaimConditionTypeData[claimConditionType].name}</Heading>
{isActive && (
<Badge colorScheme="green" borderRadius="lg" p={1.5}>
Currently active
</Badge>
<div className="mt-4 flex flex-col gap-2 md:mt-0">
<div className="flex flex-row items-center gap-3">
<p className="font-bold text-lg text-muted-foreground">
{ClaimConditionTypeData[claimConditionType].name}
</p>
{isActive && (
<Badge variant="success" className="rounded-lg p-1.5">
Currently active
</Badge>
)}
</div>
<p className="text-muted-foreground">
{ClaimConditionTypeData[claimConditionType].description}
</p>
</div>
{!field.isEditing ? (
<div className="grid grid-cols-2 gap-2 md:grid-cols-4">
<div className="flex flex-col">
<p className="font-bold text-muted-foreground">Phase start</p>
<p className="text-muted-foreground">
{field.startTime?.toLocaleString()}
</p>
</div>
<div className="flex flex-col">
<p className="font-bold text-muted-foreground">
{isErc20 ? "Tokens" : "NFTs"} to drop
</p>
<p className="text-muted-foreground capitalize">
{field.maxClaimableSupply}
</p>
</div>
<PricePreview
price={field.price}
currencyAddress={field.currencyAddress}
contractChainId={contract.chain.id}
/>
<div className="flex flex-col">
<p className="font-bold text-muted-foreground">Limit per wallet</p>
{claimConditionType === "specific" ? (
<p>Set in the snapshot</p>
) : claimConditionType === "creator" ? (
<p>Unlimited</p>
) : (
<p className="text-muted-foreground capitalize">
{field.maxClaimablePerWallet}
</p>
)}
</Flex>
</div>
</div>
) : (
<>
<CustomFormGroup>
{/* Phase Name Input / Form Title */}
{isMultiPhase ? <PhaseNameInput /> : null}
<PhaseStartTimeInput />
</CustomFormGroup>
<Text>{ClaimConditionTypeData[claimConditionType].description}</Text>
</Flex>
<CreatorInput
creatorAddress={
(field.snapshot?.[0] as { address: string })?.address
}
/>
{!field.isEditing ? (
<SimpleGrid columns={{ base: 2, md: 4 }} gap={2}>
<div className="flex flex-col">
<Text fontWeight="bold">Phase start</Text>
<Text>{field.startTime?.toLocaleString()}</Text>
</div>
<div className="flex flex-col">
<Text fontWeight="bold">
{isErc20 ? "Tokens" : "NFTs"} to drop
</Text>
<Text textTransform="capitalize">{field.maxClaimableSupply}</Text>
</div>
<PricePreview
price={field.price}
currencyAddress={field.currencyAddress}
contractChainId={contract.chain.id}
/>
<div className="flex flex-col">
<Text fontWeight="bold">Limit per wallet</Text>
{claimConditionType === "specific" ? (
<Text>Set in the snapshot</Text>
) : claimConditionType === "creator" ? (
<Text>Unlimited</Text>
) : (
<Text textTransform="capitalize">
{field.maxClaimablePerWallet}
</Text>
)}
</div>
</SimpleGrid>
) : (
<>
<CustomFormGroup>
<MaxClaimableSupplyInput />
<ClaimPriceInput contractChainId={contract.chain.id} />
</CustomFormGroup>
{claimConditionType === "specific" ||
claimConditionType === "creator" ? null : (
<CustomFormGroup>
{/* Phase Name Input / Form Title */}
{isMultiPhase ? <PhaseNameInput /> : null}
<PhaseStartTimeInput />
<MaxClaimablePerWalletInput />
</CustomFormGroup>
)}
<CreatorInput
creatorAddress={
(field.snapshot?.[0] as { address: string })?.address
}
/>
<CustomFormGroup>
<MaxClaimableSupplyInput />
<ClaimPriceInput contractChainId={contract.chain.id} />
</CustomFormGroup>
{claimConditionType === "specific" ||
claimConditionType === "creator" ? null : (
<CustomFormGroup>
<MaxClaimablePerWalletInput />
</CustomFormGroup>
)}
<ClaimerSelection />
</>
)}
</Flex>
<ClaimerSelection />
</>
)}
</Card>
);
};
@@ -1,6 +1,5 @@
import { CURRENCIES } from "constants/currencies";
import { useAllChainsData } from "hooks/chains/allChains";
import { Text } from "tw-components";
import { shortenIfAddress } from "utils/usedapp-external";
import { isAddressZero } from "utils/zeroAddress";
@@ -25,19 +24,19 @@ export const PricePreview: React.FC<PricePreviewProps> = ({
);
return (
<div className="flex flex-col">
<Text fontWeight="bold">Default price</Text>
<div className="flex flex-col text-muted-foreground">
<p className="font-bold">Default price</p>
{Number(price) === 0 ? (
<Text>Free</Text>
<p>Free</p>
) : (
<Text>
<p>
{price}{" "}
{foundCurrency
? foundCurrency.symbol
: isAddressZero(currencyAddress || "")
? chain?.nativeCurrency.symbol || "(Native Token)"
: `(${shortenIfAddress(currencyAddress)})`}
</Text>
</p>
)}
</div>
);
@@ -1,18 +1,14 @@
import {
Input,
InputGroup,
type InputProps,
InputRightElement,
} from "@chakra-ui/react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useEffect, useState } from "react";
import { Button } from "tw-components";
interface QuantityInputWithUnlimitedProps
extends Omit<InputProps, "onChange" | "value" | "onBlur" | "max" | "min"> {
interface QuantityInputWithUnlimitedProps {
value: string;
onChange: (value: string) => void;
hideMaxButton?: true;
decimals?: number;
isDisabled: boolean;
isRequired: boolean;
}
export const QuantityInputWithUnlimited: React.FC<
@@ -24,7 +20,6 @@ export const QuantityInputWithUnlimited: React.FC<
isDisabled,
isRequired,
decimals,
...restInputProps
}) => {
const [stringValue, setStringValue] = useState<string>(
Number.isNaN(Number(value)) ? "0" : value.toString(),
@@ -50,10 +45,10 @@ export const QuantityInputWithUnlimited: React.FC<
};
return (
<InputGroup {...restInputProps}>
<div className="flex flex-row items-center rounded-md border border-border">
<Input
isRequired={isRequired}
isDisabled={isDisabled}
required={isRequired}
disabled={isDisabled}
value={stringValue === "unlimited" ? "Unlimited" : stringValue}
onChange={(e) => updateValue(e.currentTarget.value)}
onBlur={() => {
@@ -65,23 +60,21 @@ export const QuantityInputWithUnlimited: React.FC<
setStringValue("0");
}
}}
className="border-none"
/>
{hideMaxButton ? null : (
<InputRightElement w="auto">
<Button
isDisabled={isDisabled}
colorScheme="primary"
variant="ghost"
size="sm"
mr={1}
onClick={() => {
updateValue("unlimited");
}}
>
Unlimited
</Button>
</InputRightElement>
<Button
disabled={isDisabled}
variant="ghost"
size="sm"
className="mr-1 text-primary"
onClick={() => {
updateValue("unlimited");
}}
>
Unlimited
</Button>
)}
</InputGroup>
</div>
);
};
@@ -17,7 +17,6 @@ import {
ChevronRightIcon,
} from "lucide-react";
import { type Column, usePagination, useTable } from "react-table";
import { Text } from "tw-components";
interface CsvDataTableProps<T extends object> {
data: T[];
@@ -78,9 +77,9 @@ export function CsvDataTable<T extends object>({
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
key={columnIndex}
>
<Text as="label" size="label.sm" color="faded">
<p className="text-muted-foreground">
{column.render("Header")}
</Text>
</p>
</Th>
))}
</Tr>
@@ -1,9 +1,10 @@
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Badge } from "@/components/ui/badge";
import { Flex, SimpleGrid, useBreakpointValue } from "@chakra-ui/react";
import { formatDistance } from "date-fns/formatDistance";
import { useAllChainsData } from "hooks/chains/allChains";
import { useActiveAccount } from "thirdweb/react";
import { Badge, Card, Heading, Text } from "tw-components";
import { Card, Heading, Text } from "tw-components";
export type AccountSignerType = {
signer: string;
@@ -45,19 +46,9 @@ export const AccountSigner: React.FC<AccountSignerProps> = ({
<WalletAddress shortenAddress={isMobile} address={signer} />
</Heading>
<div className="flex flex-row gap-2">
{isAdmin ? (
<Badge borderRadius="lg" p={1.5}>
Admin Key
</Badge>
) : (
<Badge borderRadius="lg" p={1.5}>
Scoped key
</Badge>
)}
{isAdmin ? <Badge>Admin Key</Badge> : <Badge>Scoped key</Badge>}
{signer === address && (
<Badge colorScheme="green" borderRadius="lg" p={1.5}>
Currently connected
</Badge>
<Badge variant="success">Currently connected</Badge>
)}
</div>
</Flex>
@@ -1,6 +1,7 @@
"use client";
import { Button } from "@/components/ui/button";
import { CodeClient } from "@/components/ui/code/code.client";
import { useApiKeys, useCreateApiKey } from "@3rdweb-sdk/react/hooks/useApi";
import { Flex, FormControl, Input, Select } from "@chakra-ui/react";
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
@@ -16,14 +17,7 @@ import type { StoredChain } from "stores/chainStores";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { useActiveAccount } from "thirdweb/react";
import {
Card,
CodeBlock,
FormHelperText,
FormLabel,
Heading,
Text,
} from "tw-components";
import { Card, FormHelperText, FormLabel, Heading, Text } from "tw-components";
interface EmbedSetupProps {
contract: ThirdwebContract;
@@ -304,7 +298,6 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
return (
<Flex gap={8} direction="column">
<LazyCreateAPIKeyDialog
wording="api-key"
prefill={{
name: "Embed API Key",
domains: "embed.ipfscdn.io",
@@ -536,12 +529,10 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
<Card className="flex w-full flex-col gap-2 md:w-1/2">
<Heading size="title.sm">Embed Code</Heading>
<CodeBlock
canCopy={false}
whiteSpace="pre"
overflowX="auto"
<CodeClient
code={embedCode}
language="markup"
lang="html"
loadingClassName="min-h-[190px]"
/>
<Button
className="w-auto gap-2"
@@ -1,5 +1,6 @@
"use client";
import { CodeClient } from "@/components/ui/code/code.client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import {
type InternalTransaction,
@@ -32,14 +33,7 @@ import { Fragment, useMemo, useState } from "react";
import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import { stringify } from "thirdweb/utils";
import {
Button,
Card,
CodeBlock,
FormLabel,
Heading,
Text,
} from "tw-components";
import { Button, Card, FormLabel, Heading, Text } from "tw-components";
interface EventsFeedProps {
contract: ThirdwebContract;
@@ -351,12 +345,12 @@ const EventsFeedItem: React.FC<EventsFeedItemProps> = ({
<Box gridColumn="span 3">
<Text fontWeight="bold">{event.eventName}</Text>
</Box>
<CodeBlock
gridColumn="span 9"
code={stringify(event.args)}
language="json"
/>
<Box gridColumn="span 9">
<CodeClient
code={stringify(event.args, null, 2)}
lang="json"
/>
</Box>
</SimpleGrid>
{arr.length - 1 === idx ? null : <Divider />}
@@ -4,6 +4,7 @@ import { UnexpectedValueErrorMessage } from "@/components/blocks/error-fallbacks
import { WalletAddress } from "@/components/blocks/wallet-address";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { CodeClient } from "@/components/ui/code/code.client";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
@@ -26,7 +27,8 @@ import type { ThirdwebContract } from "thirdweb";
import { getNFT as getErc721NFT } from "thirdweb/extensions/erc721";
import { getNFT as getErc1155NFT } from "thirdweb/extensions/erc1155";
import { useReadContract } from "thirdweb/react";
import { Badge, Button, Card, CodeBlock, Heading, Text } from "tw-components";
import {} from "tw-components";
import { Button, Card, Heading, Text } from "tw-components";
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
import { shortenString } from "utils/usedapp-external";
import { NftProperty } from "../components/nft-property";
@@ -158,12 +160,7 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
overflow={{ base: "auto", md: "hidden" }}
pb={{ base: 4, md: 0 }}
>
<ButtonGroup
size="sm"
variant="ghost"
spacing={2}
w={(tabs.length + 1) * 95}
>
<ButtonGroup size="sm" variant="ghost" spacing={2}>
<Button
type="button"
isActive={tab === "Details"}
@@ -241,11 +238,7 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
<GridItem colSpan={4}>
<Heading size="label.md">Token Standard</Heading>
</GridItem>
<GridItem colSpan={8}>
<Badge size="label.sm" variant="subtle">
{nft.type}
</Badge>
</GridItem>
<GridItem colSpan={8}>{nft.type}</GridItem>
{nft.type !== "ERC721" && (
<>
<GridItem colSpan={4}>
@@ -319,12 +312,9 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
))}
</SimpleGrid>
) : (
<CodeBlock
<CodeClient
code={JSON.stringify(properties, null, 2) || ""}
language="json"
canCopy={false}
wrap={false}
overflow="auto"
lang="json"
/>
)}
</Card>
@@ -1,5 +1,6 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
@@ -17,7 +18,6 @@ import type { ThirdwebContract } from "thirdweb";
import * as ERC721Ext from "thirdweb/extensions/erc721";
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
import { useReadContract, useSendAndConfirmTransaction } from "thirdweb/react";
import { Button } from "tw-components";
interface BatchLazyMintButtonProps {
canCreateDelayedRevealBatch: boolean;
@@ -55,10 +55,8 @@ export const BatchLazyMintButton: React.FC<BatchLazyMintButtonProps> = ({
<MinterOnly contract={contract}>
<Sheet onOpenChange={setOpen} open={open}>
<SheetTrigger asChild>
<Button
colorScheme="primary"
leftIcon={<FileStackIcon className="size-4" />}
>
<Button variant="primary" className="gap-2">
<FileStackIcon className="size-4" />
Batch Upload
</Button>
</SheetTrigger>
@@ -1,5 +1,6 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
@@ -18,7 +19,6 @@ import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
import { getApprovalForTransaction } from "thirdweb/extensions/erc20";
import { claimTo } from "thirdweb/extensions/erc721";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { Button } from "tw-components";
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
const CLAIM_FORM_ID = "nft-claim-form";
@@ -45,8 +45,8 @@ export const NFTClaimButton: React.FC<NFTClaimButtonProps> = ({ contract }) => {
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button colorScheme="primary" leftIcon={<GemIcon className="size-4" />}>
Claim
<Button variant="primary" className="gap-2">
<GemIcon className="size-4" /> Claim
</Button>
</SheetTrigger>
<SheetContent className="overflow-y-auto sm:w-[540px] sm:max-w-[90%] lg:w-[700px]">
@@ -1,4 +1,5 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
@@ -10,7 +11,6 @@ import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import type { ThirdwebContract } from "thirdweb";
import { Button } from "tw-components";
import { LazyMintNftForm } from "./lazy-mint-form";
interface NFTLazyMintButtonProps {
@@ -28,11 +28,8 @@ export const NFTLazyMintButton: React.FC<NFTLazyMintButtonProps> = ({
<MinterOnly contract={contract}>
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
colorScheme="primary"
leftIcon={<PlusIcon className="size-5" />}
{...restButtonProps}
>
<Button variant="primary" className="gap-2" {...restButtonProps}>
<PlusIcon className="size-5" />
Single Upload
</Button>
</SheetTrigger>
@@ -1,5 +1,6 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Accordion,
AccordionButton,
@@ -25,7 +26,6 @@ import { lazyMint as lazyMint721 } from "thirdweb/extensions/erc721";
import { lazyMint as lazyMint1155 } from "thirdweb/extensions/erc1155";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import {
Button,
FormErrorMessage,
FormHelperText,
FormLabel,
@@ -313,9 +313,9 @@ export const LazyMintNftForm: React.FC<LazyMintNftFormParams> = ({
</form>
<div className="mt-8 flex flex-row justify-end gap-3">
<Button
isDisabled={sendAndConfirmTx.isPending}
disabled={sendAndConfirmTx.isPending}
variant="outline"
mr={3}
className="mr-3"
onClick={() => setOpen(false)}
>
Cancel
@@ -1,5 +1,6 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
@@ -11,7 +12,6 @@ import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import type { ThirdwebContract } from "thirdweb";
import { Button } from "tw-components";
import { NFTMintForm } from "./mint-form";
interface NFTMintButtonProps {
@@ -30,11 +30,8 @@ export const NFTMintButton: React.FC<NFTMintButtonProps> = ({
<MinterOnly contract={contract}>
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
colorScheme="primary"
leftIcon={<PlusIcon className="size-5" />}
{...restButtonProps}
>
<Button variant="primary" className="gap-2" {...restButtonProps}>
<PlusIcon className="size-5" />
Mint
</Button>
</SheetTrigger>
@@ -1,5 +1,6 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
@@ -18,7 +19,7 @@ import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import { getBatchesToReveal, reveal } from "thirdweb/extensions/erc721";
import { useReadContract, useSendAndConfirmTransaction } from "thirdweb/react";
import { Button, FormErrorMessage, FormLabel } from "tw-components";
import { FormErrorMessage, FormLabel } from "tw-components";
interface NFTRevealButtonProps {
contract: ThirdwebContract;
@@ -48,11 +49,8 @@ export const NFTRevealButton: React.FC<NFTRevealButtonProps> = ({
<MinterOnly contract={contract}>
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
colorScheme="primary"
leftIcon={<EyeIcon className="size-4" />}
>
Reveal NFTs
<Button variant="primary" className="gap-2">
<EyeIcon className="size-4" /> Reveal NFTs
</Button>
</SheetTrigger>
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
@@ -1,4 +1,5 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
@@ -10,7 +11,6 @@ import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import type { ThirdwebContract } from "thirdweb";
import { Button } from "tw-components";
import { SharedMetadataForm } from "./shared-metadata-form";
interface NFTSharedMetadataButtonProps {
@@ -25,12 +25,8 @@ export const NFTSharedMetadataButton: React.FC<
<MinterOnly contract={contract}>
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
colorScheme="primary"
leftIcon={<PlusIcon className="size-5" />}
{...restButtonProps}
>
Set NFT Metadata
<Button variant="primary" className="gap-2" {...restButtonProps}>
<PlusIcon className="size-5" /> Set NFT Metadata
</Button>
</SheetTrigger>
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
@@ -1,5 +1,8 @@
"use client";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useResolveContractAbi } from "@3rdweb-sdk/react/hooks/useResolveContractAbi";
import {
@@ -21,7 +24,7 @@ import { CircleCheckIcon, CircleXIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import { Badge, Button, Card, Heading } from "tw-components";
import { Heading } from "tw-components";
interface ContractSourcesPageProps {
contract: ThirdwebContract;
@@ -91,9 +94,7 @@ const VerifyContractModal: React.FC<
<ModalHeader>
<Flex gap={2} align="center">
<Heading size="subtitle.md">Contract Verification</Heading>
<Badge variant="outline" colorScheme="purple" rounded="md" px={2}>
beta
</Badge>
<Badge>beta</Badge>
</Flex>
</ModalHeader>
<ModalCloseButton mt={2} />
@@ -213,11 +214,11 @@ export const ContractSourcesPage: React.FC<ContractSourcesPageProps> = ({
chainId={contract.chain.id}
contractAddress={contract.address}
/>
<Button variant="solid" colorScheme="purple" onClick={onOpen}>
<Button variant="primary" onClick={onOpen}>
Verify contract
</Button>
</Flex>
<Card p={0}>
<Card>
<SourcesPanel sources={sources} abi={abiQuery.data} />
</Card>
</Flex>
@@ -264,7 +265,7 @@ function RefreshContractMetadataButton(props: {
return (
<Button
isLoading={contractCacheMutation.isPending}
disabled={contractCacheMutation.isPending}
variant="outline"
onClick={() => {
toast.promise(contractCacheMutation.mutateAsync(), {
@@ -274,8 +275,9 @@ function RefreshContractMetadataButton(props: {
error: (e) => e?.message || "Failed to refresh contract data.",
});
}}
className="w-[182px]"
>
Refresh Contract Data
{contractCacheMutation.isPending ? <Spinner /> : "Refresh Contract Data"}
</Button>
);
}
@@ -1,5 +1,6 @@
"use client";
import { Button } from "@/components/ui/button";
import { useSplitDistributeFunds } from "@3rdweb-sdk/react/hooks/useSplit";
import { MismatchButton } from "components/buttons/MismatchButton";
import { TransactionButton } from "components/buttons/TransactionButton";
@@ -7,7 +8,6 @@ import { useTrack } from "hooks/analytics/useTrack";
import { useTxNotifications } from "hooks/useTxNotifications";
import { useMemo } from "react";
import type { ThirdwebContract } from "thirdweb";
import { Button } from "tw-components";
import type { Balance } from "../ContractSplitPage";
interface DistributeButtonProps {
@@ -90,7 +90,7 @@ export const DistributeButton: React.FC<DistributeButtonProps> = ({
if (numTransactions === 0) {
return (
<Button isDisabled colorScheme="primary" {...restButtonProps}>
<Button disabled variant="primary" {...restButtonProps}>
Nothing to distribute
</Button>
);
@@ -1,5 +1,6 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
@@ -7,12 +8,11 @@ import {
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Droplet } from "lucide-react";
import { DropletIcon } from "lucide-react";
import { useState } from "react";
import type { ThirdwebContract } from "thirdweb";
import { balanceOf } from "thirdweb/extensions/erc20";
import { useActiveAccount, useReadContract } from "thirdweb/react";
import { Button } from "tw-components";
import { TokenAirdropForm } from "./airdrop-form";
interface TokenAirdropButtonProps {
contract: ThirdwebContract;
@@ -33,12 +33,12 @@ export const TokenAirdropButton: React.FC<TokenAirdropButtonProps> = ({
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
colorScheme="primary"
leftIcon={<Droplet size={16} />}
variant="primary"
{...restButtonProps}
isDisabled={!hasBalance}
disabled={!hasBalance}
className="gap-2"
>
Airdrop
<DropletIcon size={16} /> Airdrop
</Button>
</SheetTrigger>
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
@@ -1,5 +1,6 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
@@ -23,7 +24,6 @@ import {
useSendAndConfirmTransaction,
} from "thirdweb/react";
import {
Button,
FormErrorMessage,
FormHelperText,
FormLabel,
@@ -59,12 +59,12 @@ export const TokenBurnButton: React.FC<TokenBurnButtonProps> = ({
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
colorScheme="primary"
leftIcon={<Flame size={16} />}
variant="primary"
{...restButtonProps}
isDisabled={!hasBalance}
disabled={!hasBalance}
className="gap-2"
>
Burn
<Flame size={16} /> Burn
</Button>
</SheetTrigger>
<SheetContent className="z-[10000]">
@@ -1,5 +1,6 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
@@ -11,7 +12,7 @@ import {
import { FormControl, Input } from "@chakra-ui/react";
import { TransactionButton } from "components/buttons/TransactionButton";
import { useTrack } from "hooks/analytics/useTrack";
import { Gem } from "lucide-react";
import { GemIcon } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -22,12 +23,7 @@ import {
useReadContract,
useSendAndConfirmTransaction,
} from "thirdweb/react";
import {
Button,
FormErrorMessage,
FormHelperText,
FormLabel,
} from "tw-components";
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
interface TokenClaimButtonProps {
contract: ThirdwebContract;
@@ -52,12 +48,8 @@ export const TokenClaimButton: React.FC<TokenClaimButtonProps> = ({
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
colorScheme="primary"
leftIcon={<Gem size={16} />}
{...restButtonProps}
>
Claim
<Button variant="primary" className="gap-2" {...restButtonProps}>
<GemIcon size={16} /> Claim
</Button>
</SheetTrigger>
<SheetContent className="z-[10000]">
@@ -1,5 +1,6 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
@@ -12,7 +13,7 @@ import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
import { FormControl, Input } from "@chakra-ui/react";
import { TransactionButton } from "components/buttons/TransactionButton";
import { useTrack } from "hooks/analytics/useTrack";
import { Plus } from "lucide-react";
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -23,7 +24,7 @@ import {
useReadContract,
useSendAndConfirmTransaction,
} from "thirdweb/react";
import { Button, FormErrorMessage, FormLabel } from "tw-components";
import { FormErrorMessage, FormLabel } from "tw-components";
interface TokenMintButtonProps {
contract: ThirdwebContract;
@@ -50,12 +51,8 @@ export const TokenMintButton: React.FC<TokenMintButtonProps> = ({
<MinterOnly contract={contract}>
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
colorScheme="primary"
leftIcon={<Plus size={16} />}
{...restButtonProps}
>
Mint
<Button variant="primary" {...restButtonProps} className="gap-2">
<PlusIcon size={16} /> Mint
</Button>
</SheetTrigger>
<SheetContent className="z-[10000]">
@@ -1,5 +1,6 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
@@ -12,7 +13,7 @@ import { FormControl, Input } from "@chakra-ui/react";
import { TransactionButton } from "components/buttons/TransactionButton";
import { SolidityInput } from "contract-ui/components/solidity-inputs";
import { useTrack } from "hooks/analytics/useTrack";
import { Send } from "lucide-react";
import { SendIcon } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -23,12 +24,7 @@ import {
useReadContract,
useSendAndConfirmTransaction,
} from "thirdweb/react";
import {
Button,
FormErrorMessage,
FormHelperText,
FormLabel,
} from "tw-components";
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
interface TokenTransferButtonProps {
contract: ThirdwebContract;
@@ -57,12 +53,12 @@ export const TokenTransferButton: React.FC<TokenTransferButtonProps> = ({
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
colorScheme="primary"
leftIcon={<Send size={16} />}
variant="primary"
{...restButtonProps}
isDisabled={!hasBalance}
disabled={!hasBalance}
className="gap-2"
>
Transfer
<SendIcon size={16} /> Transfer
</Button>
</SheetTrigger>
<SheetContent className="z-[10000]">
@@ -109,7 +109,7 @@ const OP_CTA = {
description:
"Successful applicants receive gas grants for use across all supported Optimism Superchain networks. These grants can sponsor gas fees for any onchain activity using our Account Abstraction tools.",
buttonText: "Apply now",
buttonLink: "/dashboard/settings/gas-credits",
buttonLink: "/team/~/~/settings/credits",
} satisfies ChainCTAProps;
const chainMetaRecord = {
@@ -1,3 +1,5 @@
"use client";
import { InlineCode } from "@/components/ui/inline-code";
import { TableContainer } from "@/components/ui/table";
import {
@@ -15,7 +17,6 @@ import { useTrack } from "hooks/analytics/useTrack";
import { useRpcValidation } from "hooks/chains/useRpcValidation";
import { AlertCircleIcon, CheckCircleIcon } from "lucide-react";
import { type ChangeEvent, useState } from "react";
import { Button, Card, FormLabel, Link } from "tw-components";
const StatusCheck = (props: {
@@ -0,0 +1,41 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import type { Metadata } from "next";
import Link from "next/link";
import ChainValidation from "./ChainValidation";
const title = "Validate Chain";
const description = "Validate a given chain is compatible with thirdweb.";
export const metadata: Metadata = {
title,
description,
robots: "noindex, nofollow",
};
export default function Page() {
return (
<ChakraProviderSetup>
<div className="container py-6">
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-2">
<h1 className="font-semibold text-2xl tracking-tight">
Validate Chain
</h1>
<p>
Validate a given chain is compatible with{" "}
<Link
href="/chainlist"
className="text-link-foreground hover:text-foreground"
>
thirdweb
</Link>
.
</p>
</div>
<ChainValidation />
</div>
</div>
</ChakraProviderSetup>
);
}
@@ -1,4 +1,5 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { notFound } from "next/navigation";
import { fetchDeployMetadata } from "thirdweb/contract";
import { DeployContractInfo } from "../../../published-contract/components/contract-info";
import { DeployFormForUri } from "../../../published-contract/components/uri-based-deploy";
@@ -16,7 +17,12 @@ export default async function DirectDeployPage(props: DirectDeployPageProps) {
client: getThirdwebClient(),
// force `ipfs://` prefix
uri: parsedUri.startsWith("ipfs://") ? parsedUri : `ipfs://${parsedUri}`,
});
}).catch(() => null);
if (!metadata) {
notFound();
}
return (
<div className="container flex flex-col gap-4 py-8">
<DeployContractInfo
@@ -0,0 +1,42 @@
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
import { DeployableContractTable } from "components/contract-components/contract-table";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Suspense } from "react";
export default async function DeployMultipleContractsPage(props: {
searchParams?: Promise<{
ipfs?: string[] | string;
}>;
}) {
const searchParams = await props.searchParams;
const ipfsHashes = searchParams?.ipfs;
if (!ipfsHashes || !Array.isArray(ipfsHashes) || ipfsHashes.length === 0) {
notFound();
}
return (
<div className="container flex grow flex-col py-10">
<h1 className="mb-1 font-semibold text-2xl tracking-tight">
Deploy Contracts
</h1>
<p className="text-muted-foreground">
Welcome to the thirdweb contract deployment flow.{" "}
<Link
className="text-link-foreground hover:text-foreground"
target="_blank"
href="https://portal.thirdweb.com/contracts/deploy/overview"
>
Learn more about deploying your contracts.
</Link>
</p>
<div className="h-6" />
<Suspense fallback={<GenericLoadingPage />}>
<DeployableContractTable contractIds={ipfsHashes} context="deploy" />
</Suspense>
</div>
);
}
@@ -25,7 +25,11 @@ export default async function PublishContractPage(
const publishMetadataFromUri = await fetchDeployMetadata({
uri: publishUri,
client: getThirdwebClient(),
});
}).catch(() => null);
if (!publishMetadataFromUri) {
notFound();
}
let publishMetadata = publishMetadataFromUri;
@@ -0,0 +1,42 @@
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
import { DeployableContractTable } from "components/contract-components/contract-table";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Suspense } from "react";
export default async function PublishMultipleContractsPage(props: {
searchParams?: Promise<{
ipfs?: string[] | string;
}>;
}) {
const searchParams = await props.searchParams;
const ipfsHashes = searchParams?.ipfs;
if (!ipfsHashes || !Array.isArray(ipfsHashes) || ipfsHashes.length === 0) {
notFound();
}
return (
<div className="container flex grow flex-col py-10">
<h1 className="mb-1 font-semibold text-2xl tracking-tight">
Publish Contracts
</h1>
<p className="text-muted-foreground">
Welcome to the thirdweb contract publish flow.{" "}
<Link
className="text-link-foreground hover:text-foreground"
target="_blank"
href="https://portal.thirdweb.com/contracts/publish/overview"
>
Learn more about publishing your contracts.
</Link>
</p>
<div className="h-6" />
<Suspense fallback={<GenericLoadingPage />}>
<DeployableContractTable contractIds={ipfsHashes} context="publish" />
</Suspense>
</div>
);
}
@@ -1,38 +0,0 @@
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
export function ConnectSidebarLayout(props: {
children: React.ReactNode;
}) {
return (
<SidebarLayout
sidebarLinks={[
{
label: "Analytics",
href: "/dashboard/connect/analytics",
},
{
label: "In-App Wallets",
href: "/dashboard/connect/in-app-wallets",
},
{
label: "Ecosystem Wallets",
href: "/dashboard/connect/ecosystem",
},
{
label: "Account Abstraction",
href: "/dashboard/connect/account-abstraction",
},
{
label: "Pay",
href: "/dashboard/connect/pay",
},
{
label: "Playground",
href: "https://playground.thirdweb.com/connect/sign-in/button",
},
]}
>
{props.children}
</SidebarLayout>
);
}
@@ -1,21 +0,0 @@
"use client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
import { ApiKeysMenu } from "../../../../../components/settings/ApiKeys/Menu";
export function AccountAbstractionAPIKeysMenu(props: {
apiKeys: Pick<ApiKey, "name" | "key">[];
selectedAPIKey: Pick<ApiKey, "name" | "key">;
}) {
const router = useDashboardRouter();
return (
<ApiKeysMenu
apiKeys={props.apiKeys}
selectedKey={props.selectedAPIKey}
onSelect={(key) => {
router.push(`/dashboard/connect/account-abstraction/${key.key}`);
}}
/>
);
}
@@ -1,27 +0,0 @@
"use client";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { CircleAlertIcon } from "lucide-react";
import { useActiveWalletChain } from "thirdweb/react";
import { isOpChainId } from "../../../../team/[team_slug]/[project_slug]/connect/account-abstraction/isOpChain";
export function OpChainAlert() {
const chain = useActiveWalletChain();
const isOpChain = chain?.id ? isOpChainId(chain.id) : false;
if (!isOpChain) {
return null;
}
return (
<Alert variant="info">
<CircleAlertIcon className="size-4" />
<AlertTitle>Using the gas credits for OP chain paymaster</AlertTitle>
<AlertDescription>
Credits will automatically be applied to cover gas fees for any onchain
activity across thirdweb services. <br />
Eligible chains: OP Mainnet, Base, Zora, Frax, Mode.
</AlertDescription>
</Alert>
);
}
@@ -1,25 +0,0 @@
import { TrackedLinkTW } from "@/components/ui/tracked-link";
export function PageHeader() {
return (
<div className="flex flex-col gap-1.5">
<h1 className="font-semibold text-2xl tracking-tight lg:text-3xl">
Account Abstraction
</h1>
<p className="text-muted-foreground text-sm">
Easily integrate Account abstraction (ERC-4337) compliant smart accounts
into your apps.{" "}
<TrackedLinkTW
target="_blank"
label="docs-wallets"
category="smart-wallet"
href="https://portal.thirdweb.com/wallets/smart-wallet"
className="text-link-foreground hover:text-foreground"
>
View Documentation
</TrackedLinkTW>
</p>
</div>
);
}
@@ -1,79 +0,0 @@
import { accountStatus } from "@3rdweb-sdk/react/hooks/useApi";
import { SmartWalletsBillingAlert } from "components/settings/ApiKeys/Alerts";
import { ConnectSDKCard } from "components/shared/ConnectSDKCard";
import { SmartWallets } from "components/smart-wallets";
import { redirect } from "next/navigation";
import { getAccount } from "../../../../../account/settings/getAccount";
import { AccountAbstractionAPIKeysMenu } from "../AccountAbstractionAPIKeysMenu";
import { OpChainAlert } from "../OpChainAlert";
import { PageHeader } from "../PageHeader";
import { getAASupportedAPIKeys } from "../getAASupportedAPIKeys";
export default async function Page(props: {
params: Promise<{
clientId: string;
}>;
searchParams: Promise<{
tab?: string;
}>;
}) {
const { clientId } = await props.params;
const dashboardAccount = await getAccount();
if (!dashboardAccount) {
redirect(
`/login?next=${encodeURIComponent(
`/dashboard/connect/account-abstraction/${clientId}`,
)}`,
);
}
const apiKeys = await getAASupportedAPIKeys();
const apiKey = apiKeys.find((key) => key.key === clientId);
if (!apiKey) {
redirect("/dashboard/connect/account-abstraction");
}
const hasSmartWalletsWithoutBilling = apiKeys.find((k) =>
k.services?.find(
(s) =>
dashboardAccount.status !== accountStatus.validPayment &&
s.name === "bundler",
),
);
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col content-start justify-between gap-4 lg:flex-row">
<PageHeader />
<div>
<AccountAbstractionAPIKeysMenu
apiKeys={apiKeys.map((x) => ({
name: x.name,
key: x.key,
}))}
selectedAPIKey={apiKey}
/>
</div>
</div>
{hasSmartWalletsWithoutBilling ? (
<SmartWalletsBillingAlert />
) : (
<OpChainAlert />
)}
<SmartWallets
apiKeyServices={apiKey.services || []}
trackingCategory="smart-wallet"
tab={(await props.searchParams).tab}
smartWalletsLayoutSlug={`/dashboard/connect/account-abstraction/${clientId}`}
clientId={apiKey.key}
/>
<ConnectSDKCard
title="Get Started"
description="Add account abstraction to your app with the Connect SDK."
/>
</div>
);
}
@@ -1,7 +0,0 @@
import { getApiKeys } from "../../../../api/lib/getAPIKeys";
export async function getAASupportedAPIKeys() {
return (await getApiKeys()).filter((key) => {
return !!(key.services || []).find((srv) => srv.name === "bundler");
});
}
@@ -1,29 +0,0 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import type { Metadata } from "next";
import { getAbsoluteUrl } from "../../../../../lib/vercel-utils";
export default function Layout(props: { children: React.ReactNode }) {
return <ChakraProviderSetup>{props.children}</ChakraProviderSetup>;
}
const seo = {
title: "The Complete Account Abstraction Toolkit | thirdweb",
desc: "Add account abstraction to your web3 app & unlock powerful features for seamless onboarding, customizable transactions, & maximum security. Get started.",
};
export const metadata: Metadata = {
title: seo.title,
description: seo.desc,
openGraph: {
title: seo.title,
description: seo.desc,
images: [
{
url: `${getAbsoluteUrl()}/assets/og-image/dashboard-wallets-smart-wallet.png`,
width: 1200,
height: 630,
alt: seo.title,
},
],
},
};
@@ -1,36 +0,0 @@
import { redirect } from "next/navigation";
import { NoApiKeys } from "../../../../../components/settings/ApiKeys/NoApiKeys";
import { ConnectSDKCard } from "../../../../../components/shared/ConnectSDKCard";
import { getAuthToken } from "../../../../api/lib/getAuthToken";
import { PageHeader } from "./PageHeader";
import { getAASupportedAPIKeys } from "./getAASupportedAPIKeys";
export default async function Page() {
const authToken = await getAuthToken();
if (!authToken) {
redirect(
`/login?next=${encodeURIComponent("/dashboard/connect/account-abstraction")}`,
);
}
const apiKeys = await getAASupportedAPIKeys();
const firstKey = apiKeys[0];
if (firstKey) {
redirect(`/dashboard/connect/account-abstraction/${firstKey.key}`);
}
return (
<div>
<PageHeader />
<div className="h-8" />
<NoApiKeys service="Account Abstraction" />
<div className="h-10" />
<ConnectSDKCard
title="Get Started"
description="Add account abstraction to your app with the Connect SDK."
/>
</div>
);
}
@@ -1,21 +0,0 @@
"use client";
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
import { useDashboardRouter } from "../../../../../../@/lib/DashboardRouter";
import { ApiKeysMenu } from "../../../../../../components/settings/ApiKeys/Menu";
export function AnalyticsPageAPIKeysMenu(props: {
apiKeys: Pick<ApiKey, "name" | "key">[];
selectedAPIKey: Pick<ApiKey, "name" | "key">;
}) {
const router = useDashboardRouter();
return (
<ApiKeysMenu
apiKeys={props.apiKeys}
selectedKey={props.selectedAPIKey}
onSelect={(key) => {
router.push(`/dashboard/connect/analytics/${key.key}`);
}}
/>
);
}
@@ -1,63 +0,0 @@
import { redirect } from "next/navigation";
import { ConnectSDKCard } from "../../../../../../components/shared/ConnectSDKCard";
import { getApiKeys } from "../../../../../api/lib/getAPIKeys";
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
import { ConnectAnalyticsDashboard } from "../../../../../team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboard";
import { AnalyticsPageAPIKeysMenu } from "./AnalyticsPageAPIKeysMenu";
export default async function Page(props: {
params: Promise<{
clientId: string;
}>;
}) {
const authToken = await getAuthToken();
const { clientId } = await props.params;
if (!authToken) {
redirect(
`/login?next=${encodeURIComponent(`/dashboard/connect/analytics/${clientId}`)}`,
);
}
const _apiKeys = await getApiKeys();
// slimmed down version to reduce client side payload
const apiKeys = _apiKeys.map((key) => ({
name: key.name,
key: key.key,
}));
const apiKey = apiKeys.find((key) => key.key === clientId);
if (!apiKey) {
redirect("/dashboard/connect/analytics");
}
return (
<div>
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 className="mb-1 font-semibold text-3xl tracking-tight">
Connect Analytics
</h1>
<p className="text-muted-foreground">
Visualize how users are connecting to your apps.
</p>
</div>
<div>
<AnalyticsPageAPIKeysMenu apiKeys={apiKeys} selectedAPIKey={apiKey} />
</div>
</div>
<div className="h-4 lg:h-8" />
<ConnectAnalyticsDashboard
clientId={apiKey.key}
connectLayoutSlug="/dashboard/connect"
/>
<div className="h-4 lg:h-8" />
<ConnectSDKCard description="Add the Connect SDK to your app to start collecting analytics." />
</div>
);
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,61 +0,0 @@
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { redirect } from "next/navigation";
import { ConnectSDKCard } from "../../../../../components/shared/ConnectSDKCard";
import { getApiKeys } from "../../../../api/lib/getAPIKeys";
import { getAuthToken } from "../../../../api/lib/getAuthToken";
// base page
// - if no keys -> show no keys page
// - else redirect to first key
export default async function Page() {
const authToken = await getAuthToken();
if (!authToken) {
redirect(
`/login?next=${encodeURIComponent("/dashboard/connect/analytics")}`,
);
}
const apiKeys = await getApiKeys();
const firstKey = apiKeys[0];
if (firstKey) {
redirect(`/dashboard/connect/analytics/${firstKey.key}`);
}
return <NoKeysCreatedPage />;
}
function NoKeysCreatedPage() {
return (
<div>
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 className="mb-1 font-semibold text-3xl tracking-tight">
Connect Analytics
</h1>
<p className="text-muted-foreground">
Visualize how users are connecting to your apps.
</p>
</div>
</div>
<div className="h-4 lg:h-8" />
<div className="flex flex-col items-center rounded-lg border border-border bg-muted/50 px-4 py-10 lg:px-6">
<h3 className="mb-3 font-semibold text-2xl">No API keys found</h3>
<p className="mb-6 text-muted-foreground text-sm">
Start using the Connect SDK in your app with a free API key.
</p>
<Button asChild variant="primary">
<Link href="/dashboard/settings/api-keys">Create API Key</Link>
</Button>
</div>
<div className="h-4 lg:h-8" />
<ConnectSDKCard description="Add the Connect SDK to your app to start collecting analytics." />
</div>
);
}
@@ -1,19 +0,0 @@
import { EcosystemLayoutSlug } from "./components/EcosystemSlugLayout";
export default async function Layout(props: {
children: React.ReactNode;
params: Promise<{ slug: string }>;
}) {
const params = await props.params;
const { children } = props;
return (
<EcosystemLayoutSlug
params={params}
ecosystemLayoutPath="/dashboard/connect/ecosystem"
>
{children}
</EcosystemLayoutSlug>
);
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,7 +0,0 @@
import { EcosystemCreatePage } from "./EcosystemCreatePage";
export default function Page() {
return (
<EcosystemCreatePage ecosystemLayoutPath="/dashboard/connect/ecosystem" />
);
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,7 +0,0 @@
import { EcosystemLandingPage } from "./EcosystemLandingPage";
export default function Page() {
return (
<EcosystemLandingPage ecosystemLayoutPath="/dashboard/connect/ecosystem" />
);
}
@@ -1,24 +0,0 @@
import { TrackedLinkTW } from "@/components/ui/tracked-link";
export function PageHeader() {
return (
<div>
<h1 className="font-semibold text-3xl tracking-tigher">In-App Wallets</h1>
<div className="h-3" />
<p className="max-w-[500px] text-muted-foreground ">
A wallet infrastructure that enables apps to create, manage, and control
their users wallets. Email login, social login, and bring-your-own auth
supported.{" "}
<TrackedLinkTW
target="_blank"
href="https://portal.thirdweb.com/connect/in-app-wallet/overview"
label="learn-more"
category="embedded-wallet"
className="text-link-foreground hover:text-foreground"
>
Learn more
</TrackedLinkTW>
</p>
</div>
);
}
@@ -1,32 +0,0 @@
"use client";
import { TabLinks } from "@/components/ui/tabs";
import { usePathname } from "next/navigation";
export function Tabs({ clientId }: { clientId: string }) {
const path = usePathname();
return (
<TabLinks
links={[
{
name: "Analytics",
href: `/dashboard/connect/in-app-wallets/${clientId}/analytics`,
isActive: path?.endsWith("/analytics") || false,
isDisabled: false,
},
{
name: "Users",
href: `/dashboard/connect/in-app-wallets/${clientId}/users`,
isActive: path?.endsWith("/users") || false,
isDisabled: false,
},
{
name: "Configuration",
href: `/dashboard/connect/in-app-wallets/${clientId}/config`,
isActive: path?.endsWith("/config") || false,
isDisabled: false,
},
]}
/>
);
}
@@ -1 +0,0 @@
export const TRACKING_CATEGORY = "embedded-wallet";
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,37 +0,0 @@
import type { Range } from "components/analytics/date-range-selector";
import { InAppWalletAnalytics } from "components/embedded-wallets/Analytics";
export default async function Page(props: {
params: Promise<{ clientId: string }>;
searchParams: Promise<{
from?: string;
to?: string;
type?: string;
interval?: string;
}>;
}) {
const searchParams = await props.searchParams;
const params = await props.params;
const range =
searchParams.from && searchParams.to
? {
type: searchParams.type ?? "last-120",
from: new Date(searchParams.from),
to: new Date(searchParams.to),
}
: undefined;
const interval: "day" | "week" = ["day", "week"].includes(
searchParams.interval ?? "",
)
? (searchParams.interval as "day" | "week")
: "week";
return (
<InAppWalletAnalytics
clientId={params.clientId}
interval={interval}
range={range as Range}
/>
);
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";

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