Compare commits

...
Author SHA1 Message Date
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
89728c6578 Version Packages (#4739)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-21 19:43:51 +12:00
Joaquim VergesandGitHub 2707db136e feat: allow linking identities when using in-app + smart wallets (#4737) 2024-09-21 19:34:23 +12:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
ca23a95bfd Version Packages (#4735)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-21 14:27:50 +12:00
Joaquim VergesandGitHub 997bfa84cc Update pregenerate docs (#4736)
Signed-off-by: Joaquim Verges <[email protected]>
2024-09-21 13:14:48 +12:00
gregfromstl 824b1bdb69 [SDK] Feature: Ecosystem account linking (#4733)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

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

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

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

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

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

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

we recently moved from react-query v4 to v5.

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

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

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

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

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

<!-- start pr-codex -->

---

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

<!-- start pr-codex -->

---

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

<!-- start pr-codex -->

---

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

<!-- start pr-codex -->

---

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

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

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

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

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

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

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

<!-- start pr-codex -->

---

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

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

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

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

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

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

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

<!-- end pr-codex -->
2024-09-19 05:55:18 +00:00
jnsdls 06cd6a62d9 fix split deployments (#4690)
FIXES: DASH-253
2024-09-19 05:29:13 +00:00
253 changed files with 5299 additions and 2022 deletions
+4 -1
View File
@@ -93,4 +93,7 @@ UNTHREAD_GROWTH_TIER_ID=""
UNTHREAD_PRO_TIER_ID=""
# Demo Engine
NEXT_PUBLIC_DEMO_ENGINE_URL=""
NEXT_PUBLIC_DEMO_ENGINE_URL=""
# API server secret (required for thirdweb.com SIWE login). Copy from Vercel.
API_SERVER_SECRET=""
+5
View File
@@ -14,6 +14,11 @@ module.exports = {
message:
'Are you *sure* you need to use "useEffect" here? If you loading any async function prefer using "useQuery".',
},
{
selector: "CallExpression[callee.name='createContext']",
message:
'Are you *sure* you need to use a "Context"? In almost all cases you should prefer passing props directly.',
},
{
selector: "CallExpression[callee.name='defineChain']",
message:
@@ -36,7 +36,7 @@ function Story() {
description="This is a description"
buttonLabel="Some Action"
buttonOnClick={() => {}}
isLoading={false}
isPending={false}
confirmationDialog={{
title: "This is confirmation title",
description: "This is confirmation description",
@@ -50,7 +50,7 @@ function Story() {
description="This is a description"
buttonLabel="Some Action"
buttonOnClick={() => {}}
isLoading={true}
isPending={true}
confirmationDialog={{
title: "This is confirmation title",
description: "This is confirmation description",
@@ -17,7 +17,7 @@ export function DangerSettingCard(props: {
description: string;
buttonLabel: string;
buttonOnClick: () => void;
isLoading: boolean;
isPending: boolean;
confirmationDialog: {
title: string;
description: string;
@@ -39,9 +39,9 @@ export function DangerSettingCard(props: {
<Button
variant="destructive"
className="bg-red-600 hover:bg-red-600/80 text-white font-semibold gap-2"
disabled={props.isLoading}
disabled={props.isPending}
>
{props.isLoading && <Spinner className="size-3" />}
{props.isPending && <Spinner className="size-3" />}
{props.buttonLabel}
</Button>
</DialogTrigger>
@@ -66,9 +66,9 @@ export function DangerSettingCard(props: {
variant="destructive"
className="bg-red-600 hover:bg-red-600/80 text-white font-semibold gap-2"
onClick={props.buttonOnClick}
disabled={props.isLoading}
disabled={props.isPending}
>
{props.isLoading && <Spinner className="size-3" />}
{props.isPending && <Spinner className="size-3" />}
{props.buttonLabel}
</Button>
</DialogFooter>
@@ -81,7 +81,7 @@ function Story() {
}}
saveButton={{
disabled: false,
isLoading: false,
isPending: false,
onClick: () => {},
}}
noPermissionText={"You do not have permission to edit this"}
@@ -100,7 +100,7 @@ function Story() {
}}
saveButton={{
disabled: false,
isLoading: false,
isPending: false,
onClick: () => {},
}}
noPermissionText={undefined}
@@ -119,7 +119,7 @@ function Story() {
}}
saveButton={{
disabled: true,
isLoading: false,
isPending: false,
onClick: () => {},
}}
noPermissionText={undefined}
@@ -138,7 +138,7 @@ function Story() {
}}
saveButton={{
disabled: false,
isLoading: true,
isPending: true,
onClick: () => {},
}}
noPermissionText={undefined}
@@ -15,7 +15,7 @@ export function SettingsCard(props: {
saveButton?: {
onClick?: () => void;
disabled: boolean;
isLoading: boolean;
isPending: boolean;
type?: "submit";
};
}) {
@@ -59,12 +59,12 @@ export function SettingsCard(props: {
size="sm"
className="gap-2"
onClick={props.saveButton.onClick}
disabled={props.saveButton.disabled || props.saveButton.isLoading}
disabled={props.saveButton.disabled || props.saveButton.isPending}
variant="outline"
type={props.saveButton.type}
>
{props.saveButton.isLoading && <Spinner className="size-3" />}
{props.saveButton.isLoading ? "Saving" : "Save"}
{props.saveButton.isPending && <Spinner className="size-3" />}
{props.saveButton.isPending ? "Saving" : "Save"}
</Button>
)}
</div>
@@ -0,0 +1,79 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useMemo, useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { MultiSelect } from "./multi-select";
const meta = {
title: "blocks/MultiSelect",
component: Story,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function createList(len: number) {
return Array.from({ length: len }, (_, i) => ({
value: `${i}`,
label: `Item ${i}`,
}));
}
function Story() {
return (
<div className="py-6 max-w-[600px] mx-auto w-full px-4 flex flex-col gap-6">
<VariantTest defaultValues={[]} storyLabel="5 items" listLen={5} />
<VariantTest defaultValues={[]} storyLabel="5000 items" listLen={5000} />
<VariantTest
defaultValues={["1", "3", "5"]}
storyLabel="20 items, 3 selected by default"
listLen={20}
/>
<VariantTest
defaultValues={["1", "2", "3", "4", "5", "6", "7"]}
storyLabel="20 items, 7 selected, show max 3"
listLen={20}
maxCount={3}
/>
</div>
);
}
function VariantTest(props: {
defaultValues: string[];
maxCount?: number;
storyLabel: string;
listLen: number;
}) {
const list = useMemo(() => createList(props.listLen), [props.listLen]);
const [values, setValues] = useState<string[]>(props.defaultValues);
return (
<BadgeContainer label={props.storyLabel}>
<MultiSelect
selectedValues={values}
options={list}
onSelectedValuesChange={(values) => {
setValues(values);
}}
placeholder="Select items"
maxCount={props.maxCount}
/>
</BadgeContainer>
);
}
@@ -0,0 +1,263 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import { CheckIcon, ChevronDown, SearchIcon, XIcon } from "lucide-react";
import * as React from "react";
import { useShowMore } from "../../lib/useShowMore";
import { ScrollShadow } from "../ui/ScrollShadow/ScrollShadow";
import { Input } from "../ui/input";
interface MultiSelectProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
options: {
label: string;
value: string;
}[];
selectedValues: string[];
onSelectedValuesChange: (value: string[]) => void;
placeholder: string;
/**
* Maximum number of items to display. Extra selected items will be summarized.
* Optional, defaults to 3.
*/
maxCount?: number;
className?: string;
}
export const MultiSelect = React.forwardRef<
HTMLButtonElement,
MultiSelectProps
>(
(
{
options,
onSelectedValuesChange,
placeholder,
maxCount = Number.POSITIVE_INFINITY,
className,
selectedValues,
...props
},
ref,
) => {
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const [searchValue, setSearchValue] = React.useState("");
const handleInputKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
setIsPopoverOpen(true);
} else if (event.key === "Backspace" && !event.currentTarget.value) {
const newSelectedValues = [...selectedValues];
newSelectedValues.pop();
onSelectedValuesChange(newSelectedValues);
}
},
[selectedValues, onSelectedValuesChange],
);
const toggleOption = React.useCallback(
(option: string) => {
const newSelectedValues = selectedValues.includes(option)
? selectedValues.filter((value) => value !== option)
: [...selectedValues, option];
onSelectedValuesChange(newSelectedValues);
},
[selectedValues, onSelectedValuesChange],
);
const handleClear = React.useCallback(() => {
onSelectedValuesChange([]);
}, [onSelectedValuesChange]);
const handleTogglePopover = () => {
setIsPopoverOpen((prev) => !prev);
};
const clearExtraOptions = React.useCallback(() => {
const newSelectedValues = selectedValues.slice(0, maxCount);
onSelectedValuesChange(newSelectedValues);
}, [selectedValues, onSelectedValuesChange, maxCount]);
// show 50 initially and then 20 more when reaching the end
const { itemsToShow, lastItemRef } = useShowMore<HTMLButtonElement>(50, 20);
const optionsToShow = React.useMemo(() => {
const searchValLowercase = searchValue.toLowerCase();
const filteredOptions = options.filter((option) => {
return option.label.toLowerCase().includes(searchValLowercase);
});
return filteredOptions.slice(0, itemsToShow);
}, [options, searchValue, itemsToShow]);
return (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
<Button
ref={ref}
{...props}
onClick={handleTogglePopover}
className={cn(
"flex w-full p-3 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit",
className,
)}
>
{selectedValues.length > 0 ? (
<div className="flex justify-between w-full">
{/* badges */}
<div className="flex flex-wrap items-center gap-1.5">
{selectedValues.slice(0, maxCount).map((value) => {
const option = options.find((o) => o.value === value);
return (
<ClosableBadge
key={value}
label={option?.label || ""}
onClose={() => toggleOption(value)}
/>
);
})}
{/* +X more */}
{selectedValues.length > maxCount && (
<ClosableBadge
label={`+ ${selectedValues.length - maxCount} more`}
onClose={clearExtraOptions}
/>
)}
</div>
<div className="flex items-center justify-between gap-2">
{/* Clear All */}
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
{/* biome-ignore lint/a11y/useFocusableInteractive: <explanation> */}
<div
role="button"
onClick={(event) => {
event.stopPropagation();
handleClear();
}}
className="hover:bg-muted p-1 rounded"
>
<XIcon className="h-4 cursor-pointer text-muted-foreground" />
</div>
<Separator
orientation="vertical"
className="flex min-h-6 h-full"
/>
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
</div>
</div>
) : (
<div className="flex items-center justify-between w-full">
<span className="text-sm text-muted-foreground">
{placeholder}
</span>
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
</div>
)}
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
align="center"
sideOffset={10}
onEscapeKeyDown={() => setIsPopoverOpen(false)}
style={{
width: "var(--radix-popover-trigger-width)",
maxHeight: "var(--radix-popover-content-available-height)",
}}
>
<div>
{/* Search */}
<div className="relative">
<Input
placeholder="Search"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
className="pl-10 py-4 !h-auto focus-visible:ring-0 focus-visible:ring-offset-0 border-0 border-b border-border rounded-b-none"
onKeyDown={handleInputKeyDown}
/>
<SearchIcon className="size-4 absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground" />
</div>
<ScrollShadow
scrollableClassName="max-h-[350px] p-1"
className="rounded"
>
{/* List */}
<div>
{optionsToShow.length === 0 && (
<div className="py-10 justify-center flex">
No results found
</div>
)}
{optionsToShow.map((option, i) => {
const isSelected = selectedValues.includes(option.value);
return (
<Button
key={option.value}
role="option"
aria-selected={isSelected}
onClick={() => toggleOption(option.value)}
variant="ghost"
className="cursor-pointer flex gap-3 py-2 px-3 w-full justify-start rounded-sm"
ref={
i === optionsToShow.length - 1 ? lastItemRef : undefined
}
>
<div
className={cn(
"flex size-4 items-center justify-center rounded-sm border border-foreground",
isSelected
? "bg-inverted text-inverted-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className="size-4" />
</div>
<span>{option.label}</span>
</Button>
);
})}
</div>
</ScrollShadow>
</div>
</PopoverContent>
</Popover>
);
},
);
function ClosableBadge(props: {
label: string;
onClose: () => void;
}) {
return (
<span className="rounded-xl py-1 px-2 text-xs flex items-center gap-3 bg-muted border border-border text-foreground">
{props.label}
<XIcon
className="h-3 w-3 cursor-pointer text-muted-foreground"
onClick={(e) => {
e.stopPropagation();
props.onClose();
}}
/>
</span>
);
}
MultiSelect.displayName = "MultiSelect";
@@ -7,8 +7,9 @@ import {
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useClipboard } from "hooks/useClipboard";
import { Check, Copy, ExternalLinkIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { useMemo } from "react";
import { type ThirdwebClient, isAddress } from "thirdweb";
import { ZERO_ADDRESS } from "thirdweb";
import {
@@ -45,7 +46,7 @@ export function WalletAddress(props: {
client: thirdwebClient,
});
const [isCopied, setIsCopied] = useState(false);
const { onCopy, hasCopied } = useClipboard(address, 2000);
if (!isAddress(address)) {
return <span>Invalid Address ({address})</span>;
@@ -56,16 +57,6 @@ export function WalletAddress(props: {
return <span className="font-mono cursor-pointer">{shortenedAddress}</span>;
}
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(address);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000); // Reset after 2 seconds
} catch (err) {
console.error("Failed to copy: ", err);
}
};
return (
<HoverCard>
<HoverCardTrigger asChild>
@@ -102,22 +93,22 @@ export function WalletAddress(props: {
<Button
variant="outline"
size="sm"
onClick={copyToClipboard}
onClick={onCopy}
className="flex items-center gap-2"
>
{isCopied ? (
{hasCopied ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
{isCopied ? "Copied!" : "Copy"}
{hasCopied ? "Copied!" : "Copy"}
</Button>
</div>
<p className="text-sm font-mono bg-muted p-2 rounded text-center">
{lessShortenedAddress}
</p>
<h3 className="text-lg font-semibold">Social Profiles</h3>
{profiles.isLoading ? (
{profiles.isPending ? (
<p className="text-sm text-muted-foreground">Loading profiles...</p>
) : !profiles.data?.length ? (
<p className="text-sm text-muted-foreground">No profiles found</p>
@@ -1,8 +1,8 @@
"use client";
import { cn } from "@/lib/utils";
import { useClipboard } from "hooks/useClipboard";
import { CheckIcon, CopyIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "./button";
import { ToolTipLabel } from "./tooltip";
@@ -12,20 +12,16 @@ export function CopyButton(props: {
iconClassName?: string;
variant?: "ghost" | "primary" | "secondary";
}) {
const [isCopied, setIsCopied] = useState(false);
const { hasCopied, onCopy } = useClipboard(props.text, 1000);
return (
<ToolTipLabel label="Copy">
<Button
variant={props.variant || "ghost"}
aria-label="Copy"
className={cn("h-auto w-auto p-1", props.className)}
onClick={() => {
navigator.clipboard.writeText(props.text);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 1000);
}}
onClick={onCopy}
>
{isCopied ? (
{hasCopied ? (
<CheckIcon
className={cn("size-4 text-green-500", props.iconClassName)}
/>
@@ -1,8 +1,8 @@
"use client";
import { cn } from "@/lib/utils";
import { useClipboard } from "hooks/useClipboard";
import { CheckIcon, CopyIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "./button";
import { ToolTipLabel } from "./tooltip";
@@ -22,8 +22,8 @@ export function CopyTextButton(props: {
copyIconPosition: "left" | "right";
onClick?: () => void;
}) {
const [isCopied, setIsCopied] = useState(false);
const copyButton = isCopied ? (
const { hasCopied, onCopy } = useClipboard(props.textToCopy, 1000);
const copyButton = hasCopied ? (
<CheckIcon
className={cn("size-3 text-green-500 shrink-0", props.iconClassName)}
/>
@@ -46,9 +46,7 @@ export function CopyTextButton(props: {
props.className,
)}
onClick={(e) => {
navigator.clipboard.writeText(props.textToCopy);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 1000);
onCopy();
e.stopPropagation();
props.onClick?.();
}}
@@ -71,8 +71,15 @@ export function ScrollShadow(props: {
});
});
content.addEventListener("scroll", handleScroll);
const resizeObserver = new ResizeObserver(() => {
handleScroll();
});
resizeObserver.observe(content);
return () => {
content.removeEventListener("scroll", handleScroll);
resizeObserver.disconnect();
};
}, []);
+37
View File
@@ -0,0 +1,37 @@
"use client";
import { useCallback, useState } from "react";
/**
*
* @internal
*/
export function useShowMore<T extends HTMLElement>(
initialItemsToShow: number,
itemsToAdd: number,
) {
// start with showing first `initialItemsToShow` items, when the last item is in view, show `itemsToAdd` more
const [itemsToShow, setItemsToShow] = useState(initialItemsToShow);
const lastItemRef = useCallback(
(node: T) => {
if (!node) {
return;
}
const observer = new IntersectionObserver(
(entries) => {
if (entries[0]?.isIntersecting) {
setItemsToShow((prev) => prev + itemsToAdd); // show 10 more items
}
},
{ threshold: 1 },
);
observer.observe(node);
// when the node is removed from the DOM, observer will be disconnected automatically by the browser
},
[itemsToAdd],
);
return { itemsToShow, lastItemRef };
}
+18 -1
View File
@@ -140,7 +140,24 @@
}
}
body,
html {
height: 100%;
}
body {
min-height: 100%;
}
/* Fix colors on autofilled inputs */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
/* Revert text color */
-webkit-text-fill-color: hsl(var(--foreground)) !important;
color: hsl(var(--foreground)) !important;
caret-color: hsl(var(--foreground)) !important;
/* Revert background color */
transition: background-color 5000s ease-in-out 0s;
}
@@ -104,4 +104,11 @@ export const engineKeys = {
[...engineKeys.all, engineId, "systemMetrics"] as const,
queueMetrics: (engineId: string) =>
[...engineKeys.all, engineId, "queueMetrics"] as const,
alertRules: (engineId: string) =>
[...engineKeys.all, engineId, "alertRules"] as const,
alerts: (engineId: string) =>
[...engineKeys.all, engineId, "alerts"] as const,
notificationChannels: (engineId: string) =>
[...engineKeys.all, engineId, "notificationChannels"] as const,
};
@@ -15,14 +15,15 @@ import {
useAddRecentlyUsedChainId,
useRecentlyUsedChains,
} from "hooks/chains/recentlyUsedChains";
import { useSetIsNetworkConfigModalOpen } from "hooks/networkConfigModal";
import { useTheme } from "next-themes";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useCallback, useMemo } from "react";
import { useCallback, useMemo, useState } from "react";
import type { Chain } from "thirdweb";
import { AutoConnect, ConnectButton, useConnectModal } from "thirdweb/react";
import { LazyConfigureNetworkModal } from "../../../../components/configure-networks/LazyConfigureNetworkModal";
import type { StoredChain } from "../../../../contexts/configured-chains";
import { useFavoriteChains } from "../../hooks/useFavoriteChains";
import { useLoggedInUser } from "../../hooks/useLoggedInUser";
import { popularChains } from "../popularChains";
@@ -38,12 +39,10 @@ export const CustomConnectWallet = (props: {
const { theme } = useTheme();
const recentChainsv4 = useRecentlyUsedChains();
const addRecentlyUsedChainId = useAddRecentlyUsedChainId();
// const setIsNetworkConfigModalOpen = useSetIsNetworkConfigModalOpen();
const t = theme === "light" ? "light" : "dark";
const allv4Chains = useSupportedChains();
const chainsRecord = useSupportedChainsRecord();
const favChainsQuery = useFavoriteChains();
const setIsNetworkConfigModalOpen = useSetIsNetworkConfigModalOpen();
const allChains = useMemo(() => {
return allv4Chains.map(mapV4ChainToV5Chain);
}, [allv4Chains]);
@@ -52,6 +51,12 @@ export const CustomConnectWallet = (props: {
return popularChains.map((x) => chainsRecord[x.id] || x);
}, [chainsRecord]);
const [isNetworkConfigModalOpen, setIsNetworkConfigModalOpen] =
useState(false);
const [editChain, setEditChain] = useState<StoredChain | undefined>(
undefined,
);
const chainSections = useMemo(() => {
return [
{
@@ -70,10 +75,10 @@ export const CustomConnectWallet = (props: {
}, [recentChainsv4, favChainsQuery.data, popularChainsWithMeta]);
// ensures login status on pages that need it
const { isLoading, isLoggedIn } = useLoggedInUser();
const { isPending, isLoggedIn } = useLoggedInUser();
const pathname = usePathname();
if (isLoading) {
if (isPending) {
return (
<>
<div className="w-[144px] h-[48px] bg-muted border border-border rounded-lg flex items-center justify-center">
@@ -102,50 +107,69 @@ export const CustomConnectWallet = (props: {
}
return (
<ConnectButton
theme={getSDKTheme(t)}
client={thirdwebClient}
connectModal={{
privacyPolicyUrl: "/privacy",
termsOfServiceUrl: "/tos",
showThirdwebBranding: false,
welcomeScreen: () => <ConnectWalletWelcomeScreen theme={t} />,
}}
appMetadata={{
name: "thirdweb",
logoUrl: "https://thirdweb.com/favicon.ico",
url: "https://thirdweb.com",
}}
onDisconnect={async () => {
try {
// log out the user
await fetch("/api/auth/logout", {
method: "POST",
});
} catch (err) {
console.error("Failed to log out", err);
}
}}
connectButton={{
className: props.connectButtonClassName,
}}
detailsButton={{
className: props.detailsButtonClassName,
}}
chains={allChains}
detailsModal={{
networkSelector: {
sections: chainSections,
onSwitch(chain) {
addRecentlyUsedChainId(chain.id);
<>
<ConnectButton
theme={getSDKTheme(t)}
client={thirdwebClient}
connectModal={{
privacyPolicyUrl: "/privacy",
termsOfServiceUrl: "/tos",
showThirdwebBranding: false,
welcomeScreen: () => <ConnectWalletWelcomeScreen theme={t} />,
}}
appMetadata={{
name: "thirdweb",
logoUrl: "https://thirdweb.com/favicon.ico",
url: "https://thirdweb.com",
}}
onDisconnect={async () => {
try {
// log out the user
await fetch("/api/auth/logout", {
method: "POST",
});
} catch (err) {
console.error("Failed to log out", err);
}
}}
connectButton={{
className: props.connectButtonClassName,
}}
detailsButton={{
className: props.detailsButtonClassName,
}}
chains={allChains}
detailsModal={{
networkSelector: {
sections: chainSections,
onSwitch(chain) {
addRecentlyUsedChainId(chain.id);
},
renderChain(props) {
return (
<CustomChainRenderer
{...props}
openEditChainModal={(c) => {
setIsNetworkConfigModalOpen(true);
setEditChain(c);
}}
/>
);
},
onCustomClick: () => {
setEditChain(undefined);
setIsNetworkConfigModalOpen(true);
},
},
renderChain: CustomChainRenderer,
onCustomClick: () => {
setIsNetworkConfigModalOpen(true);
},
},
}}
/>
}}
/>
<LazyConfigureNetworkModal
open={isNetworkConfigModalOpen}
onOpenChange={setIsNetworkConfigModalOpen}
editChain={editChain}
/>
</>
);
};
@@ -17,10 +17,14 @@ export type EVMContractInfo = {
type SetEVMContractInfo = (info: EVMContractInfo) => void;
// Legacy: will be removed with contract page -> app router refactor
// eslint-disable-next-line no-restricted-syntax
const EVMContractInfoContext = createContext<EVMContractInfo | undefined>(
undefined,
);
// Legacy: will be removed with contract page -> app router refactor
// eslint-disable-next-line no-restricted-syntax
const SetEVMContractInfoContext = createContext<SetEVMContractInfo | undefined>(
undefined,
);
@@ -1539,3 +1539,177 @@ export function useEngineSystemMetrics(engineId: string) {
refetchInterval: 5_000,
});
}
export interface EngineAlertRule {
id: string;
title: string;
// A unique string identifying the alert.
// Note the "." which allows `subscriptionRoutes` to use wildcards in notificationChannel.
// Example: "alert.sla-5xx-99"
routingKey: string;
description: string;
createdAt: Date;
pausedAt: Date | null;
}
export function useEngineAlertRules(engineId: string) {
const { isLoggedIn } = useLoggedInUser();
return useQuery({
queryKey: engineKeys.alertRules(engineId),
queryFn: async () => {
const res = await fetch(
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/alert-rules`,
{ method: "GET" },
);
if (!res.ok) {
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
}
const json = await res.json();
return json.data as EngineAlertRule[];
},
enabled: isLoggedIn,
});
}
export interface EngineAlert {
id: string;
alertRuleId: string;
status: "pending" | "firing" | "resolved";
startsAt: Date;
endsAt: Date | null;
}
export function useEngineAlerts(engineId: string, limit: number, offset = 0) {
const { isLoggedIn } = useLoggedInUser();
return useQuery({
queryKey: engineKeys.alerts(engineId),
queryFn: async () => {
const res = await fetch(
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/alerts?limit=${limit}&offset=${offset}`,
{ method: "GET" },
);
if (!res.ok) {
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
}
const json = await res.json();
return json.data as EngineAlert[];
},
enabled: isLoggedIn,
});
}
export const EngineNotificationChannelTypeConfig = {
slack: {
display: "Slack",
valueDisplay: "Slack Webhook URL",
},
email: {
display: "Email",
valueDisplay: "Email Address",
},
} as const;
type EngineNotificationChannelType =
keyof typeof EngineNotificationChannelTypeConfig;
export type EngineNotificationChannel = {
id: string;
type: EngineNotificationChannelType;
value: string;
createdAt: Date;
pausedAt: Date | null;
// A list of routingKeys to listen to. Supports wildcards.
// Example: [ 'alert.sla-5xx-99' ] or [ 'alert.*' ] will both notify when
// the alert with routingKey `alert.sla-5xx-99` triggers.
subscriptionRoutes: string[];
};
export function useEngineNotificationChannels(engineId: string) {
const { isLoggedIn } = useLoggedInUser();
return useQuery({
queryKey: engineKeys.notificationChannels(engineId),
queryFn: async () => {
const res = await fetch(
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels`,
{ method: "GET" },
);
if (!res.ok) {
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
}
const json = await res.json();
return json.data as EngineNotificationChannel[];
},
enabled: isLoggedIn,
});
}
export interface CreateNotificationChannelInput {
subscriptionRoutes: string[];
type: "slack" | "email"; // TODO: Add others when implemented.
value: string;
}
export function useEngineCreateNotificationChannel(engineId: string) {
const { isLoggedIn } = useLoggedInUser();
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: CreateNotificationChannelInput) => {
invariant(isLoggedIn, "Must be logged in.");
const res = await fetch(
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels`,
{
method: "POST",
body: JSON.stringify(input),
},
);
if (!res.ok) {
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
}
const json = await res.json();
return json.data as EngineNotificationChannel;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.notificationChannels(engineId),
});
},
},
);
}
export function useEngineDeleteNotificationChannel(engineId: string) {
const { isLoggedIn } = useLoggedInUser();
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (notificationChannelId: string) => {
invariant(isLoggedIn, "Must be logged in.");
const res = await fetch(
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels/${notificationChannelId}`,
{ method: "DELETE" },
);
if (!res.ok) {
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
}
res.body?.cancel();
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.notificationChannels(engineId),
});
},
},
);
}
@@ -25,7 +25,7 @@ export function useFavoriteChains() {
}, [favChainsQuery.data, allChains]);
return {
isLoading: favChainsQuery.isLoading,
isPending: favChainsQuery.isPending,
data,
};
}
@@ -17,7 +17,7 @@ declare global {
}
export function useLoggedInUser(): {
isLoading: boolean;
isPending: boolean;
isLoggedIn: boolean;
user: { address: string; jwt?: string } | null;
} {
@@ -82,7 +82,7 @@ export function useLoggedInUser(): {
// if we are "disconnected" we are not logged in
if (connectionStatus === "disconnected") {
return {
isLoading: false,
isPending: false,
isLoggedIn: false,
user: null,
};
@@ -91,7 +91,7 @@ export function useLoggedInUser(): {
// if we are still connecting, we are "loading"
if (connectionStatus === "connecting") {
return {
isLoading: true,
isPending: true,
isLoggedIn: false,
user: null,
};
@@ -100,7 +100,7 @@ export function useLoggedInUser(): {
// same if we do not yet have a path
if (!pathname) {
return {
isLoading: true,
isPending: true,
isLoggedIn: false,
user: null,
};
@@ -109,7 +109,7 @@ export function useLoggedInUser(): {
// if we do not have an address we are not logged in
if (!connectedAddress) {
return {
isLoading: false,
isPending: false,
isLoggedIn: false,
user: null,
};
@@ -118,7 +118,7 @@ export function useLoggedInUser(): {
// if we are not on a logged in path, we can simply return the connected address
if (!isLoginRequired(pathname)) {
return {
isLoading: false,
isPending: false,
isLoggedIn: true,
user: { address: connectedAddress },
};
@@ -126,7 +126,7 @@ export function useLoggedInUser(): {
// otherwise we return the query data
return {
isLoading: query.isLoading,
isPending: query.isPending,
isLoggedIn: query.data ? query.data.isLoggedIn : false,
user: query.data?.isLoggedIn
? { address: connectedAddress, jwt: query.data.jwt }
@@ -118,7 +118,7 @@ export function FaucetButton({
faucetWalletBalanceQuery.data.value < toUnits("1", 17);
// loading state
if (faucetWalletBalanceQuery.isLoading || canClaimFaucetQuery.isLoading) {
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
return (
<Button variant="outline" className="w-full gap-2">
Checking Faucet <Spinner className="size-3" />
@@ -92,7 +92,7 @@ export function StarButton(props: {
disabled={
!loggedInUser.user?.address ||
mutation.isPending ||
favChainsQuery.isLoading
favChainsQuery.isPending
}
>
<ToolTipLabel label={label}>
@@ -64,9 +64,9 @@ function EcosystemSelect(props: {
ecosystem: Ecosystem;
ecosystemLayoutPath: string;
}) {
const { data: ecosystems, isLoading } = useEcosystemList();
const { data: ecosystems, isPending } = useEcosystemList();
return isLoading ? (
return isPending ? (
<Skeleton className="h-10 w-full md:w-[160px]" />
) : (
<DropdownMenu>
@@ -22,9 +22,9 @@ import { usePartners } from "../../hooks/use-partners";
import { UpdatePartnerModal } from "../client/update-partner-modal.client";
export function PartnersTable({ ecosystem }: { ecosystem: Ecosystem }) {
const { partners, isLoading } = usePartners({ ecosystem });
const { partners, isPending } = usePartners({ ecosystem });
if (isLoading) {
if (isPending) {
return (
<div className="flex flex-col gap-2">
{Array.from({ length: 3 }).map((_, i) => (
@@ -53,7 +53,7 @@ export function PartnersTable({ ecosystem }: { ecosystem: Ecosystem }) {
</TableHeader>
<TableBody>
{[...partners].reverse().map((partner: Partner) => (
<EcosystemRow
<PartnerRow
key={partner.id}
partner={partner}
ecosystem={ecosystem}
@@ -65,7 +65,7 @@ export function PartnersTable({ ecosystem }: { ecosystem: Ecosystem }) {
);
}
function EcosystemRow(props: {
function PartnerRow(props: {
partner: Partner;
ecosystem: Ecosystem;
}) {
@@ -114,7 +114,7 @@ function EcosystemRow(props: {
: "Never prompt"}
</TableCell>
<td className="table-cell py-1 align-middle">
<div className="flex gap-1.5 justify-end">
<div className="flex gap-1.5 pr-1.5 justify-end">
<UpdatePartnerModal
partner={props.partner}
ecosystem={props.ecosystem}
@@ -29,7 +29,7 @@ export function usePartners({ ecosystem }: { ecosystem: Ecosystem }) {
});
return {
isLoading: user?.jwt ? partnersQuery.isLoading : false,
isPending: user?.jwt ? partnersQuery.isPending : false,
partners: (partnersQuery.data ?? []) satisfies Partner[],
};
}
@@ -26,7 +26,7 @@ export function useEcosystemList() {
return {
...ecosystemQuery,
isLoading: isLoggedIn ? ecosystemQuery.isLoading : false,
isPending: isLoggedIn ? ecosystemQuery.isPending : false,
ecosystems: (ecosystemQuery.data ?? []) satisfies Ecosystem[],
};
}
@@ -78,7 +78,7 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) {
},
});
if (webhooksQuery.isLoading) {
if (webhooksQuery.isPending) {
return <Spinner className="size-8 mx-auto" />;
}
@@ -50,7 +50,8 @@ export const TransactionSimulator = (props: {
}) => {
const activeAccount = useActiveAccount();
const initialFormValues = props.searchParams;
const [isLoading, setIsLoading] = useState(false);
// TODO - replace this with a mutation.isPending
const [isPending, setIsPending] = useState(false);
const [state, setState] = useState<State>({
success: false,
message: "",
@@ -66,7 +67,7 @@ export const TransactionSimulator = (props: {
async function handleSimulation(data: SimulateTransactionForm) {
try {
setIsLoading(true);
setIsPending(true);
const { from, to, value, functionArgs, functionName } = data;
if (!chain) {
throw new Error("Invalid chainId");
@@ -154,7 +155,7 @@ ${Object.keys(populatedTransaction)
shareUrl: "",
});
}
setIsLoading(false);
setIsPending(false);
}
return (
<div className="max-w-[800px] space-y-4">
@@ -259,7 +260,7 @@ ${Object.keys(populatedTransaction)
</div>
<Button type="submit">
{isLoading ? (
{isPending ? (
<>
<Spinner className="w-4 h-4 mr-2" />
Simulating
@@ -341,11 +342,11 @@ const getCodeExample = (
}
return item;
});
return `import {
return `import {
getContract,
defineChain,
prepareContractCall,
createThirdwebClient
createThirdwebClient
} from "thirdweb";
const client = createThirdwebClient({
@@ -85,7 +85,7 @@ function AccountAvatarFormControl() {
saveButton={{
onClick: handleSave,
disabled: false,
isLoading: updateAvatarMutation.isPending,
isPending: updateAvatarMutation.isPending,
}}
noPermissionText={undefined}
errorText={undefined}
@@ -143,7 +143,7 @@ function AccountNameFormControl(props: {
saveButton={{
onClick: handleSave,
disabled: accountName.length === 0,
isLoading: updateAccountNameMutation.isPending,
isPending: updateAccountNameMutation.isPending,
}}
errorText={undefined}
noPermissionText={undefined} // TODO
@@ -191,7 +191,7 @@ function DeleteAccountCard() {
description={description}
buttonLabel={title}
buttonOnClick={handleDelete}
isLoading={deleteAccount.isPending}
isPending={deleteAccount.isPending}
confirmationDialog={{
title: "Are you sure you want to delete your account?",
description:
+25 -11
View File
@@ -13,34 +13,46 @@ import type {
const THIRDWEB_API_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
const THIRDWEB_API_SECRET = process.env.API_SERVER_SECRET || "";
export async function getLoginPayload(
params: GenerateLoginPayloadParams,
): Promise<LoginPayload> {
const res = await fetch(`${THIRDWEB_API_HOST}/v1/auth/payload`, {
if (!THIRDWEB_API_SECRET) {
throw new Error("API_SERVER_SECRET is not set");
}
const res = await fetch(`${THIRDWEB_API_HOST}/v2/siwe/payload`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-service-api-key": THIRDWEB_API_SECRET,
},
body: JSON.stringify({
address: params.address,
chainId: params.chainId?.toString(),
}),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
console.error("Failed to fetch login payload", res.status, res.statusText);
throw new Error("Failed to fetch login payload");
}
return (await res.json()).payload;
return (await res.json()).data.payload;
}
export async function doLogin(payload: VerifyLoginPayloadParams) {
if (!THIRDWEB_API_SECRET) {
throw new Error("API_SERVER_SECRET is not set");
}
// forward the request to the API server
const res = await fetch(`${THIRDWEB_API_HOST}/v1/auth/login`, {
const res = await fetch(`${THIRDWEB_API_HOST}/v2/siwe/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-service-api-key": THIRDWEB_API_SECRET,
},
body: JSON.stringify({ payload }),
body: JSON.stringify(payload),
});
if (!res.ok) {
@@ -67,7 +79,9 @@ export async function doLogin(payload: VerifyLoginPayloadParams) {
const json = await res.json();
if (!json.token) {
const jwt = json.data.jwt;
if (!jwt) {
console.error("Failed to login - invalid json", json);
throw new Error("Failed to login - invalid json");
}
@@ -77,7 +91,7 @@ export async function doLogin(payload: VerifyLoginPayloadParams) {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${json.token}`,
Authorization: `Bearer ${jwt}`,
},
});
// if we do not have a user, create one
@@ -85,7 +99,7 @@ export async function doLogin(payload: VerifyLoginPayloadParams) {
const newUserRes = await fetch(`${THIRDWEB_API_HOST}/v1/account/create`, {
method: "POST",
headers: {
Authorization: `Bearer ${json.token}`,
Authorization: `Bearer ${jwt}`,
},
});
@@ -104,7 +118,7 @@ export async function doLogin(payload: VerifyLoginPayloadParams) {
// set the token cookie
cookieStore.set(
COOKIE_PREFIX_TOKEN + getAddress(payload.payload.address),
json.token,
jwt,
{
httpOnly: true,
secure: true,
+15 -14
View File
@@ -13,25 +13,25 @@ import { doLogin, doLogout, getLoginPayload, isLoggedIn } from "./auth-actions";
export default function LoginPage() {
return (
<div className="bg-background h-full">
<div className="bg-muted/30 h-screen relative grid place-items-center">
<nav className="fixed top-0 w-full flex flex-row justify-between items-center px-6 py-4 z-20">
<ThirdwebMiniLogo className="max-h-7" />
<ColorModeToggle />
</nav>
<main className="h-full relative grid place-items-center">
<div className="flex flex-col gap-8 z-10">
<h1 className="font-semibold text-2xl">Get started with thirdweb</h1>
<Suspense>
<CustomConnectEmmbed />
</Suspense>
</div>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
alt=""
src="/assets/login/background.svg"
className="fixed bottom-0 right-0"
/>
<main className="flex flex-col gap-6 z-10">
<h1 className="font-semibold text-2xl lg:text-3xl tracking-tight text-center">
Get started with thirdweb
</h1>
<Suspense>
<CustomConnectEmmbed />
</Suspense>
</main>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
alt=""
src="/assets/login/background.svg"
className="fixed -bottom-12 -right-12 lg:bottom-0 lg:right-0"
/>
</div>
);
}
@@ -77,6 +77,7 @@ function CustomConnectEmmbed() {
client={client}
modalSize={isLG ? "wide" : "compact"}
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
className="shadow-lg"
/>
);
}
@@ -2,14 +2,13 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
import { Flex } from "@chakra-ui/react";
import { ApplyForOpCreditsModal } from "components/onboarding/ApplyForOpCreditsModal";
import { Heading, LinkButton } from "tw-components";
export const SettingsGasCreditsPage = () => {
const { isLoading } = useLoggedInUser();
const { isPending } = useLoggedInUser();
if (isLoading) {
if (isPending) {
return (
<div className="grid w-full min-h-[400px] place-items-center">
<Spinner className="size-10" />
@@ -18,8 +17,8 @@ export const SettingsGasCreditsPage = () => {
}
return (
<Flex flexDir="column" gap={8}>
<Flex direction="row" gap={4} align="center">
<div className="flex flex-col gap-8">
<div className="flex flex-row gap-4 items-center">
<Heading size="title.lg" as="h1">
Apply to the Optimism Superchain App Accelerator
</Heading>
@@ -32,9 +31,9 @@ export const SettingsGasCreditsPage = () => {
>
Learn More
</LinkButton>
</Flex>
</div>
<ApplyForOpCreditsModal />
</Flex>
</div>
);
};
@@ -63,7 +63,7 @@ function TeamNameFormControl(props: {
saveButton={{
onClick: handleSave,
disabled: teamName.length === 0,
isLoading: updateTeamMutation.isPending,
isPending: updateTeamMutation.isPending,
}}
errorText={undefined}
noPermissionText={undefined} // TODO
@@ -121,7 +121,7 @@ function TeamSlugFormControl(props: {
saveButton={{
onClick: handleSave,
disabled: teamSlug.length === 0,
isLoading: updateTeamMutation.isPending,
isPending: updateTeamMutation.isPending,
}}
noPermissionText={undefined} // TODO
>
@@ -168,7 +168,7 @@ function TeamAvatarFormControl() {
saveButton={{
onClick: handleSave,
disabled: false,
isLoading: updateTeamAvatarMutation.isPending,
isPending: updateTeamAvatarMutation.isPending,
}}
noPermissionText={undefined} // TODO
errorText={undefined}
@@ -250,7 +250,7 @@ export function LeaveTeamCard(props: {
description={description}
buttonLabel={title}
buttonOnClick={handleLeave}
isLoading={leaveTeam.isPending}
isPending={leaveTeam.isPending}
confirmationDialog={{
title: `Are you sure you want to leave team "${props.teamName}" ?`,
description:
@@ -311,7 +311,7 @@ export function DeleteTeamCard(props: {
description={description}
buttonLabel={title}
buttonOnClick={handleDelete}
isLoading={deleteTeam.isPending}
isPending={deleteTeam.isPending}
confirmationDialog={{
title: `Are you sure you want to delete team "${props.teamName}" ?`,
description: description,
@@ -2,7 +2,7 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { useAccount, useAccountUsage } from "@3rdweb-sdk/react/hooks/useApi";
import { Flex, HStack } from "@chakra-ui/react";
import { HStack } from "@chakra-ui/react";
import { BillingPeriod } from "components/settings/Account/Billing/Period";
import { BillingPlan } from "components/settings/Account/Billing/Plan";
import { Usage } from "components/settings/Account/Usage";
@@ -12,7 +12,7 @@ export const SettingsUsagePage = () => {
const usageQuery = useAccountUsage();
const account = meQuery.data;
if (meQuery.isLoading || !account) {
if (meQuery.isPending || !account) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<Spinner className="size-10" />
@@ -21,8 +21,8 @@ export const SettingsUsagePage = () => {
}
return (
<Flex flexDir="column" gap={8}>
<Flex direction="column">
<div className="flex flex-col gap-8">
<div className="flex flex-col">
<h1 className="font-semibold text-3xl tracking-tight mb-2">Usage</h1>
<HStack
justifyContent="space-between"
@@ -32,9 +32,9 @@ export const SettingsUsagePage = () => {
<BillingPlan account={account} />
<BillingPeriod account={account} usage={usageQuery.data} />
</HStack>
</Flex>
</div>
<Usage usage={usageQuery.data} usageLoading={usageQuery.isLoading} />
</Flex>
<Usage usage={usageQuery.data} usagePending={usageQuery.isPending} />
</div>
);
};
@@ -65,7 +65,7 @@ export function AccountAbstractionPage(props: {
<div className="h-6" />
{looggedInUserQuery.isLoading ? (
{looggedInUserQuery.isPending ? (
<div className="flex items-center justify-center h-[400px] border border-border rounded-lg">
<Spinner className="size-14" />
</div>
@@ -34,8 +34,8 @@ export function ConnectAnalyticsDashboard(props: {
<ConnectAnalyticsDashboardUI
walletUsage={walletUsageQuery.data || []}
aggregateWalletUsage={walletUsageAggregateQuery.data || []}
isLoading={
walletUsageQuery.isLoading || walletUsageAggregateQuery.isLoading
isPending={
walletUsageQuery.isPending || walletUsageAggregateQuery.isPending
}
/>
);
@@ -9,7 +9,7 @@ import { WalletDistributionChartCard } from "./_components/WalletDistributionCha
export function ConnectAnalyticsDashboardUI(props: {
walletUsage: WalletStats[];
aggregateWalletUsage: WalletStats[];
isLoading: boolean;
isPending: boolean;
}) {
const { totalWallets, uniqueWallets } = useMemo(() => {
return props.aggregateWalletUsage.reduce(
@@ -36,17 +36,17 @@ export function ConnectAnalyticsDashboardUI(props: {
<DailyConnectionsChartCard
walletStats={props.walletUsage}
isLoading={props.isLoading}
isPending={props.isPending}
/>
<WalletConnectorsChartCard
walletStats={props.walletUsage}
isLoading={props.isLoading}
isPending={props.isPending}
/>
<WalletDistributionChartCard
walletStats={props.walletUsage}
isLoading={props.isLoading}
isPending={props.isPending}
/>
</div>
);
@@ -31,7 +31,7 @@ function Component() {
<ConnectAnalyticsDashboardUI
walletUsage={createWalletStatsStub(30)}
aggregateWalletUsage={createWalletStatsStub(30)}
isLoading={false}
isPending={false}
/>
</div>
);
@@ -34,35 +34,35 @@ function Component() {
<BadgeContainer label="30 days">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(30)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="60 days">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(60)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="10 days">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(10)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="0 days">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(0)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(0)}
isLoading={true}
isPending={true}
/>
</BadgeContainer>
</div>
@@ -46,7 +46,7 @@ const chartLabelToShow: Record<ChartToShow, string> = {
export function DailyConnectionsChartCard(props: {
walletStats: WalletStats[];
isLoading: boolean;
isPending: boolean;
}) {
const { walletStats } = props;
@@ -73,7 +73,7 @@ export function DailyConnectionsChartCard(props: {
return Array.from(chartDataMap.values());
}, [walletStats]);
const disableActions = props.isLoading || barChartData.length === 0;
const disableActions = props.isPending || barChartData.length === 0;
return (
<div className="bg-muted/50 border border-border rounded-lg p-4 md:p-6 relative w-full">
@@ -126,7 +126,7 @@ export function DailyConnectionsChartCard(props: {
config={chartConfig}
className="w-full h-[250px] md:h-[350px]"
>
{props.isLoading ? (
{props.isPending ? (
<LoadingChartState />
) : barChartData.length === 0 ? (
<EmptyChartState />
@@ -34,7 +34,7 @@ const chartLabelToShow: Record<ChartToShow, string> = {
};
export function WalletConnectorsChartCard(props: {
walletStats: WalletStats[];
isLoading: boolean;
isPending: boolean;
}) {
const { walletStats } = props;
const [chartToShow, setChartToShow] = useState<ChartToShow>(
@@ -90,7 +90,7 @@ export function WalletConnectorsChartCard(props: {
}, [walletStats, chartToShow]);
const uniqueWalletTypes = Object.keys(chartConfig);
const disableActions = props.isLoading || chartData.length === 0;
const disableActions = props.isPending || chartData.length === 0;
return (
<div className="bg-muted/50 border border-border rounded-lg p-4 md:p-6 relative w-full">
@@ -142,7 +142,7 @@ export function WalletConnectorsChartCard(props: {
{/* Chart */}
<ChartContainer config={chartConfig} className="w-full h-[400px]">
{props.isLoading ? (
{props.isPending ? (
<LoadingChartState />
) : chartData.length === 0 ? (
<EmptyChartState />
@@ -34,35 +34,35 @@ function Component() {
<BadgeContainer label="30 days">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(30)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="60 days">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(60)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="10 days">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(10)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="0 days">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(0)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(0)}
isLoading={true}
isPending={true}
/>
</BadgeContainer>
</div>
@@ -34,35 +34,35 @@ function Component() {
<BadgeContainer label="30 days">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(30)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="60 days">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(60)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="10 days">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(10)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="0 days">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(0)}
isLoading={false}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(0)}
isLoading={true}
isPending={true}
/>
</BadgeContainer>
</div>
@@ -37,7 +37,7 @@ const chartLabelToShow: Record<ChartToShow, string> = {
export function WalletDistributionChartCard(props: {
walletStats: WalletStats[];
isLoading: boolean;
isPending: boolean;
}) {
const { walletStats } = props;
const [chartToShow, setChartToShow] = useState<ChartToShow>("total");
@@ -114,7 +114,7 @@ export function WalletDistributionChartCard(props: {
};
}, [walletStats]);
const disableActions = props.isLoading || chartData.length === 0;
const disableActions = props.isPending || chartData.length === 0;
return (
<div className="bg-muted/50 border border-border rounded-lg p-4 md:p-6 relative w-full">
<h3 className="text-xl md:text-2xl font-semibold tracking-tight mb-1">
@@ -131,7 +131,7 @@ export function WalletDistributionChartCard(props: {
setChartToShow(v as ChartToShow);
}}
value={chartToShow}
disabled={props.isLoading || chartData.length === 0}
disabled={props.isPending || chartData.length === 0}
>
<SelectTrigger>
<SelectValue />
@@ -184,7 +184,7 @@ export function WalletDistributionChartCard(props: {
config={chartConfig}
className="w-full h-[500px] [&_.recharts-pie-label-text]:fill-foreground mt-6"
>
{props.isLoading ? (
{props.isPending ? (
<LoadingChartState />
) : chartData.length === 0 ? (
<EmptyChartState />
@@ -11,7 +11,7 @@ export default function Page() {
const hasContracts =
deployedContracts.data && deployedContracts.data?.length > 0;
if (deployedContracts.isLoading) {
if (deployedContracts.isPending) {
return (
<div className="min-h-[400px] flex justify-center items-center">
<Spinner className="size-10" />
@@ -0,0 +1,17 @@
"use client";
import { WithEngineInstance } from "components/engine/EnginePageLayout";
import { EngineAlertsPage } from "../../../../../../../../components/engine/alerts/EngineAlertsPage";
import type { EngineInstancePageProps } from "../types";
export default function Page(props: EngineInstancePageProps) {
const { params } = props;
return (
<WithEngineInstance
engineId={props.params.engineId}
content={(res) => <EngineAlertsPage instance={res.instance} />}
rootPath={`/team/${params.team_slug}/${params.project_slug}/engine`}
/>
);
}
@@ -283,7 +283,7 @@ function ProjectNameSetting(props: {
saveButton={{
onClick: handleSubmit,
disabled: !isNameDirty,
isLoading: updateMutation.isPending && isNameDirty,
isPending: updateMutation.isPending && isNameDirty,
}}
bottomText="Please use 64 characters at maximum"
>
@@ -349,7 +349,7 @@ function AllowedDomainsSetting(props: {
saveButton={{
onClick: handleSubmit,
disabled: !isDomainsDirty,
isLoading: updateMutation.isPending && isDomainsDirty,
isPending: updateMutation.isPending && isDomainsDirty,
}}
bottomText={"This is only applicable for web applications"}
>
@@ -417,7 +417,7 @@ function AllowedBundleIDsSetting(props: {
saveButton={{
onClick: handleSubmit,
disabled: !isBundleIdsDirty,
isLoading: updateMutation.isPending && isBundleIdsDirty,
isPending: updateMutation.isPending && isBundleIdsDirty,
}}
noPermissionText={undefined}
header={{
@@ -523,7 +523,7 @@ function EnabledServicesSetting(props: {
saveButton={{
onClick: handleSubmit,
disabled: !form.formState.isDirty,
isLoading: updateMutation.isPending,
isPending: updateMutation.isPending,
}}
bottomText=""
>
@@ -739,7 +739,7 @@ function DeleteProject(props: {
description: description,
}}
description={description}
isLoading={deleteMutation.isPending}
isPending={deleteMutation.isPending}
title={wording === "project" ? "Delete Project" : "Delete API Key"}
/>
);
@@ -6,7 +6,7 @@ export default function RootTeamLayout(props: {
params: { team_slug: string };
}) {
return (
<div className="min-h-full flex flex-col bg-background">
<div className="min-h-screen flex flex-col">
<div className="grow flex flex-col">{props.children}</div>
<TWAutoConnect />
<AppFooter />
@@ -4,14 +4,13 @@ import {
EVMContractInfoProvider,
} from "@3rdweb-sdk/react";
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
import { Flex, SimpleGrid } from "@chakra-ui/react";
import { SimpleGrid } from "@chakra-ui/react";
import {
type DehydratedState,
HydrationBoundary,
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import { ConfigureNetworkModal } from "components/configure-networks/ConfigureNetworkModal";
import { AppShell, type AppShellProps } from "components/layout/app-shell";
import { Onboarding as OnboardingModal } from "components/onboarding";
import { OpCreditsGrantedModalWrapper } from "components/onboarding/OpCreditsGrantedModalWrapper";
@@ -20,11 +19,6 @@ import { AllChainsProvider } from "contexts/all-chains";
import { ChainsProvider } from "contexts/configured-chains";
import { ErrorProvider } from "contexts/error-handler";
import { isSanctionedAddress } from "data/eth-sanctioned-addresses";
import { useAddRecentlyUsedChainId } from "hooks/chains/recentlyUsedChains";
import {
useIsNetworkConfigModalOpen,
useSetIsNetworkConfigModalOpen,
} from "hooks/networkConfigModal";
import { useEffect, useMemo, useState } from "react";
import { useActiveAccount } from "thirdweb/react";
import { Heading } from "tw-components";
@@ -62,7 +56,6 @@ export const AppLayout: ComponentWithChildren<AppLayoutProps> = (props) => {
<DashboardThirdwebProvider>
<SanctionedAddressesChecker>
<PosthogIdentifier />
<ConfigModal />
<OnboardingModal />
<OpCreditsGrantedModalWrapper />
@@ -96,31 +89,12 @@ const SanctionedAddressesChecker: ComponentWithChildren = ({ children }) => {
bg="black"
zIndex="banner"
>
<Flex gap={4} direction="column" align="center">
<div className="gap-4 flex flex-col items-center">
<Heading as="p">Address is blocked</Heading>
<CustomConnectWallet />
</Flex>
</div>
</SimpleGrid>
);
}
return <>{children}</>;
};
function ConfigModal() {
const isNetworkConfigModalOpen = useIsNetworkConfigModalOpen();
const setIsNetworkConfigModalOpen = useSetIsNetworkConfigModalOpen();
const addRecentlyUsedChains = useAddRecentlyUsedChainId();
if (!isNetworkConfigModalOpen) {
return null;
}
return (
<ConfigureNetworkModal
onNetworkAdded={(_chain) => {
addRecentlyUsedChains(_chain.chainId);
}}
onClose={() => setIsNetworkConfigModalOpen(false)}
/>
);
}
@@ -121,7 +121,7 @@ export const MismatchButton = forwardRef<HTMLButtonElement, ButtonProps>(
>
<PopoverTrigger>
<Button
isLoading={evmBalance.isLoading}
isLoading={evmBalance.isPending}
{...props}
type={networksMismatch || notEnoughBalance ? "button" : type}
loadingText={loadingText}
@@ -49,7 +49,7 @@ function useWalletRequiresExternalConfirmation() {
export const TransactionButton: React.FC<TransactionButtonProps> = ({
children,
transactionCount,
isLoading,
isLoading: isPending,
size,
colorScheme,
variant,
@@ -88,7 +88,7 @@ export const TransactionButton: React.FC<TransactionButtonProps> = ({
returnFocusOnClose={false}
initialFocusRef={initialFocusRef}
isLazy
isOpen={walletRequiresExternalConfirmation && isLoading}
isOpen={walletRequiresExternalConfirmation && isPending}
>
<PopoverTrigger>
<ButtonComponent
@@ -96,14 +96,14 @@ export const TransactionButton: React.FC<TransactionButtonProps> = ({
position="relative"
role="group"
colorScheme={colorScheme}
isLoading={isLoading}
isLoading={isPending}
size={size}
variant={variant}
{...restButtonProps}
overflow="hidden"
boxSizing="border-box"
pl={
isLoading || !isConnected
isPending || !isConnected
? undefined
: `calc(${52 + numberWidth}px + var(--chakra-space-${
size === "sm" ? 3 : size === "lg" ? 6 : size === "xs" ? 2 : 4
@@ -1,4 +1,3 @@
import { Flex } from "@chakra-ui/react";
import { ChakraNextImage } from "components/Image";
const partnersCompanies = [
@@ -46,13 +45,7 @@ const partnersCompanies = [
const PartnersSection = () => {
return (
<Flex
alignItems="center"
justifyContent="center"
flexWrap="wrap"
gap={10}
mt="24px"
>
<div className="items-center justify-center flex-wrap gap-10 mt-6 flex flex-row">
{partnersCompanies.slice(0, 10).map((partner, idx) => (
<ChakraNextImage
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
@@ -62,7 +55,7 @@ const PartnersSection = () => {
alt="partner"
/>
))}
</Flex>
</div>
);
};
@@ -1,42 +1,43 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import type { StoredChain } from "contexts/configured-chains";
import { useSetEditChain } from "hooks/networkConfigModal";
import { useAddRecentlyUsedChainId } from "../../hooks/chains/recentlyUsedChains";
import { ConfigureNetworks } from "./ConfigureNetworks";
interface AddNetworkModalProps {
onClose: () => void;
export type ConfigureNetworkModalProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
onNetworkAdded?: (chain: StoredChain) => void;
}
editChain: StoredChain | undefined;
};
export const ConfigureNetworkModal: React.FC<AddNetworkModalProps> = (
export const ConfigureNetworkModal: React.FC<ConfigureNetworkModalProps> = (
props,
) => {
const setEditChain = useSetEditChain();
const addRecentlyUsedChains = useAddRecentlyUsedChainId();
const onModalClose = () => {
props.onClose();
setEditChain(undefined);
props.onOpenChange(false);
};
return (
<Dialog
open={true}
onOpenChange={(v) => {
if (!v) {
onModalClose();
}
}}
>
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
<DialogContent
className="p-0 z-[10001] max-w-[480px]"
dialogOverlayClassName="z-[10000]"
>
<ConfigureNetworks
onNetworkAdded={(chain) => {
props.onNetworkAdded?.(chain);
onModalClose();
}}
onNetworkConfigured={onModalClose}
/>
{/* TODO - remove after moving ConfigureNetworks to shadcn */}
<ChakraProviderSetup>
<ConfigureNetworks
onNetworkAdded={(chain) => {
addRecentlyUsedChains(chain.chainId);
props.onNetworkAdded?.(chain);
onModalClose();
}}
onNetworkConfigured={onModalClose}
editChain={props.editChain}
/>
</ChakraProviderSetup>
</DialogContent>
</Dialog>
);
@@ -2,7 +2,6 @@ import type { StoredChain } from "contexts/configured-chains";
import { useTrack } from "hooks/analytics/useTrack";
import { useAddRecentlyUsedChainId } from "hooks/chains/recentlyUsedChains";
import { useModifyChain } from "hooks/chains/useModifyChain";
import { useEditChain } from "hooks/networkConfigModal";
import { toast } from "sonner";
import { ConfigureNetworkForm } from "./ConfigureNetworkForm";
@@ -22,13 +21,14 @@ interface ConfigureNetworksProps {
onNetworkAdded?: (chain: StoredChain) => void;
prefillSlug?: string;
prefillChainId?: string;
editChain: StoredChain | undefined;
}
export const ConfigureNetworks: React.FC<ConfigureNetworksProps> = (props) => {
const trackChainConfig = useChainConfigTrack();
const addRecentlyUsedChainId = useAddRecentlyUsedChainId();
const modifyChain = useModifyChain();
const editChain = useEditChain();
const { editChain } = props;
const handleSubmit = (chain: StoredChain) => {
modifyChain(chain);
@@ -0,0 +1,28 @@
"use client";
import { Suspense, useEffect, useState } from "react";
import {
ConfigureNetworkModal,
type ConfigureNetworkModalProps,
} from "./ConfigureNetworkModal";
export function LazyConfigureNetworkModal(props: ConfigureNetworkModalProps) {
const [hasEverOpened, setHasEverOpened] = useState(false);
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (props.open) {
setHasEverOpened(true);
}
}, [props.open]);
if (hasEverOpened) {
return (
<Suspense fallback={null}>
<ConfigureNetworkModal {...props} />
</Suspense>
);
}
return null;
}
@@ -1,6 +1,7 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Alert, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
import { ToolTipLabel } from "@/components/ui/tooltip";
@@ -22,7 +23,7 @@ import { SolidityInput } from "contract-ui/components/solidity-inputs";
import { useTrack } from "hooks/analytics/useTrack";
import { useTxNotifications } from "hooks/useTxNotifications";
import { replaceTemplateValues } from "lib/deployment/template-values";
import { ExternalLinkIcon } from "lucide-react";
import { CircleAlertIcon, ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { useCallback, useMemo } from "react";
import { FormProvider, type UseFormReturn, useForm } from "react-hook-form";
@@ -394,6 +395,16 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
});
}
// handle split things
const payees: string[] = [];
const shares: string[] = [];
if (isSplit && params.recipients?.length) {
for (const recipient of params.recipients) {
payees.push(recipient.address);
shares.push(recipient.sharesBps.toString());
}
}
if (metadata.name === "MarketplaceV3") {
// special case for marketplace
return await deployMarketplaceContract({
@@ -416,6 +427,8 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
const initializeParams = {
...params.contractMetadata,
...params.deployParams,
payees,
shares,
_contractURI,
};
@@ -468,6 +481,9 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
enabled: walletChain !== undefined && metadata !== undefined,
});
const shouldShowDeterministicDeployWarning =
constructorParams.length > 0 && form.watch("deployDeterministic");
return (
<>
<FormProvider {...form}>
@@ -825,23 +841,37 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
{metadata?.deployType === "standard" && (
<>
{/* Deterministic deploy */}
<CheckboxWithLabel>
<Checkbox
{...form.register("deployDeterministic")}
checked={form.watch("deployDeterministic")}
onCheckedChange={(c) =>
form.setValue("deployDeterministic", !!c)
}
/>
<ToolTipLabel label="Allows having the same contract address on multiple chains. You can control the address by specifying a salt for create2 deployment below">
<div className="inline-flex gap-1.5 items-center">
<span className="tex-sm">
Deploy at a deterministic address
</span>
<FiHelpCircle className="size-4" />
</div>
</ToolTipLabel>
</CheckboxWithLabel>
<div className="flex flex-col gap-3">
<CheckboxWithLabel>
<Checkbox
{...form.register("deployDeterministic")}
checked={form.watch("deployDeterministic")}
onCheckedChange={(c) =>
form.setValue("deployDeterministic", !!c)
}
/>
<ToolTipLabel label="Allows having the same contract address on multiple chains. You can control the address by specifying a salt for create2 deployment below">
<div className="inline-flex gap-1.5 items-center">
<span className="tex-sm">
Deploy at a deterministic address
</span>
<FiHelpCircle className="size-4" />
</div>
</ToolTipLabel>
</CheckboxWithLabel>
{shouldShowDeterministicDeployWarning && (
<Alert variant="warning">
<CircleAlertIcon className="size-5" />
<AlertTitle>
Deterministic deployment would only result in the same
contract address if you use the same contructor params
on every deployment.
</AlertTitle>
</Alert>
)}
</div>
{/* Optional Salt Input */}
{isCreate2Deployment && (
@@ -1,4 +1,3 @@
import { Flex } from "@chakra-ui/react";
import { Select } from "chakra-react-select";
import type { SizeProp } from "chakra-react-select";
import { useSupportedChains } from "hooks/chains/configureChains";
@@ -54,7 +53,7 @@ export const NetworkDropdown: React.FC<NetworkDropdownProps> = ({
}, [form, options]);
return (
<Flex gap={2} alignItems="center" w="full">
<div className="flex flex-row gap-2 items-center w-full">
<Select
size={size}
placeholder={`${
@@ -106,6 +105,6 @@ export const NetworkDropdown: React.FC<NetworkDropdownProps> = ({
: options.find(({ value: val }) => val === value)
}
/>
</Flex>
</div>
);
};
@@ -1,4 +1,3 @@
import { Flex } from "@chakra-ui/react";
import type { Abi } from "abitype";
import { Select } from "chakra-react-select";
import { useMemo } from "react";
@@ -27,7 +26,7 @@ export const AbiSelector: React.FC<AbiSelectorProps> = ({
}, [abi]);
return (
<Flex gap={2} alignItems="center" w="full">
<div className="flex flex-row gap-2 items-center w-full">
<Select
placeholder="Select function"
options={options}
@@ -45,6 +44,6 @@ export const AbiSelector: React.FC<AbiSelectorProps> = ({
}
}}
/>
</Flex>
</div>
);
};
@@ -173,7 +173,7 @@ export function ContractPublishForm(props: {
: initializerParams;
// during loading and after success we should stay in loading state
const isLoading = sendTx.isPending || sendTx.isSuccess;
const isPending = sendTx.isPending || sendTx.isSuccess;
useIsomorphicLayoutEffect(() => {
window?.scrollTo({
@@ -400,7 +400,7 @@ export function ContractPublishForm(props: {
position="relative"
role="group"
colorScheme="blue"
isLoading={isLoading}
isLoading={isPending}
form="contract-release-form"
isDisabled={isDisabled}
loadingText={
@@ -96,7 +96,7 @@ function ImportForm() {
const address = useActiveAccount()?.address;
const registry = useAllContractList(address);
const isLoading =
const showLoading =
form.formState.isSubmitting ||
addToDashboard.isPending ||
addToDashboard.isSuccess ||
@@ -205,13 +205,13 @@ function ImportForm() {
<div className="flex justify-end">
<Button type="submit" className="gap-2">
{isLoading ? (
{showLoading ? (
<Spinner className="size-4" />
) : (
<PlusIcon className="size-4" />
)}
{isLoading
{showLoading
? addToDashboard.isSuccess || isRedirecting
? "Redirecting"
: "Importing contract"
@@ -14,7 +14,7 @@ interface PublisherAvatarProps extends Omit<MaskedAvatarProps, "src"> {
export const PublisherAvatar: React.FC<PublisherAvatarProps> = ({
address,
isLoading,
isPending,
...restProps
}) => {
const client = useThirdwebClient();
@@ -24,7 +24,7 @@ export const PublisherAvatar: React.FC<PublisherAvatarProps> = ({
);
return (
<MaskedAvatar
isLoading={isLoading || ensQuery.isLoading || publisherProfile.isLoading}
isPending={isPending || ensQuery.isPending || publisherProfile.isPending}
src={
publisherProfile.data?.avatar
? resolveScheme({
@@ -1,4 +1,3 @@
import { Flex } from "@chakra-ui/react";
import type { Abi } from "abitype";
import { Link, Text } from "tw-components";
import type { SourceFile } from "../types";
@@ -11,8 +10,8 @@ interface SourcesPanelProps {
export const SourcesPanel: React.FC<SourcesPanelProps> = ({ sources, abi }) => {
return (
<Flex flexDir="column" gap={8}>
<Flex flexDir="column" gap={4}>
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
{sources && sources?.length > 0 ? (
<SourcesAccordion sources={sources} abi={abi} />
) : (
@@ -27,7 +26,7 @@ export const SourcesPanel: React.FC<SourcesPanelProps> = ({ sources, abi }) => {
</Link>
</Text>
)}
</Flex>
</Flex>
</div>
</div>
);
};
@@ -115,7 +115,7 @@ export const DeployedContracts: React.FC<DeployedContractsProps> = ({
combinedList={contractListQuery.data}
limit={limit}
chainIdsWithDeployments={chainIdsWithDeployments}
loading={contractListQuery.isLoading}
loading={contractListQuery.isPending}
/>
{contractListQuery.data.length === 0 && contractListQuery.isFetched && (
@@ -89,7 +89,7 @@ export const PublishedContracts: React.FC<PublishedContractsProps> = ({
contractDetails={slicedData}
hidePublisher
>
{publishedContractsQuery.isLoading && (
{publishedContractsQuery.isPending && (
<Center>
<Flex py={4} direction="row" gap={4} align="center">
<Spinner size="sm" />
@@ -1,5 +1,4 @@
import { Skeleton } from "@/components/ui/skeleton";
import { Flex } from "@chakra-ui/react";
import { formatDistance } from "date-fns/formatDistance";
import { ArrowRightIcon } from "lucide-react";
import { Link } from "tw-components";
@@ -17,17 +16,12 @@ interface ChangelogProps {
export const Changelog: React.FC<ChangelogProps> = ({ changelog }) => {
return (
<Flex
flexDir="column"
gap={6}
position="relative"
className="border-l border-border py-2"
>
<div className="border-l border-border py-2 flex flex-col gap-6 relative">
{changelog.map((item) => (
<Flex key={item.title} gap={4}>
<div className="flex flex-row gap-4" key={item.title}>
<div className="size-2.5 bg-border rounded-full shrink-0 -translate-x-1/2 translate-y-1/2" />
<Flex flexDir="column">
<div className="flex flex-col">
<Link
isExternal
href={`${item.url}?utm_source=thirdweb&utm_campaign=changelog`}
@@ -43,8 +37,8 @@ export const Changelog: React.FC<ChangelogProps> = ({ changelog }) => {
})}
</ClientOnly>
</div>
</Flex>
</Flex>
</div>
</div>
))}
<Link
href="https://blog.thirdweb.com/changelog?utm_source=thirdweb&utm_campaign=changelog"
@@ -53,6 +47,6 @@ export const Changelog: React.FC<ChangelogProps> = ({ changelog }) => {
>
View More <ArrowRightIcon className="size-4" />
</Link>
</Flex>
</div>
);
};
@@ -155,7 +155,7 @@ export const InAppWalletUsersPageContent = (props: {
title="active in-app wallets"
data={itemsToShow}
columns={columns}
isLoading={walletsQuery.isLoading}
isPending={walletsQuery.isPending}
isFetched={walletsQuery.isFetched}
/>
@@ -46,14 +46,18 @@ const sidebarLinkMeta: Array<{ pathId: string; label: string }> = [
pathId: "webhooks",
label: "Webhooks",
},
{
pathId: "configuration",
label: "Configuration",
},
{
pathId: "metrics",
label: "Metrics",
},
{
pathId: "alerts",
label: "Alerts",
},
{
pathId: "configuration",
label: "Configuration",
},
];
const NEXT_PUBLIC_DEMO_ENGINE_URL = process.env.NEXT_PUBLIC_DEMO_ENGINE_URL;
@@ -121,7 +125,7 @@ function QueryAndRenderInstanceHeader(props: {
const instancesQuery = useEngineInstances();
const instance = instancesQuery.data?.find((x) => x.id === props.engineId);
if (instancesQuery.isLoading) {
if (instancesQuery.isPending) {
return <PageLoading />;
}
@@ -151,7 +155,7 @@ function EnsurePermissionAndRenderInstance(props: {
instanceUrl: props.instance.url,
});
if (permissionQuery.isLoading) {
if (permissionQuery.isPending) {
return <PageLoading />;
}
@@ -0,0 +1,234 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import {
type EngineAlertRule,
EngineNotificationChannelTypeConfig,
} from "../../../@3rdweb-sdk/react/hooks/useEngine";
const alertFormSchema = z.object({
subscriptionRoutes: z.array(z.string()),
type: z.enum(
Object.keys(EngineNotificationChannelTypeConfig) as [
keyof typeof EngineNotificationChannelTypeConfig,
],
),
value: z.string(),
});
type EngineAlertFormValues = z.infer<typeof alertFormSchema>;
export function EngineAlertDialogForm(props: {
alertRules: EngineAlertRule[];
title: string;
values: EngineAlertFormValues | null;
open: boolean;
onOpenChange: (open: boolean) => void;
submitButton: {
isLoading: boolean;
label: string;
onSubmit: (value: EngineAlertFormValues) => void;
};
}) {
const form = useForm<z.infer<typeof alertFormSchema>>({
resolver: zodResolver(alertFormSchema),
defaultValues: {
subscriptionRoutes: ["alert.*"],
type: "slack",
},
values: props.values ?? undefined,
});
function onSubmit(values: z.infer<typeof alertFormSchema>) {
props.submitButton.onSubmit(values);
}
const alertRuleOptions: EngineAlertRule[] = [
{
id: "_all_alerts",
routingKey: "alert.*",
title: "All alerts",
description: "Get notified of all alerts.",
createdAt: new Date(),
pausedAt: null,
},
...props.alertRules,
];
const selectedAlertRule = alertRuleOptions.find(
(alertRule) =>
alertRule.routingKey === form.getValues("subscriptionRoutes")[0],
);
return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
<DialogContent
className="p-0 z-[10001]"
dialogOverlayClassName="z-[10000]"
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="p-6">
<DialogHeader className="mb-4">
<DialogTitle className="text-2xl font-semibold tracking-tight">
{props.title}
</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-5">
{/* Alert Rule */}
<FormField
control={form.control}
name="subscriptionRoutes"
render={({ field }) => (
<FormItem>
<FormLabel>Alert Rule</FormLabel>
<FormControl>
<Select
onValueChange={(value) => field.onChange([value])}
value={field.value[0] ?? "alert.*"}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent className="z-[10001]">
<SelectGroup>
{alertRuleOptions.map((option) => (
<SelectItem
key={option.id}
value={option.routingKey}
>
{option.title}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
{selectedAlertRule?.description && (
<FormDescription>
{selectedAlertRule.description}
</FormDescription>
)}
</FormItem>
)}
/>
{/* Notification Type */}
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Notification Type</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
value={field.value}
>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select Notification Type" />
</SelectTrigger>
<SelectContent className="z-[10001]">
<SelectGroup>
{Object.entries(
EngineNotificationChannelTypeConfig,
).map(([type, config]) => (
<SelectItem key={type} value={type}>
{config.display}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Value (webhook URL, Slack URL, email address, etc.) */}
<FormField
control={form.control}
name="value"
render={({ field }) => (
<FormItem>
<FormLabel>
{EngineNotificationChannelTypeConfig[
form.getValues("type")
].valueDisplay ?? "Value"}
</FormLabel>
<FormControl>
<Input
type={
form.getValues("type") === "email"
? "email"
: "text"
}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<DialogFooter className="p-6 bg-muted/50 border-t border-border mt-4 gap-4 lg:gap-2 ">
<Button
variant="outline"
onClick={() => {
props.onOpenChange(false);
}}
>
Cancel
</Button>
<Button
type="submit"
className="min-w-28 gap-2"
disabled={props.submitButton.isLoading}
>
{props.submitButton.isLoading && <Spinner className="size-4" />}
{props.submitButton.label}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,29 @@
import {
type EngineInstance,
useEngineAlertRules,
} from "@3rdweb-sdk/react/hooks/useEngine";
import { ManageEngineAlertsSection } from "./ManageEngineAlerts";
import { RecentEngineAlertsSection } from "./RecentEngineAlerts";
export function EngineAlertsPage(props: {
instance: EngineInstance;
}) {
const alertRulesQuery = useEngineAlertRules(props.instance.id);
const alertRules = alertRulesQuery.data ?? [];
return (
<div>
<ManageEngineAlertsSection
alertRules={alertRules}
engineId={props.instance.id}
alertRulesIsLoading={alertRulesQuery.isLoading}
/>
<div className="h-8" />
<RecentEngineAlertsSection
alertRules={alertRules}
engineId={props.instance.id}
alertRulesIsLoading={alertRulesQuery.isLoading}
/>
</div>
);
}
@@ -0,0 +1,52 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { DialogDescription } from "@radix-ui/react-dialog";
export function EngineDeleteAlertModal(props: {
onConfirm: () => void;
isPending: boolean;
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
<DialogContent className="p-0">
<DialogHeader className="mb-4 p-6">
<DialogTitle className="text-2xl font-semibold tracking-tight">
Delete Alert
</DialogTitle>
<DialogDescription>
You will no longer receive notifications from this webhook.
</DialogDescription>
</DialogHeader>
<DialogFooter className="p-6 bg-muted/50 border-t border-border gap-4 lg:gap-2">
<Button
variant="outline"
onClick={() => {
props.onOpenChange(false);
}}
>
Cancel
</Button>
<Button
className="min-w-28 gap-2"
variant="destructive"
onClick={props.onConfirm}
>
{props.isPending && <Spinner className="size-4" />}
Delete Webhook
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,131 @@
import type {
CreateNotificationChannelInput,
EngineNotificationChannel,
} from "@3rdweb-sdk/react/hooks/useEngine";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { Toaster } from "sonner";
import {
createEngineAlertRuleStub,
createEngineNotificationChannelStub,
} from "../../../stories/stubs";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { ManageEngineAlertsSectionUI } from "./ManageEngineAlerts";
const meta = {
title: "engine/alerts/manage",
component: Story,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
const createEngineAlertMutation = useMutation({
mutationFn: async (values: CreateNotificationChannelInput) => {
console.log("creating alert using input:", values);
await new Promise((resolve) => setTimeout(resolve, 1000));
const channelStub: EngineNotificationChannel = {
createdAt: new Date(),
id: "new-channel",
pausedAt: null,
subscriptionRoutes: values.subscriptionRoutes,
value: values.value,
type: values.type,
};
console.log("created channel:", channelStub);
return channelStub;
},
});
async function deleteAlert(alertId: string) {
console.log("deleting alert:", alertId);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
return (
<div className="py-6 container max-w-[1154px] flex flex-col gap-14">
<BadgeContainer label="2 Alerts">
<ManageEngineAlertsSectionUI
isLoading={false}
onAlertsUpdated={() => {}}
engineId="test-engine-id"
createAlertMutation={createEngineAlertMutation}
deleteAlert={deleteAlert}
alertRules={[
createEngineAlertRuleStub("foo"),
createEngineAlertRuleStub("bar"),
]}
notificationChannels={[
createEngineNotificationChannelStub("foo"),
createEngineNotificationChannelStub("foo", {
pausedAt: null,
}),
]}
/>
</BadgeContainer>
<BadgeContainer label="No Alerts">
<ManageEngineAlertsSectionUI
alertRules={[
createEngineAlertRuleStub("foo"),
createEngineAlertRuleStub("bar"),
]}
engineId="test-engine-id"
notificationChannels={[]}
isLoading={false}
onAlertsUpdated={() => {}}
createAlertMutation={createEngineAlertMutation}
deleteAlert={deleteAlert}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<ManageEngineAlertsSectionUI
alertRules={[
createEngineAlertRuleStub("foo"),
createEngineAlertRuleStub("bar"),
]}
engineId="test-engine-id"
notificationChannels={[]}
isLoading={true}
onAlertsUpdated={() => {}}
createAlertMutation={createEngineAlertMutation}
deleteAlert={deleteAlert}
/>
</BadgeContainer>
<BadgeContainer label="No Alert Rules">
<ManageEngineAlertsSectionUI
alertRules={[]}
engineId="test-engine-id"
notificationChannels={[]}
isLoading={false}
onAlertsUpdated={() => {}}
createAlertMutation={createEngineAlertMutation}
deleteAlert={deleteAlert}
/>
</BadgeContainer>
<Toaster richColors />
</div>
);
}
@@ -0,0 +1,383 @@
"use client";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ToolTipLabel } from "@/components/ui/tooltip";
import {
type CreateNotificationChannelInput,
type EngineAlertRule,
type EngineNotificationChannel,
useEngineCreateNotificationChannel,
useEngineDeleteNotificationChannel,
useEngineNotificationChannels,
} from "@3rdweb-sdk/react/hooks/useEngine";
import { type UseMutationResult, useMutation } from "@tanstack/react-query";
import { formatDate } from "date-fns";
import { EllipsisVerticalIcon, PlusIcon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { EngineAlertDialogForm } from "./EngineAlertDialogForm";
import { EngineDeleteAlertModal } from "./EngineDeleteAlertModal";
type CreateAlertMutation = UseMutationResult<
EngineNotificationChannel,
unknown,
CreateNotificationChannelInput,
unknown
>;
export function ManageEngineAlertsSection(props: {
alertRules: EngineAlertRule[];
alertRulesIsLoading: boolean;
engineId: string;
}) {
const notificationChannelsQuery = useEngineNotificationChannels(
props.engineId,
);
const deleteAlertMutation = useEngineDeleteNotificationChannel(
props.engineId,
);
const createAlertMutation = useEngineCreateNotificationChannel(
props.engineId,
);
// not passing the mutation to avoid multiple rows sharing the same mutation state, we create the new mutation for each row instead in each component instead
async function deleteAlert(notificationChannelId: string) {
return deleteAlertMutation.mutate(notificationChannelId);
}
return (
<ManageEngineAlertsSectionUI
alertRules={props.alertRules}
engineId={props.engineId}
notificationChannels={notificationChannelsQuery.data ?? []}
isLoading={
notificationChannelsQuery.isLoading || props.alertRulesIsLoading
}
onAlertsUpdated={() => {
notificationChannelsQuery.refetch();
}}
createAlertMutation={createAlertMutation}
deleteAlert={deleteAlert}
/>
);
}
export function ManageEngineAlertsSectionUI(props: {
alertRules: EngineAlertRule[];
engineId: string;
notificationChannels: EngineNotificationChannel[];
isLoading: boolean;
onAlertsUpdated: () => void;
createAlertMutation: CreateAlertMutation;
deleteAlert: (notificationChannelId: string) => Promise<void>;
}) {
const {
engineId,
notificationChannels,
isLoading,
onAlertsUpdated,
createAlertMutation,
deleteAlert,
} = props;
return (
<section>
<div className="flex gap-4 lg:gap-6 flex-col lg:flex-row justify-between">
<div>
<h2 className="text-2xl font-semibold tracking-tight mb-1">
Manage Alerts
</h2>
<p className="text-muted-foreground">
Get notified when alerts are triggered on your Engine instance.
</p>
</div>
<CreateAlertButton
engineId={engineId}
alertRules={props.alertRules}
onSuccess={onAlertsUpdated}
createAlertMutation={createAlertMutation}
/>
</div>
<div className="h-5" />
{notificationChannels.length === 0 || isLoading ? (
<div className="flex justify-center items-center min-h-[200px] w-full border border-border rounded-lg">
{isLoading ? <Spinner className="size-8" /> : "No alerts set up yet."}
</div>
) : (
<EngineAlertsTableUI
engineId={engineId}
alertRules={props.alertRules}
notificationChannels={notificationChannels}
onAlertsUpdated={props.onAlertsUpdated}
deleteAlert={deleteAlert}
/>
)}
</section>
);
}
function EngineAlertsTableUI(props: {
engineId: string;
alertRules: EngineAlertRule[];
notificationChannels: EngineNotificationChannel[];
onAlertsUpdated: () => void;
deleteAlert: (notificationChannelId: string) => Promise<void>;
}) {
return (
<TableContainer>
<Table>
<TableHeader>
<TableRow>
<TableHead>Status</TableHead>
<TableHead>Alert Rule</TableHead>
<TableHead>Notification Type</TableHead>
<TableHead>Created At</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
{props.notificationChannels.map((notificationChannel) => {
const isPaused = notificationChannel.pausedAt !== null;
return (
<TableRow key={notificationChannel.id}>
<TableCell>
<Badge
variant={isPaused ? "warning" : "default"}
className="text-sm"
>
{isPaused ? "Paused" : "Active"}
</Badge>
</TableCell>
<TableCell>
{getMatchingAlertRules(
notificationChannel.subscriptionRoutes,
props.alertRules,
).map((alertRule) => (
<p key={alertRule.routingKey}>{alertRule.title}</p>
))}
</TableCell>
<TableCell>
<Badge variant="outline" className="text-sm capitalize">
{notificationChannel.type}
</Badge>
<CopyTextButton
textToCopy={notificationChannel.value}
textToShow={truncateValue(notificationChannel.value)}
copyIconPosition="left"
tooltip="Copy"
variant="ghost"
iconClassName="size-3"
className="-translate-x-2 max-w-[250px] text-muted-foreground"
/>
</TableCell>
<TableCell className="text-muted-foreground">
{formatDate(
new Date(notificationChannel.createdAt),
"MMM dd, yyyy",
)}
</TableCell>
<TableCell className="text-muted-foreground">
<AlertOptionsButton
alertRules={props.alertRules}
engineId={props.engineId}
notificationChannel={notificationChannel}
onDeleteSuccess={props.onAlertsUpdated}
deleteAlert={props.deleteAlert}
/>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
);
}
function CreateAlertButton(props: {
engineId: string;
alertRules: EngineAlertRule[];
onSuccess: () => void;
createAlertMutation: CreateAlertMutation;
}) {
const [isModalOpen, setIsModalOpen] = useState(false);
const { createAlertMutation } = props;
return (
<>
<ToolTipLabel
label={
props.alertRules.length === 0
? "This feature is only available for cloud-hosted Engines."
: undefined
}
>
<Button
onClick={() => {
setIsModalOpen(true);
}}
className="gap-2"
disabled={props.alertRules.length === 0}
>
<PlusIcon className="size-4" />
Add Alert
</Button>
</ToolTipLabel>
<EngineAlertDialogForm
alertRules={props.alertRules}
open={isModalOpen}
onOpenChange={setIsModalOpen}
title="Add Alert"
submitButton={{
label: "Add Alert",
isLoading: createAlertMutation.isPending,
onSubmit: (values) => {
const addPromise = createAlertMutation.mutateAsync(values);
toast.promise(addPromise, {
success: "Alert added successfully",
error: "Failed to add alert",
});
addPromise.then(() => {
setIsModalOpen(false);
props.onSuccess();
});
},
}}
values={null}
/>
</>
);
}
function AlertOptionsButton(props: {
alertRules: EngineAlertRule[];
engineId: string;
notificationChannel: EngineNotificationChannel;
onDeleteSuccess: () => void;
deleteAlert: (notificationChannelId: string) => Promise<void>;
}) {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const deleteMutation = useMutation({
mutationFn: props.deleteAlert,
});
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="icon" variant="ghost">
<EllipsisVerticalIcon className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-36">
<DropdownMenuItem
className="text-destructive-text hover:!bg-destructive"
onClick={() => {
setIsDeleteModalOpen(true);
}}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<EngineDeleteAlertModal
isPending={deleteMutation.isPending}
onConfirm={() => {
const deletePromise = deleteMutation.mutateAsync(
props.notificationChannel.id,
);
toast.promise(deletePromise, {
success: "Alert Deleted Successfully",
error: "Failed To Delete Alert",
});
deletePromise.then(() => {
setIsDeleteModalOpen(false);
props.onDeleteSuccess();
});
}}
open={isDeleteModalOpen}
onOpenChange={setIsDeleteModalOpen}
/>
</>
);
}
const getMatchingAlertRules = (
subscriptionRoutes: string[],
alertRules: EngineAlertRule[],
): EngineAlertRule[] => {
const results: EngineAlertRule[] = [];
for (const alertRule of alertRules) {
for (const subscriptionRoute of subscriptionRoutes) {
if (isRoutingKeyMatch(subscriptionRoute, alertRule.routingKey)) {
results.push(alertRule);
break;
}
}
}
return results;
};
/**
* Check if routingKey matches subscriptionRoute.
*
* Example: exact match
* `alert.stuck-nonce` matches `alert.stuck-nonce`
*
* Example: wildcard
* `alert.*` matches `alert.stuck-nonce`
*/
const isRoutingKeyMatch = (
subscriptionRoute: string,
alertRoutingKey: string,
): boolean => {
if (!subscriptionRoute.includes("*")) {
// Check exact match if no wildcard.
return subscriptionRoute === alertRoutingKey;
}
const [prefix, suffix] = subscriptionRoute.split("*");
if (prefix && !subscriptionRoute.startsWith(prefix)) return false;
if (suffix && !subscriptionRoute.endsWith(suffix)) return false;
return true;
};
const truncateValue = (str: string): string => {
if (str.length <= 16) return str;
return `${str.slice(0, 12)}...${str.slice(-4)}`;
};
@@ -0,0 +1,75 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Toaster } from "sonner";
import {
createEngineAlertRuleStub,
createEngineAlertStub,
} from "../../../stories/stubs";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { RecentEngineAlertsSectionUI } from "./RecentEngineAlerts";
const meta = {
title: "engine/alerts/recent",
component: Story,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="py-6 container max-w-[1154px] flex flex-col gap-14">
<BadgeContainer label="3 Alerts">
<RecentEngineAlertsSectionUI
isLoading={false}
onAlertsUpdated={() => {}}
alertRules={[createEngineAlertRuleStub("foo")]}
alerts={[
createEngineAlertStub("foo"),
createEngineAlertStub("foo", {
status: "firing",
}),
createEngineAlertStub("foo", {
status: "resolved",
}),
]}
/>
</BadgeContainer>
<BadgeContainer label="No Alerts">
<RecentEngineAlertsSectionUI
isLoading={false}
alertRules={[]}
alerts={[]}
onAlertsUpdated={() => {}}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<RecentEngineAlertsSectionUI
isLoading={true}
alertRules={[]}
alerts={[]}
onAlertsUpdated={() => {}}
/>
</BadgeContainer>
<Toaster richColors />
</div>
);
}
@@ -0,0 +1,138 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Badge, type BadgeProps } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
type EngineAlert,
type EngineAlertRule,
useEngineAlerts,
} from "@3rdweb-sdk/react/hooks/useEngine";
import { formatDistance } from "date-fns";
import { useMemo } from "react";
export function RecentEngineAlertsSection(props: {
alertRules: EngineAlertRule[];
alertRulesIsLoading: boolean;
engineId: string;
}) {
// TODO - pagination
// required : return the total number of alerts in response from API
const alertsQuery = useEngineAlerts(props.engineId, 100, 0);
const alerts = alertsQuery.data ?? [];
return (
<RecentEngineAlertsSectionUI
alertRules={props.alertRules}
alerts={alerts}
isLoading={alertsQuery.isLoading || props.alertRulesIsLoading}
onAlertsUpdated={() => {
alertsQuery.refetch();
}}
/>
);
}
export function RecentEngineAlertsSectionUI(props: {
alertRules: EngineAlertRule[];
alerts: EngineAlert[];
isLoading: boolean;
onAlertsUpdated: () => void;
}) {
const { alerts, isLoading } = props;
return (
<section>
<h2 className="text-2xl font-semibold tracking-tight mb-1">
Recent Alerts
</h2>
<div className="h-2" />
{alerts.length === 0 || isLoading ? (
<div className="flex justify-center items-center min-h-[200px] w-full border border-border rounded-lg">
{isLoading ? <Spinner className="size-8" /> : "No alerts triggered."}
</div>
) : (
<RecentEngineAlertsTableUI
alertRules={props.alertRules}
alerts={alerts}
onAlertsUpdated={props.onAlertsUpdated}
/>
)}
</section>
);
}
function RecentEngineAlertsTableUI(props: {
alertRules: EngineAlertRule[];
alerts: EngineAlert[];
onAlertsUpdated: () => void;
}) {
const alertRulesMap = useMemo(() => {
const map: Record<string, EngineAlertRule> = {};
for (const alertRule of props.alertRules) {
map[alertRule.id] = alertRule;
}
return map;
}, [props.alertRules]);
return (
<TableContainer>
<Table>
<TableHeader>
<TableRow>
<TableHead>Alert Trigger</TableHead>
<TableHead>Status</TableHead>
<TableHead>Timestamp</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{props.alerts.map((alert) => {
let variant: BadgeProps["variant"];
let status: string;
if (alert.status === "pending") {
variant = "warning";
status = "Pending";
} else if (alert.status === "firing") {
variant = "destructive";
status = "Triggered";
} else if (alert.status === "resolved") {
variant = "success";
status = "Resolved";
} else {
return null;
}
return (
<TableRow key={alert.id}>
<TableCell>{alertRulesMap[alert.alertRuleId]?.title}</TableCell>
<TableCell>
<Badge variant={variant} className="text-sm">
{status}
</Badge>
</TableCell>
<TableCell className="text-muted-foreground">
{formatDistance(
new Date(alert.endsAt ?? alert.startsAt),
new Date(),
{ addSuffix: true },
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
);
}
@@ -1,7 +1,6 @@
"use client";
import type { EngineInstance } from "@3rdweb-sdk/react/hooks/useEngine";
import { Flex } from "@chakra-ui/react";
import { EngineCorsConfig } from "./cors";
import { EngineWalletConfig } from "./engine-wallet-config";
import { EngineIpAllowlistConfig } from "./ip-allowlist";
@@ -15,11 +14,11 @@ export const EngineConfiguration: React.FC<EngineConfigurationProps> = ({
instance,
}) => {
return (
<Flex flexDir="column" gap={12}>
<div className="flex flex-col gap-12">
<EngineWalletConfig instanceUrl={instance.url} />
<EngineCorsConfig instanceUrl={instance.url} />
<EngineIpAllowlistConfig instanceUrl={instance.url} />
<EngineSystem instance={instance} />
</Flex>
</div>
);
};
@@ -1,7 +1,6 @@
"use client";
import { useEngineWebhooks } from "@3rdweb-sdk/react/hooks/useEngine";
import { Flex } from "@chakra-ui/react";
import { Heading, Link, Text } from "tw-components";
import { AddWebhookButton } from "./add-webhook-button";
import { WebhooksTable } from "./webhooks-table";
@@ -16,8 +15,8 @@ export const EngineWebhooks: React.FC<EngineWebhooksProps> = ({
const webhooks = useEngineWebhooks(instanceUrl);
return (
<Flex flexDir="column" gap={4}>
<Flex flexDir="column" gap={2}>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Heading size="title.md">Webhooks</Heading>
<Text>
Notify your app backend when transaction and backend wallet events
@@ -31,14 +30,14 @@ export const EngineWebhooks: React.FC<EngineWebhooksProps> = ({
</Link>
.
</Text>
</Flex>
</div>
<WebhooksTable
instanceUrl={instanceUrl}
webhooks={webhooks.data || []}
isLoading={webhooks.isLoading}
isPending={webhooks.isPending}
isFetched={webhooks.isFetched}
/>
<AddWebhookButton instanceUrl={instanceUrl} />
</Flex>
</div>
);
};
@@ -36,7 +36,7 @@ export function beautifyString(str: string): string {
interface WebhooksTableProps {
instanceUrl: string;
webhooks: EngineWebhook[];
isLoading: boolean;
isPending: boolean;
isFetched: boolean;
}
@@ -109,7 +109,7 @@ const columns = [
export const WebhooksTable: React.FC<WebhooksTableProps> = ({
instanceUrl,
webhooks,
isLoading,
isPending,
isFetched,
}) => {
const [webhookToRevoke, setWebhookToRevoke] = useState<EngineWebhook>();
@@ -209,7 +209,7 @@ export const WebhooksTable: React.FC<WebhooksTableProps> = ({
title="webhooks"
data={activeWebhooks}
columns={columns}
isLoading={isLoading}
isPending={isPending}
isFetched={isFetched}
onMenuClick={[
{
@@ -530,7 +530,7 @@ const FilterSelector = ({
return (
<Card>
{abiQuery.isLoading ? (
{abiQuery.isPending ? (
<Spinner size="sm" />
) : filterNames.length === 0 ? (
<Text>
@@ -52,7 +52,7 @@ function shortenAddress(address: string) {
interface ContractSubscriptionTableProps {
instanceUrl: string;
contractSubscriptions: EngineContractSubscription[];
isLoading: boolean;
isPending: boolean;
isFetched: boolean;
autoUpdate: boolean;
}
@@ -64,7 +64,7 @@ export const ContractSubscriptionTable: React.FC<
> = ({
instanceUrl,
contractSubscriptions,
isLoading,
isPending,
isFetched,
autoUpdate,
}) => {
@@ -218,7 +218,7 @@ export const ContractSubscriptionTable: React.FC<
title="contract subscriptions"
data={contractSubscriptions}
columns={columns}
isLoading={isLoading}
isPending={isPending}
isFetched={isFetched}
onMenuClick={[
{
@@ -52,7 +52,7 @@ export const EngineContractSubscriptions: React.FC<
<ContractSubscriptionTable
instanceUrl={instanceUrl}
contractSubscriptions={contractSubscriptionsQuery.data ?? []}
isLoading={contractSubscriptionsQuery.isLoading}
isPending={contractSubscriptionsQuery.isPending}
isFetched={contractSubscriptionsQuery.isFetched}
autoUpdate={autoUpdate}
/>
@@ -42,7 +42,7 @@ import { FormLabel } from "tw-components";
interface EngineInstancesTableProps {
instances: EngineInstance[];
isLoading: boolean;
isPending: boolean;
isFetched: boolean;
refetch: ReturnType<typeof useEngineInstances>["refetch"];
engineLinkPrefix: string;
@@ -50,7 +50,7 @@ interface EngineInstancesTableProps {
export const EngineInstancesTable: React.FC<EngineInstancesTableProps> = ({
instances,
isLoading,
isPending,
isFetched,
refetch,
engineLinkPrefix,
@@ -141,7 +141,7 @@ export const EngineInstancesTable: React.FC<EngineInstancesTableProps> = ({
data={instances}
columns={columns}
isFetched={isFetched}
isLoading={isLoading}
isPending={isPending}
onMenuClick={[
{
icon: BiPencil,
@@ -22,7 +22,7 @@ export const EngineInstancesList = (props: {
const instances = instancesQuery.data ?? [];
const trackEvent = useTrack();
if (instancesQuery.isLoading) {
if (instancesQuery.isPending) {
return (
<div className="flex justify-center items-center h-[300px]">
<Spinner className="size-14" />
@@ -122,7 +122,7 @@ export const EngineInstancesList = (props: {
<EngineInstancesTable
instances={instances}
isFetched={instancesQuery.isFetched}
isLoading={instancesQuery.isLoading}
isPending={instancesQuery.isPending}
refetch={instancesQuery.refetch}
engineLinkPrefix={props.engineLinkPrefix}
/>
@@ -51,7 +51,7 @@ import { prettyPrintCurrency } from "../utils";
interface BackendWalletsTableProps {
wallets: BackendWallet[];
instanceUrl: string;
isLoading: boolean;
isPending: boolean;
isFetched: boolean;
}
@@ -146,7 +146,7 @@ const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
wallets,
instanceUrl,
isLoading,
isPending,
isFetched,
}) => {
const editDisclosure = useDisclosure();
@@ -162,7 +162,7 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
title="backend wallets"
data={wallets}
columns={columns as ColumnDef<BackendWallet, string>[]}
isLoading={isLoading}
isPending={isPending}
isFetched={isFetched}
onMenuClick={[
{
@@ -90,7 +90,7 @@ export const EngineOverview: React.FC<EngineOverviewProps> = ({
<BackendWalletsTable
instanceUrl={instanceUrl}
wallets={backendWallets.data ?? []}
isLoading={backendWallets.isLoading}
isPending={backendWallets.isPending}
isFetched={backendWallets.isFetched}
/>
</Flex>
@@ -119,7 +119,7 @@ export const EngineOverview: React.FC<EngineOverviewProps> = ({
</Flex>
<TransactionsTable
transactions={transactionsQuery.data?.transactions ?? []}
isLoading={transactionsQuery.isLoading}
isPending={transactionsQuery.isPending}
isFetched={transactionsQuery.isFetched}
instanceUrl={instanceUrl}
/>
@@ -37,7 +37,7 @@ import { TransactionTimeline } from "./transaction-timeline";
interface TransactionsTableProps {
transactions: Transaction[];
isLoading: boolean;
isPending: boolean;
isFetched: boolean;
instanceUrl: string;
}
@@ -97,7 +97,7 @@ const columnHelper = createColumnHelper<Transaction>();
export const TransactionsTable: React.FC<TransactionsTableProps> = ({
transactions,
isLoading,
isPending,
isFetched,
instanceUrl,
}) => {
@@ -264,7 +264,7 @@ export const TransactionsTable: React.FC<TransactionsTableProps> = ({
title="transactions"
data={transactions}
columns={columns}
isLoading={isLoading}
isPending={isPending}
isFetched={isFetched}
onRowClick={(row) => {
setSelectedTransaction(row);
@@ -32,7 +32,7 @@ import { toDateTimeLocal } from "utils/date-utils";
interface AccessTokensTableProps {
instanceUrl: string;
accessTokens: AccessToken[];
isLoading: boolean;
isPending: boolean;
isFetched: boolean;
}
@@ -80,7 +80,7 @@ const columns = [
export const AccessTokensTable: React.FC<AccessTokensTableProps> = ({
instanceUrl,
accessTokens,
isLoading,
isPending,
isFetched,
}) => {
const editDisclosure = useDisclosure();
@@ -93,7 +93,7 @@ export const AccessTokensTable: React.FC<AccessTokensTableProps> = ({
title="access tokens"
data={accessTokens}
columns={columns}
isLoading={isLoading}
isPending={isPending}
isFetched={isFetched}
onMenuClick={[
{
@@ -32,7 +32,7 @@ import { Button, FormLabel, Text } from "tw-components";
interface AdminsTableProps {
instanceUrl: string;
admins: EngineAdmin[];
isLoading: boolean;
isPending: boolean;
isFetched: boolean;
}
@@ -67,7 +67,7 @@ const columns = [
export const AdminsTable: React.FC<AdminsTableProps> = ({
instanceUrl,
admins,
isLoading,
isPending,
isFetched,
}) => {
const editDisclosure = useDisclosure();
@@ -80,7 +80,7 @@ export const AdminsTable: React.FC<AdminsTableProps> = ({
title="admins"
data={admins}
columns={columns}
isLoading={isLoading}
isPending={isPending}
isFetched={isFetched}
onMenuClick={[
{
@@ -96,7 +96,7 @@ const StandardAccessTokensPanel = ({
<AccessTokensTable
instanceUrl={instanceUrl}
accessTokens={accessTokens.data ?? []}
isLoading={accessTokens.isLoading}
isPending={accessTokens.isPending}
isFetched={accessTokens.isFetched}
/>
<AddAccessTokenButton instanceUrl={instanceUrl} />
@@ -146,7 +146,7 @@ const KeypairAuthenticationPanel = ({
<KeypairsTable
instanceUrl={instanceUrl}
keypairs={keypairs.data || []}
isLoading={keypairs.isLoading}
isPending={keypairs.isPending}
isFetched={keypairs.isFetched}
/>
<AddKeypairButton instanceUrl={instanceUrl} />
@@ -1,7 +1,6 @@
"use client";
import { useEnginePermissions } from "@3rdweb-sdk/react/hooks/useEngine";
import { Flex } from "@chakra-ui/react";
import { Heading, Link, Text } from "tw-components";
import { AddAdminButton } from "./add-admin-button";
import { AdminsTable } from "./admins-table";
@@ -14,8 +13,8 @@ export const EngineAdmins: React.FC<EngineAdminsProps> = ({ instanceUrl }) => {
const admins = useEnginePermissions(instanceUrl);
return (
<Flex flexDir="column" gap={4}>
<Flex flexDir="column" gap={2}>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Heading size="title.md">Admins</Heading>
<Text>
Admins are allowed to manage your Engine instance from the dashboard.{" "}
@@ -29,14 +28,14 @@ export const EngineAdmins: React.FC<EngineAdminsProps> = ({ instanceUrl }) => {
</Link>
.
</Text>
</Flex>
</div>
<AdminsTable
instanceUrl={instanceUrl}
admins={admins.data || []}
isLoading={admins.isLoading}
isPending={admins.isPending}
isFetched={admins.isFetched}
/>
<AddAdminButton instanceUrl={instanceUrl} />
</Flex>
</div>
);
};
@@ -1,3 +1,4 @@
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
import {
type Keypair,
useEngineRemoveKeypair,
@@ -28,7 +29,7 @@ import { toDateTimeLocal } from "utils/date-utils";
interface KeypairsTableProps {
instanceUrl: string;
keypairs: Keypair[];
isLoading: boolean;
isPending: boolean;
isFetched: boolean;
}
@@ -41,6 +42,14 @@ const columns = [
return <Text>{cell.getValue()}</Text>;
},
}),
columnHelper.accessor("hash", {
header: "Key ID",
cell: (cell) => {
return (
<CopyAddressButton address={cell.getValue()} copyIconPosition="right" />
);
},
}),
columnHelper.accessor("publicKey", {
header: "Public Key",
cell: (cell) => {
@@ -64,7 +73,7 @@ const columns = [
export const KeypairsTable: React.FC<KeypairsTableProps> = ({
instanceUrl,
keypairs,
isLoading,
isPending,
isFetched,
}) => {
const removeDisclosure = useDisclosure();
@@ -76,7 +85,7 @@ export const KeypairsTable: React.FC<KeypairsTableProps> = ({
title="public keys"
data={keypairs}
columns={columns}
isLoading={isLoading}
isPending={isPending}
isFetched={isFetched}
onMenuClick={[
{
@@ -37,7 +37,7 @@ export const EngineRelayer: React.FC<EngineRelayerProps> = ({
<RelayersTable
instanceUrl={instanceUrl}
relayers={relayers.data || []}
isLoading={relayers.isLoading}
isPending={relayers.isPending}
isFetched={relayers.isFetched}
/>
<AddRelayerButton instanceUrl={instanceUrl} />
@@ -47,7 +47,7 @@ import { type AddModalInput, parseAddressListRaw } from "./add-relayer-button";
interface RelayersTableProps {
instanceUrl: string;
relayers: EngineRelayer[];
isLoading: boolean;
isPending: boolean;
isFetched: boolean;
}
@@ -56,7 +56,7 @@ const columnHelper = createColumnHelper<EngineRelayer>();
export const RelayersTable: React.FC<RelayersTableProps> = ({
instanceUrl,
relayers,
isLoading,
isPending,
isFetched,
}) => {
const editDisclosure = useDisclosure();
@@ -156,7 +156,7 @@ export const RelayersTable: React.FC<RelayersTableProps> = ({
title="relayers"
data={relayers}
columns={columns}
isLoading={isLoading}
isPending={isPending}
isFetched={isFetched}
onMenuClick={[
{
@@ -70,37 +70,48 @@ export const EngineSystemMetrics: React.FC<EngineStatusProps> = ({
);
}
let queueMetricsPanel = <Spinner className="h-4 w-4" />;
if (!queueMetricsQuery.data || queueMetricsQuery.isError) {
queueMetricsPanel = <p>N/A</p>;
} else {
let queueMetricsPanel = (
<div className="border border-border rounded-lg min-h-[200px] flex items-center justify-center">
<Spinner className="size-6" />
</div>
);
if (
!queueMetricsQuery.isPending &&
(!queueMetricsQuery.data || queueMetricsQuery.isError)
) {
queueMetricsPanel = (
<div className="border border-border rounded-lg min-h-[200px] flex items-center justify-center">
No Data Available
</div>
);
} else if (queueMetricsQuery.data) {
const numQueued = queueMetricsQuery.data.result.queued;
const numPending = queueMetricsQuery.data.result.pending;
const msToSend = queueMetricsQuery.data.result.latency?.msToSend;
const msToMine = queueMetricsQuery.data.result.latency?.msToMine;
queueMetricsPanel = (
<Card p={16}>
<Stack spacing={4}>
<Card p={8}>
<Stack spacing={6}>
<Flex gap={2} align="center">
<Icon as={FaChartArea} />
<Heading size="title.md">Queue Metrics</Heading>
</Flex>
<div className="flex gap-x-24">
<div className="flex flex-col lg:flex-row gap-6 lg:gap-12">
<div className="flex-col gap-y-4">
<h2 className="font-semibold"># queued</h2>
<p>{numQueued}</p>
<h2 className="font-semibold"> Queued</h2>
<p className="text-muted-foreground">{numQueued}</p>
</div>
<div className="flex-col gap-y-4">
<h2 className="font-semibold"># pending</h2>
<p>{numPending}</p>
<h2 className="font-semibold">Pending</h2>
<p className="text-muted-foreground">{numPending}</p>
</div>
{msToSend && (
<div className="flex-col gap-y-4">
<h2 className="font-semibold">Time to send</h2>
<p>
<p className="text-muted-foreground">
p50 {(msToSend.p50 / 1000).toFixed(2)}s, p90{" "}
{(msToSend.p90 / 1000).toFixed(2)}s
</p>
@@ -109,8 +120,10 @@ export const EngineSystemMetrics: React.FC<EngineStatusProps> = ({
{msToMine && (
<div className="flex-col gap-y-4">
<h2 className="font-semibold">Time to mine</h2>
p50 {(msToMine.p50 / 1000).toFixed(2)}s, p90{" "}
{(msToMine.p90 / 1000).toFixed(2)}s
<p className="text-muted-foreground">
p50 {(msToMine.p50 / 1000).toFixed(2)}s, p90{" "}
{(msToMine.p90 / 1000).toFixed(2)}s
</p>
</div>
)}
</div>
@@ -94,7 +94,7 @@ export const ContractCard: React.FC<ContractCardProps> = ({
`${publisher}/${contractId}/${version}`,
);
const showSkeleton = publishedContractResult.isLoading;
const showSkeleton = publishedContractResult.isPending;
return (
<article
@@ -24,7 +24,7 @@ export const ContractPublisher: React.FC<ContractPublisherProps> = ({
)}
>
<PublisherAvatar
isLoading={showSkeleton}
isPending={showSkeleton}
boxSize={5}
address={addressOrEns || ""}
/>
@@ -1,4 +1,3 @@
import { Flex } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { Text } from "tw-components";
@@ -58,9 +57,9 @@ const Timer = ({ dateStr }: TimerProps) => {
];
return (
<Flex gap="2">
<div className="flex flex-row gap-2">
{items.map(({ label, value }) => (
<Flex flexDir="column" key={label} align="center">
<div className="flex flex-col items-center" key={label}>
<Text
fontSize={{ base: "36px", md: "48px" }}
bg="#FFFFFF14"
@@ -72,9 +71,9 @@ const Timer = ({ dateStr }: TimerProps) => {
{value < 10 ? `0${value}` : value}
</Text>
<Text color="white">{value === 1 ? label : `${label}s`}</Text>
</Flex>
</div>
))}
</Flex>
</div>
);
};
@@ -1,16 +1,10 @@
import { Flex } from "@chakra-ui/react";
import { LandingGridSection } from "components/landing-pages/grid-section";
import { LandingIconSectionItem } from "components/landing-pages/icon-section-item";
import { Heading } from "tw-components";
const DesireSection = () => {
return (
<Flex
w="full"
flexDir={{ base: "column", xl: "row" }}
alignItems={{ base: "center", xl: "flex-start" }}
gap="60px"
>
<div className="w-full flex flex-col xl:flex-row items-center xl:items-start gap-[60px]">
<Heading
size="title.lg"
fontWeight="semibold"
@@ -19,7 +13,7 @@ const DesireSection = () => {
>
thirdweb will...
</Heading>
<Flex maxW="2xl">
<div className="max-w-[96rem] flex flex-row">
<LandingGridSection desktopColumns={2}>
<LandingIconSectionItem
shouldShowNoBorder
@@ -53,8 +47,8 @@ const DesireSection = () => {
description="There is infinite upside to building in open. Open-sourcing all of our tools increases transparency and security for the whole industry. Anybody can contribute to thirdweb by proposing new features, identifying bugs and optimising gas."
/>
</LandingGridSection>
</Flex>
</Flex>
</div>
</div>
);
};
@@ -1,10 +1,9 @@
import { Flex } from "@chakra-ui/react";
import { LandingDesktopMobileImage } from "components/landing-pages/desktop-mobile-image";
import { Heading } from "tw-components";
const HeroSection = ({ text }: { text: string }) => {
return (
<Flex flexDir="column" alignItems="center" mt={{ base: 20, md: 140 }}>
<div className="flex flex-col items-center mt-5 md:mt-[140px]">
<LandingDesktopMobileImage
image={require("../../../public/assets/landingpage/desktop/xl-logo.png")}
mobileImage={require("../../../public/assets/landingpage/mobile/xl-logo.png")}
@@ -20,7 +19,7 @@ const HeroSection = ({ text }: { text: string }) => {
>
{text}
</Heading>
</Flex>
</div>
);
};
@@ -1,16 +1,10 @@
import { Flex } from "@chakra-ui/react";
import { ChakraNextImage } from "components/Image";
import { LandingGridSection } from "components/landing-pages/grid-section";
import { Heading, Text } from "tw-components";
const MajorSection = () => {
return (
<Flex
w="full"
flexDir={{ base: "column", xl: "row" }}
alignItems={{ base: "center", xl: "flex-start" }}
gap="60px"
>
<div className="w-full flex flex-col xl:flex-row items-center xl:items-start gap-[60px]">
<Heading
size="title.lg"
fontWeight="semibold"
@@ -19,29 +13,29 @@ const MajorSection = () => {
>
However, there are two major obstacles to mass adoption.
</Heading>
<Flex maxW="2xl">
<div className="flex flex-row max-w-[96rem]">
<LandingGridSection desktopColumns={2}>
<Flex flexDir="column" gap={6}>
<div className="flex flex-col gap-6">
<ChakraNextImage
src={require("../../../public/assets/landingpage/mobile/complexity.png")}
alt="img-one"
/>
<Flex flexDir="column" gap={4}>
<div className="flex flex-col gap-4">
<Text size="body.xl" color="white" fontWeight="bold">
Developer complexity
</Text>
To build a web3 app, developers need to piece together 10+
different tools that don&apos;t natively talk to each other —
creating a messy, fragmented DX that stifles innovation.
</Flex>
</Flex>
</div>
</div>
<Flex flexDir="column" gap={6}>
<div className="flex flex-col gap-6">
<ChakraNextImage
src={require("../../../public/assets/landingpage/mobile/user-experience.png")}
alt="img-one"
/>
<Flex flexDir="column" gap={4}>
<div className="flex flex-col gap-4">
<Text size="body.xl" color="white" fontWeight="bold">
User experience
</Text>
@@ -49,11 +43,11 @@ const MajorSection = () => {
their private keys, purchase & transfer crypto, pay gas, and sign
a transaction for every action — creating a daunting onboarding
process that stifles adoption.
</Flex>
</Flex>
</div>
</div>
</LandingGridSection>
</Flex>
</Flex>
</div>
</div>
);
};
@@ -1,6 +1,5 @@
import { useUpdateAccount } from "@3rdweb-sdk/react/hooks/useApi";
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
import { Flex } from "@chakra-ui/react";
import { useTrack } from "hooks/analytics/useTrack";
import { Button, TrackedLink } from "tw-components";
import { shortenString } from "utils/usedapp-external";
@@ -87,8 +86,8 @@ const OnboardingLinkWallet: React.FC<OnboardingLinkWalletProps> = ({
}
/>
<form>
<Flex gap={8} flexDir="column" w="full">
<Flex flexDir="column" gap={3}>
<div className="flex flex-col w-full gap-8">
<div className="flex flex-col gap-3">
<Button
w="full"
size="lg"
@@ -111,8 +110,8 @@ const OnboardingLinkWallet: React.FC<OnboardingLinkWalletProps> = ({
>
Use another email
</Button>
</Flex>
</Flex>
</div>
</div>
</form>
</>
);

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