Compare commits

...
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
ca6c913361 Version Packages (#6132)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-03 22:41:38 -08:00
MananTank cf166e4c04 [TOOL-3216] Dashboard: Add Faucet Refill, Fix toast colors on light mode (#6147)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving the UI and functionality of the dashboard by updating theme management, enhancing the `FaucetButton` component, and integrating a new modal for refilling the faucet. It also modifies color schemes for buttons and introduces a loading state.

### Detailed summary
- Removed `<Toaster richColors />` from `RootLayout`.
- Added `<ToasterSetup />` component in `AppRouterProviders` for dynamic theming.
- Updated button color schemes in `sdk-component-theme.ts`.
- Enhanced `FaucetButton` with loading states and a new modal for refilling.
- Introduced `SendFundsToFaucetModalButton` and `SendFundsToFaucetModalContent` components.
- Added form validation for the amount in the faucet refill modal.

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

<!-- end pr-codex -->
2025-02-04 01:03:52 +00:00
MananTank a51192ffbd [TOOL-3055] Dashboard: Add Refresh Balances button for engine backend wallets (#6153)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the `BackendWalletsTable` component by improving the balance display functionality, adding a refresh feature, and optimizing some data fetching mechanisms.

### Detailed summary
- Updated `backendWalletBalance` keys in `cache-keys.ts`.
- Introduced `backendWalletBalanceAll` function.
- Added `isFetching` state to balance fetching logic.
- Changed balance skeleton size.
- Replaced `LinkButton` with `Link` for external navigation.
- Added a refresh button with tooltip for balance updates.
- Included `queryClient` in dependencies for memoized columns.

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

<!-- end pr-codex -->
2025-02-04 01:00:27 +00:00
MananTank 6fd36aabc7 [TOOL-3286] Dashboard: Add Feedback button on header, Add more links in mobile burger menu (#6145) 2025-02-03 19:43:05 +00:00
kumaryash90 08cc48910d [SDK] Update implementation addr extraction logic (#6093)
TOOL-3258

<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces support for EIP-7702 by adding functionality to extract the implementation address from a specific bytecode pattern. It includes a new utility function and a corresponding test case.

### Detailed summary
- Added EIP-7702 support in `extractMinimalProxyImplementationAddress.ts` to extract the implementation address from bytecode.
- Implemented a check for bytecode length and prefix.
- Added a test case in `extractMinimalProxyImplementationAddress.test.ts` to validate the new functionality.

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

<!-- end pr-codex -->
2025-02-03 17:56:14 +00:00
MananTank f03ded300f [TOOL-3276] SDK: Fix JSDoc comments of download function (#6133)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on updating the import path for the `createThirdwebClient` function from `@thirdweb-dev/sdk` to `thirdweb` in the `download.ts` file.

### Detailed summary
- Changed import of `createThirdwebClient` from `@thirdweb-dev/sdk` to `thirdweb` in multiple instances within the `download.ts` file.

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

<!-- end pr-codex -->
2025-02-02 03:27:49 +00:00
kumaryash90 b651bae4ba Update addresses (#6141)
TOOL-3279

<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refining type imports in `usageV2.ts`, adding a new address in `implementations.ts`, and simplifying the `package.json` formatting for better readability.

### Detailed summary
- Changed import style in `usageV2.ts` to use `import type`.
- Added a new address entry in `implementations.ts`.
- Reformatted `typesVersions` and `files` sections in `package.json` for improved clarity.

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

<!-- end pr-codex -->
2025-02-02 01:19:51 +00:00
Phillip HoandGitHub 66833a832c fix: usageV2 API endpoint (#6140) 2025-01-31 20:20:58 -08:00
Alec AnanianandGitHub 9d5828eeab [SDK] Fix: skip factory entrypoint lookup for ZKsync chains (#6116) 2025-01-31 17:10:33 -08:00
Jonas DanielsandGitHub b191658fcd update login script URL to HTTPS (#6138) 2025-01-31 16:26:11 -08:00
Jonas DanielsandGitHub a6b7e8d818 update dependencies (#6137) 2025-01-31 16:23:07 -08:00
Phillip HoandGitHub f4f93e6353 [service-utils] ESM imports for compression (#6139) 2025-01-31 16:17:41 -08:00
gregfromstl b693b78645 [SDK] Fix: Switch underlying admin wallet with ecosystem smart account (#6119)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the functionality of smart wallets in the `thirdweb` ecosystem, ensuring they correctly trigger chain switches and improving related wallet checks and configurations.

### Detailed summary
- Fixed smart wallets to trigger chain switch on admin wallets.
- Updated `smartWalletGetCapabilities` to accept a generic `Wallet` type.
- Implemented `isSmartWallet` function to check for smart wallet status.
- Modified `getSmartWallet` to throw an error for non-smart wallets.
- Refactored wallet checks in `createConnectionManager`.
- Added tests for `isSmartWallet` and `getSmartWallet`.

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

<!-- end pr-codex -->
2025-01-31 22:21:58 +00:00
jnsdls daceeed2ec remove deprecated NFT embed functionality (#6136)
fixes: CORE-720
2025-01-31 22:18:32 +00:00
Jonas DanielsandGitHub 5f7c726917 add JWT signing and redirect for login flow (#6129) 2025-01-31 13:44:37 -08:00
Jonas DanielsandGitHub 2405456e5f add knip dependency to login app (#6135) 2025-01-31 13:03:26 -08:00
Phillip HoandGitHub a76e6534c4 [service-utils] switch to lz4 for compression lib (#6131) 2025-01-31 10:41:53 -08:00
Phillip HoandGitHub 0fb586db52 [service-utils] fix: use lz4js for usageV2 (#6130) 2025-01-31 10:22:48 -08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
f8ebbe54d9 Version Packages (#6118)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-31 09:17:07 -08:00
Phillip HoandGitHub ca756819db [service-utils] CJS import for kafkajs-lz4 (#6127) 2025-01-30 17:29:44 -08:00
Jonas DanielsandGitHub 7217e774b2 update login permissions UI and styling (#6128) 2025-01-30 17:26:07 -08:00
Jonas DanielsandGitHub 5de72c5ecd fix arrow function in login script (#6126) 2025-01-30 17:05:46 -08:00
Jonas DanielsandGitHub 60c5c26583 add thirdweb login client script (#6125) 2025-01-30 16:57:22 -08:00
Jonas DanielsandGitHub dc74c3b4b9 add Suspense wrapper to root layout (#6124) 2025-01-30 16:15:42 -08:00
Jonas DanielsandGitHub 69a921d01f tw:login (#6122) 2025-01-30 16:09:20 -08:00
Phillip HoandGitHub 9abdb92028 [service-utils] import LZ4 compression for usageV2 (#6123) 2025-01-30 15:59:08 -08:00
Phillip HoandGitHub 6ed2fd13de [service-utils] fix: Add LZ4 package for usageV2 compression (#6121) 2025-01-30 14:55:37 -08:00
Phillip HoandGitHub a22457a69c [service-utils] Expose broker settings for usageV2 (#6120) 2025-01-30 12:38:52 -08:00
gregfromstl ae675db8c8 [Wagmi Adapter] Fix: use smart account chain in getProvider (#6117)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on fixing the `getProvider` function in the `wagmi-adapter` package to improve the autoconnection behavior with the account factory by enhancing the chain selection logic.

### Detailed summary
- Updated `getProvider` function to include `args.smartAccount?.chain?.id` in chain selection.
- Changed the logic for defining `chain` to prioritize `params?.chainId`, followed by the smart account's chain ID, the last stored chain ID, and defaulting to `1`.

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

<!-- end pr-codex -->
2025-01-30 19:17:54 +00:00
MananTank 54c9333fc9 [TOOL-3268] Dashboard: Fix in-app wallet settings page form submit not working (#6115) 2025-01-30 17:12:59 +00:00
MananTank 541cf0b3e9 [TOOL-3259] Dashboard: Fix Claimable Module UI crashing on for ERC20 token currency set in claim condition (#6114)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `ClaimableModule` component by integrating the `useQuery` hook for fetching currency decimals and refining the logic for determining when to fetch this data.

### Detailed summary
- Added `useQuery` to fetch currency decimals based on the `claimConditionCurrency`.
- Refactored the logic to check if currency decimals should be fetched.
- Updated the conditional rendering of fetched data in the `ClaimableModule`.
- Made the "Add Claim Condition" button disabled based on `props.isOwnerAccount`.

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

<!-- end pr-codex -->
2025-01-30 15:25:38 +00:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
c65022174d Version Packages (#6111)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-29 22:45:20 -08:00
Phillip HoandGitHub 6818cbabf2 chore: change to per-product usageV2 topic (#6113) 2025-01-29 22:32:42 -08:00
gregfromstl a7fb844d87 [Dashboard] Fix: Remove learn more button (#6109)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on simplifying the `WalletAddress` component by removing the "Learn more" button and its associated link to the blog, while also cleaning up the imports by removing the unused `ExternalLinkIcon`.

### Detailed summary
- Removed the unused import of `ExternalLinkIcon` from `lucide-react`.
- Eliminated the "Learn more" button, which included:
  - The button element with `asChild`, `variant`, `className`, and `size` props.
  - The inner `<a>` element linking to the blog post.

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

<!-- end pr-codex -->
2025-01-30 06:05:07 +00:00
Manan TankandGitHub bf431961ec [TOOL-3260] SDK: Fix Pay Modal logging react-query error when opened (#6104) 2025-01-29 16:59:25 -08:00
gregandGitHub c5d4cab8f8 [Portal] Fix: deployContract doc imports (#6108) 2025-01-29 16:55:21 -08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
1ee0dffec4 Version Packages (#6107)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-29 14:52:28 -08:00
Jonas DanielsandGitHub 33611409b0 update dependencies (#6106) 2025-01-29 14:34:20 -08:00
Joaquim VergesandGitHub ab55aec10c [SDK] feat: Optional wallet activation during connection (#6105) 2025-01-29 14:21:08 -08:00
74 changed files with 3290 additions and 1960 deletions
+11 -11
View File
@@ -46,12 +46,12 @@
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tooltip": "1.1.7",
"@sentry/nextjs": "8.51.0",
"@sentry/nextjs": "8.53.0",
"@shazow/whatsabi": "^0.19.0",
"@tanstack/react-query": "5.65.0",
"@tanstack/react-query": "5.66.0",
"@tanstack/react-table": "^8.20.6",
"@thirdweb-dev/service-utils": "workspace:*",
"@vercel/functions": "^1.5.2",
"@vercel/functions": "^1.6.0",
"@vercel/og": "^0.6.5",
"abitype": "1.0.8",
"chakra-react-select": "^4.7.6",
@@ -74,7 +74,7 @@
"nextjs-toploader": "^1.6.12",
"openapi-types": "^12.1.3",
"p-limit": "^6.2.0",
"papaparse": "^5.5.1",
"papaparse": "^5.5.2",
"pluralize": "^8.0.0",
"posthog-js": "1.67.1",
"qrcode": "^1.5.3",
@@ -83,7 +83,7 @@
"react-day-picker": "^8.10.1",
"react-dom": "19.0.0",
"react-dropzone": "^14.3.5",
"react-error-boundary": "^4.1.2",
"react-error-boundary": "^5.0.0",
"react-hook-form": "7.54.2",
"react-markdown": "^9.0.1",
"react-table": "^7.8.0",
@@ -91,9 +91,9 @@
"remark-gfm": "^4.0.0",
"server-only": "^0.0.1",
"shiki": "1.27.0",
"sonner": "^1.7.2",
"sonner": "^1.7.4",
"spdx-correct": "^3.2.0",
"swagger-ui-react": "^5.18.2",
"swagger-ui-react": "^5.18.3",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"thirdweb": "workspace:*",
@@ -106,7 +106,7 @@
"@chromatic-com/storybook": "3.2.4",
"@next/bundle-analyzer": "15.1.6",
"@next/eslint-plugin-next": "15.1.6",
"@playwright/test": "1.50.0",
"@playwright/test": "1.50.1",
"@storybook/addon-essentials": "8.5.2",
"@storybook/addon-interactions": "8.5.2",
"@storybook/addon-links": "8.5.2",
@@ -117,7 +117,7 @@
"@storybook/react": "8.5.2",
"@storybook/test": "8.5.2",
"@types/color": "4.2.0",
"@types/node": "22.10.10",
"@types/node": "22.13.0",
"@types/papaparse": "^5.3.15",
"@types/pluralize": "^0.0.33",
"@types/qrcode": "^1.5.5",
@@ -129,12 +129,12 @@
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
"autoprefixer": "^10.4.19",
"checkly": "^4.18.0",
"checkly": "^4.19.1",
"eslint": "8.57.0",
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
"eslint-plugin-storybook": "^0.11.1",
"knip": "5.43.5",
"knip": "5.43.6",
"next-sitemap": "^4.2.3",
"postcss": "8.5.1",
"storybook": "8.5.2",
@@ -8,7 +8,7 @@ import {
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
import { useClipboard } from "hooks/useClipboard";
import { Check, Copy, ExternalLinkIcon } from "lucide-react";
import { Check, Copy } from "lucide-react";
import { useMemo } from "react";
import { type ThirdwebClient, isAddress } from "thirdweb";
import { ZERO_ADDRESS } from "thirdweb";
@@ -148,21 +148,6 @@ export function WalletAddress(props: {
);
})
)}
<Button
asChild
variant="upsell"
className="flex flex-row items-center gap-2 text-sm"
size="sm"
>
<a
target="_blank"
href="https://blog.thirdweb.com/changelog/introducing-the-social-sdk?ref=dashboard-social-wallet"
rel="noreferrer"
>
Learn more
<ExternalLinkIcon className="size-4" />
</a>
</Button>
</div>
</HoverCardContent>
</HoverCard>
@@ -95,7 +95,8 @@ export const engineKeys = {
backendWallet: (address: string, chainId: number) =>
["backendWallet", address, chainId] as const,
backendWalletBalance: (address: string, chainId: number) =>
[...engineKeys.backendWallet(address, chainId), "balance"] as const,
["backendWallet", "balance", address, chainId] as const,
backendWalletBalanceAll: () => ["backendWallet", "balance"] as const,
corsUrls: (instance: string) =>
[...engineKeys.all, instance, "corsUrls"] as const,
ipAllowlist: (instance: string) =>
@@ -548,41 +548,6 @@ export function useResendEmailConfirmation() {
});
}
export function useApiKeys(props: {
isLoggedIn: boolean;
}) {
const address = useActiveAccount()?.address;
return useQuery({
queryKey: apiKeys.keys(address || ""),
queryFn: async () => {
type Result = {
data: ApiKey[];
error?: { message: string };
};
const res = await apiServerProxy<Result>({
pathname: "/v1/keys",
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
throw new Error(res.error);
}
const json = res.data;
if (json.error) {
throw new Error(json.error.message);
}
return json.data;
},
enabled: !!address && props.isLoggedIn,
});
}
export function useCreateApiKey() {
const address = useActiveAccount()?.address;
const queryClient = useQueryClient();
@@ -1,13 +1,32 @@
"use client";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
THIRDWEB_ENGINE_FAUCET_WALLET,
TURNSTILE_SITE_KEY,
} from "@/constants/env";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { zodResolver } from "@hookform/resolvers/zod";
import { Turnstile } from "@marsidev/react-turnstile";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
@@ -17,9 +36,21 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { toUnits } from "thirdweb";
import {
type ThirdwebClient,
prepareTransaction,
toUnits,
toWei,
} from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { useActiveAccount, useWalletBalance } from "thirdweb/react";
import type { Chain } from "thirdweb/chains";
import {
useActiveAccount,
useActiveWalletChain,
useSendTransaction,
useSwitchActiveWalletChain,
useWalletBalance,
} from "thirdweb/react";
import { z } from "zod";
import { isOnboardingComplete } from "../../../../../../login/onboarding/isOnboardingRequired";
@@ -137,6 +168,35 @@ export function FaucetButton({
const form = useForm<z.infer<typeof claimFaucetSchema>>();
// loading state
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
return (
<Button variant="outline" className="w-full gap-2">
Checking Faucet <Spinner className="size-3" />
</Button>
);
}
// faucet is empty
if (isFaucetEmpty) {
return (
<div className="w-full">
<div className="mb-3 text-center text-muted-foreground text-sm">
Faucet is empty right now
</div>
<SendFundsToFaucetModalButton
chain={definedChain}
isLoggedIn={!!twAccount}
client={client}
chainMeta={chain}
onFaucetRefill={() => {
faucetWalletBalanceQuery.refetch();
}}
/>
</div>
);
}
// Force users to log in to claim the faucet
if (!address || !twAccount) {
return (
@@ -164,24 +224,6 @@ export function FaucetButton({
);
}
// loading state
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
return (
<Button variant="outline" className="w-full gap-2">
Checking Faucet <Spinner className="size-3" />
</Button>
);
}
// faucet is empty
if (isFaucetEmpty) {
return (
<Button variant="outline" disabled className="!opacity-100 w-full">
Faucet is empty right now
</Button>
);
}
// Can not claim
if (canClaimFaucetQuery.data && canClaimFaucetQuery.data.canClaim === false) {
return (
@@ -250,3 +292,165 @@ export function FaucetButton({
</div>
);
}
const faucetFormSchema = z.object({
amount: z.coerce.number().refine((value) => value > 0, {
message: "Amount must be greater than 0",
}),
});
function SendFundsToFaucetModalButton(props: {
chain: Chain;
isLoggedIn: boolean;
client: ThirdwebClient;
chainMeta: ChainMetadata;
onFaucetRefill: () => void;
}) {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="default" className="w-full">
Refill Faucet
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="mb-2">
<DialogTitle>Refill Faucet</DialogTitle>
<DialogDescription>Send funds to faucet wallet</DialogDescription>
</DialogHeader>
<SendFundsToFaucetModalContent {...props} />
</DialogContent>
</Dialog>
);
}
function SendFundsToFaucetModalContent(props: {
chain: Chain;
isLoggedIn: boolean;
client: ThirdwebClient;
chainMeta: ChainMetadata;
onFaucetRefill: () => void;
}) {
const account = useActiveAccount();
const activeChain = useActiveWalletChain();
const switchActiveWalletChain = useSwitchActiveWalletChain();
const sendTxMutation = useSendTransaction({
payModal: false,
});
const switchChainMutation = useMutation({
mutationFn: async () => {
await switchActiveWalletChain(props.chain);
},
});
const form = useForm<z.infer<typeof faucetFormSchema>>({
resolver: zodResolver(faucetFormSchema),
defaultValues: {
amount: 0.1,
},
});
function onSubmit(values: z.infer<typeof faucetFormSchema>) {
const sendNativeTokenTx = prepareTransaction({
chain: props.chain,
client: props.client,
to: THIRDWEB_ENGINE_FAUCET_WALLET,
value: toWei(values.amount.toString()),
});
const promise = sendTxMutation.mutateAsync(sendNativeTokenTx);
toast.promise(promise, {
success: `Sent ${values.amount} ${props.chainMeta.nativeCurrency.symbol} to faucet`,
error: `Failed to send ${values.amount} ${props.chainMeta.nativeCurrency.symbol} to faucet`,
});
promise.then(() => {
props.onFaucetRefill();
});
}
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex min-w-0 flex-col gap-5"
>
<div className="min-w-0">
<p className="mb-2 text-foreground text-sm"> Faucet Wallet </p>
<CopyTextButton
copyIconPosition="right"
variant="outline"
className="w-full justify-between bg-card py-2 font-mono"
textToCopy={THIRDWEB_ENGINE_FAUCET_WALLET}
textToShow={THIRDWEB_ENGINE_FAUCET_WALLET}
tooltip={undefined}
/>
</div>
<FormField
control={form.control}
name="amount"
render={({ field }) => (
<FormItem>
<FormLabel>Amount</FormLabel>
<FormControl>
<div className="relative">
<Input
{...field}
type="number"
className="h-auto bg-card text-2xl md:text-2xl [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
/>
<div className="-translate-y-1/2 absolute top-1/2 right-4 text-muted-foreground text-sm">
{props.chainMeta.nativeCurrency.symbol}
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{!account && (
<CustomConnectWallet
chain={props.chain}
loginRequired={false}
isLoggedIn={props.isLoggedIn}
connectButtonClassName="!w-full"
detailsButtonClassName="!w-full"
/>
)}
{account && activeChain && (
<div>
{activeChain.id === props.chain.id ? (
<Button
key="submit"
type="submit"
className="mt-4 w-full gap-2"
disabled={sendTxMutation.isPending}
>
{sendTxMutation.isPending && <Spinner className="size-4" />}
Send funds to faucet
</Button>
) : (
<Button
key="switch"
className="mt-4 w-full gap-2"
disabled={switchChainMutation.isPending}
onClick={() => switchChainMutation.mutate()}
>
Switch to {props.chainMeta.name}{" "}
{switchChainMutation.isPending && (
<Spinner className="size-4" />
)}
</Button>
)}
</div>
)}
</form>
</Form>
);
}
@@ -39,12 +39,6 @@ export function getContractPageSidebarLinks(data: {
href: `${layoutPrefix}/events`,
exactMatch: true,
},
{
label: "Embed",
href: `${layoutPrefix}/embed`,
hide: data.metadata.embedType === null,
exactMatch: true,
},
{
label: "Analytics",
href: `${layoutPrefix}/analytics`,
@@ -1,35 +0,0 @@
"use client";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { EmbedSetup } from "./embed-setup";
export function EmbedSetupClient(props: {
contract: ThirdwebContract;
twAccount: Account | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (metadataQuery.data.embedType === null) {
return <RedirectToContractOverview contract={props.contract} />;
}
return (
<EmbedSetup
contract={props.contract}
ercOrMarketplace={metadataQuery.data.embedType}
twAccount={props.twAccount}
/>
);
}
@@ -1,613 +0,0 @@
"use client";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { CodeClient } from "@/components/ui/code/code.client";
import {
type Account,
useApiKeys,
useCreateApiKey,
} from "@3rdweb-sdk/react/hooks/useApi";
import { Flex, FormControl, Input, Select } from "@chakra-ui/react";
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
import { useTrack } from "hooks/analytics/useTrack";
import { useAllChainsData } from "hooks/chains/allChains";
import { AlertCircleIcon } from "lucide-react";
import { useTheme } from "next-themes";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import type { StoredChain } from "stores/chainStores";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { useActiveAccount } from "thirdweb/react";
import { Card, FormHelperText, FormLabel, Heading, Text } from "tw-components";
interface EmbedSetupProps {
contract: ThirdwebContract;
ercOrMarketplace: string;
twAccount: Account | undefined;
}
// MAKE SURE THIS IS v1 embed hashes!!
const IPFS_URI = "bafybeigdie2yyiazou7grjowoevmuip6akk33nqb55vrpezqdwfssrxyfy";
const ERC721_IPFS_URI =
"bafybeicd3qfzelz4su7ng6n523virdsgobrc5pcbarhwqv3dj3drh645pi";
interface IframeSrcOptions {
clientId: string;
chain: string;
tokenId?: string;
listingId?: string;
listingType?: string;
directListingId?: string;
englishAuctionId?: string;
relayUrl?: string;
theme?: string;
primaryColor?: string;
secondaryColor?: string;
biconomyApiKey?: string;
biconomyApiId?: string;
}
const colorOptions = [
"purple",
"blue",
"orange",
"pink",
"green",
"red",
"teal",
"cyan",
"yellow",
];
const isValidUrl = (url: string | undefined) => {
if (!url) {
return false;
}
try {
new URL(url);
return true;
} catch {
return false;
}
};
const buildIframeSrc = (
contract: ThirdwebContract,
ercOrMarketplace?: string,
options?: IframeSrcOptions,
): string => {
const contractPath =
ercOrMarketplace === "erc721" ? "" : `${ercOrMarketplace}.html`;
const contractEmbedHash =
ercOrMarketplace === "erc721" ? ERC721_IPFS_URI : IPFS_URI;
if (!contract || !options || !contractEmbedHash) {
return "";
}
const {
clientId,
chain,
tokenId,
listingId,
listingType,
directListingId,
englishAuctionId,
relayUrl,
theme,
primaryColor,
secondaryColor,
biconomyApiKey,
biconomyApiId,
} = options;
const url = new URL(
`https://embed.ipfscdn.io/ipfs/${contractEmbedHash}/${contractPath}`,
);
url.searchParams.append("contract", contract.address);
url.searchParams.append("chain", chain);
url.searchParams.append("clientId", clientId);
if (tokenId !== undefined && ercOrMarketplace === "erc1155") {
url.searchParams.append("tokenId", tokenId.toString());
}
if (listingId !== undefined && ercOrMarketplace === "marketplace") {
url.searchParams.append("listingId", listingId.toString());
}
if (
directListingId !== undefined &&
ercOrMarketplace === "marketplace-v3" &&
listingType === "direct-listing"
) {
url.searchParams.append("directListingId", directListingId.toString());
}
if (
englishAuctionId !== undefined &&
ercOrMarketplace === "marketplace-v3" &&
listingType === "english-auction"
) {
url.searchParams.append("englishAuctionId", englishAuctionId.toString());
}
if (isValidUrl(relayUrl)) {
url.searchParams.append("relayUrl", relayUrl || "");
}
if (biconomyApiKey) {
url.searchParams.append("biconomyApiKey", biconomyApiKey);
}
if (biconomyApiId) {
url.searchParams.append("biconomyApiId", biconomyApiId);
}
if (theme) {
url.searchParams.append("theme", theme);
}
if (primaryColor && primaryColor !== "default") {
url.searchParams.append("primaryColor", primaryColor);
}
if (secondaryColor && secondaryColor !== "orange") {
url.searchParams.append("secondaryColor", secondaryColor);
}
return url.toString();
};
// taken from @thirdweb-dev /chains
// @internal
function minimizeChain(
chain: ChainMetadata,
): Pick<
ChainMetadata,
| "name"
| "chain"
| "rpc"
| "nativeCurrency"
| "shortName"
| "chainId"
| "testnet"
| "slug"
| "icon"
> {
const [firstRpc] = chain.rpc;
return {
name: chain.name,
chain: chain.chain,
rpc: [firstRpc || ""],
nativeCurrency: chain.nativeCurrency,
shortName: chain.shortName,
chainId: chain.chainId,
testnet: chain.testnet,
slug: chain.slug,
icon: chain.icon,
};
}
// taken from @thirdweb-dev /chains
// @internal
type ChainConfiguration = {
rpc?: string | string[];
};
function configureChain(
chain: ChainMetadata,
chainConfig: ChainConfiguration,
): ChainMetadata {
let additionalRPCs: string[] = [];
if (chainConfig?.rpc) {
if (typeof chainConfig.rpc === "string") {
additionalRPCs = [chainConfig.rpc];
} else {
additionalRPCs = chainConfig.rpc;
}
}
// prepend additional RPCs to the chain's RPCs
return { ...chain, rpc: [...additionalRPCs, ...chain.rpc] };
}
export const EmbedSetup: React.FC<EmbedSetupProps> = ({
contract,
ercOrMarketplace,
twAccount,
}) => {
const trackEvent = useTrack();
const { theme } = useTheme();
const apiKeys = useApiKeys({
isLoggedIn: !!twAccount,
});
const createKeyMutation = useCreateApiKey();
const validApiKey = (apiKeys.data || []).find(
(apiKey) =>
(apiKey.domains.includes("*") ||
apiKey.domains.includes("embed.ipfscdn.io") ||
apiKey.domains.includes("*.ipfscdn.io")) &&
(apiKey.services || [])
.find((service) => service.name === "storage")
?.actions.includes("read") &&
!!(apiKey.services || []).find((service) => service.name === "rpc"),
);
const chainId = contract.chain.id;
const { idToChain } = useAllChainsData();
const chain: StoredChain = (chainId ? idToChain.get(chainId) : undefined) || {
name: "Unknown Chain",
chainId: chainId || -1,
rpc: [],
chain: "unknown",
nativeCurrency: {
name: "Ethereum",
symbol: "ETH",
decimals: 18,
},
shortName: "unknown",
slug: "unknown",
testnet: false,
stackType: "",
};
const { register, watch } = useForm<{
rpcUrl: string;
relayUrl: string;
tokenId: string;
listingId: string;
listingType: string;
directListingId: string;
englishAuctionId: string;
theme: string;
primaryColor: string;
secondaryColor: string;
biconomyApiKey: string;
biconomyApiId: string;
gasless: string;
}>({
defaultValues: {
rpcUrl: chain?.rpc[0],
tokenId: "0",
listingId: "0",
directListingId: "0",
englishAuctionId: "0",
theme: theme === "light" ? "light" : "dark",
primaryColor: "purple",
secondaryColor: "orange",
},
reValidateMode: "onChange",
});
const configuredChainWithNewRpc = configureChain(chain, {
rpc: watch("rpcUrl"),
});
const minimizedChain = minimizeChain(configuredChainWithNewRpc);
const iframeSrc = buildIframeSrc(contract, ercOrMarketplace, {
chain: JSON.stringify(minimizedChain),
clientId: validApiKey?.key || "",
...watch(),
});
const embedCode = useMemo(
() =>
`<iframe
src="${iframeSrc}"
width="100%"
height="750px"
style="max-width:100%;"
frameborder="0"
></iframe>`,
[iframeSrc],
);
const [showCreateAPIKeyModal, setShowCreateAPIKeyModal] = useState(false);
const activeAccount = useActiveAccount();
const pathname = usePathname();
return (
<div>
<LazyCreateAPIKeyDialog
prefill={{
name: "Embed API Key",
domains: "embed.ipfscdn.io",
}}
open={showCreateAPIKeyModal}
onOpenChange={setShowCreateAPIKeyModal}
onCreateAndComplete={() => {
trackEvent({
category: "api-keys",
action: "create",
label: "success",
fromEmbed: true,
});
apiKeys.refetch();
}}
enableNebulaServiceByDefault={false}
teamSlug={undefined}
/>
<Alert variant="warning">
<AlertCircleIcon className="size-5" />
<AlertTitle>Deprecated</AlertTitle>
<AlertDescription className="leading-relaxed">
<span>
thirdweb NFT Embeds are deprecated and not actively maintained
</span>{" "}
<br />
<span>
Use the{" "}
<Link
href="https://github.com/thirdweb-example/nft-minting-template"
target="_blank"
className="underline underline-offset-2 hover:text-foreground"
>
{" "}
NFT Minting Template
</Link>{" "}
or build a custom NFT minting app using{" "}
<Link
href="https://portal.thirdweb.com/react/v5"
className="underline underline-offset-2 hover:text-foreground"
>
thirdweb React SDK components
</Link>{" "}
instead
</span>
</AlertDescription>
</Alert>
<div className="h-8" />
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div>
<h2 className="mb-3 font-semibold text-xl tracking-tight">
Configuration
</h2>
<Card className="flex w-full flex-col gap-5 bg-card">
{ercOrMarketplace === "marketplace" ? (
<FormControl>
<FormLabel>Listing ID</FormLabel>
<Input type="number" {...register("listingId")} />
<FormHelperText>
The listing ID the embed should display
</FormHelperText>
</FormControl>
) : null}
{ercOrMarketplace === "marketplace-v3" ? (
<FormControl>
<FormLabel>Listing type</FormLabel>
<Select {...register("listingType")}>
<option value="direct-listing">Direct Listing</option>
<option value="english-auction">English Auction</option>
</Select>
<FormHelperText>
The type of listing the embed should display
</FormHelperText>
</FormControl>
) : null}
{ercOrMarketplace === "marketplace-v3" &&
watch("listingType") === "direct-listing" ? (
<FormControl>
<FormLabel>Direct Listing ID</FormLabel>
<Input type="number" {...register("directListingId")} />
<FormHelperText>
The direct listing ID the embed should display
</FormHelperText>
</FormControl>
) : null}
{ercOrMarketplace === "marketplace-v3" &&
watch("listingType") === "english-auction" ? (
<FormControl>
<FormLabel>English Auction ID</FormLabel>
<Input type="number" {...register("englishAuctionId")} />
<FormHelperText>
The english auction ID the embed should display
</FormHelperText>
</FormControl>
) : null}
{ercOrMarketplace === "erc1155" ? (
<FormControl>
<FormLabel>Token ID</FormLabel>
<Input type="number" {...register("tokenId")} />
<FormHelperText>
The token ID the embed should display
</FormHelperText>
</FormControl>
) : null}
<FormControl>
<FormLabel>Client ID</FormLabel>
{!activeAccount ? (
<Button asChild className="w-full">
<Link
href={`/login${
pathname ? `?next=${encodeURIComponent(pathname)}` : ""
}`}
>
Sign in to create a client ID
</Link>
</Button>
) : validApiKey ? (
<Input
readOnly
disabled
value={`${validApiKey?.name} - ${validApiKey?.key}`}
/>
) : (
<Button
className="w-full"
onClick={() => {
trackEvent({
category: "api-keys",
action: "create",
label: "attempt",
fromEmbed: true,
});
setShowCreateAPIKeyModal(true);
}}
disabled={createKeyMutation.isPending}
>
Create Client ID
</Button>
)}
<FormHelperText>
You need a client ID to use embeds.{" "}
<Link
href="https://portal.thirdweb.com/account/api-keys"
className="text-link-foreground hover:text-foreground"
target="_blank"
>
Learn more
</Link>
.
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>RPC Url</FormLabel>
<Input type="url" {...register("rpcUrl")} />
<FormHelperText>
RPC the embed should use to connect to the blockchain.
</FormHelperText>
</FormControl>
{ercOrMarketplace === "marketplace" ||
ercOrMarketplace === "marketplace-v3" ? null : (
<FormControl gap={4}>
<Heading size="title.sm" mb={2}>
Gasless
</Heading>
<Select {...register("gasless")} mb={4}>
<option value="false">Disabled</option>
<option value="openZeppelin">OpenZeppelin Relayer</option>
<option value="biconomy">Biconomy Relayer</option>
</Select>
{watch("gasless") === "openZeppelin" && (
<FormControl>
<FormLabel>OpenZeppelin Relayer URL</FormLabel>
<Input type="url" {...register("relayUrl")} />
</FormControl>
)}
{watch("gasless") === "biconomy" && (
<Flex gap={4} flexDir="column">
<FormControl>
<FormLabel>Biconomy API key</FormLabel>
<Input type="url" {...register("biconomyApiKey")} />
</FormControl>
<FormControl>
<FormLabel>Biconomy API ID</FormLabel>
<Input type="url" {...register("biconomyApiId")} />
</FormControl>
</Flex>
)}
<FormHelperText>
A relayer can be used to make the transaction gasless for the
end user.{" "}
<Link
target="_blank"
className="text-link-foreground hover:text-foreground"
href="https://blog.thirdweb.com/guides/setup-gasless-transactions"
>
Learn more
</Link>
</FormHelperText>
</FormControl>
)}
<FormControl>
<FormLabel>Theme</FormLabel>
<Select {...register("theme")}>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">User system</option>
</Select>
<FormHelperText>
Selecting system will make it so the embed would change
depending on the user system&apos;s preferences.
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>Primary Color</FormLabel>
<Select {...register("primaryColor")}>
{ercOrMarketplace === "erc721" && (
<option value="default">Default</option>
)}
{colorOptions.map((color) => (
<option key={color} value={color}>
{color[0]?.toUpperCase() + color.substring(1)}
</option>
))}
</Select>
<FormHelperText>
Used for the main actions button backgrounds.
</FormHelperText>
</FormControl>
{ercOrMarketplace === "marketplace" ||
ercOrMarketplace === "marketplace-v3" ? (
<FormControl>
<FormLabel>Secondary Color</FormLabel>
<Select {...register("secondaryColor")}>
{colorOptions.map((color) => (
<option key={color} value={color}>
{color[0]?.toUpperCase() + color.substring(1)}
</option>
))}
</Select>
<FormHelperText>
Use for secondary actions (like when the user is connected to
the wrong network)
</FormHelperText>
</FormControl>
) : null}
</Card>
</div>
<div>
<h2 className="mb-3 font-semibold text-xl tracking-tight">
Embed Code
</h2>
<CodeClient
code={embedCode}
lang="html"
onCopy={() => {
trackEvent({
category: "embed",
action: "click",
label: "copy-code",
address: contract.address,
chainId,
});
}}
/>
</div>
</div>
<div className="h-8" />
<h2 className="mb-3 font-semibold text-xl tracking-tight">Preview</h2>
<div className="overflow-hidden rounded-lg border border-border">
{!validApiKey ? (
<Text>You need to create a client ID to use embeds</Text>
) : iframeSrc ? (
<iframe
title="thirdweb embed"
src={iframeSrc}
width="100%"
height="750px"
frameBorder="0"
/>
) : null}
</div>
</div>
);
};
@@ -1,42 +0,0 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getRawAccount } from "../../../../../account/settings/getAccount";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { EmbedSetupClient } from "./EmbedSetup.client";
import { EmbedSetup } from "./embed-setup";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const account = await getRawAccount();
const { contract } = info;
if (contract.chain.id === localhost.id) {
return <EmbedSetupClient contract={contract} twAccount={account} />;
}
const { embedType } = await getContractPageMetadata(contract);
if (embedType === null) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
}
return (
<EmbedSetup
contract={contract}
ercOrMarketplace={embedType}
twAccount={account}
/>
);
}
@@ -26,6 +26,7 @@ import { ToolTipLabel } from "@/components/ui/tooltip";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { TransactionButton } from "components/buttons/TransactionButton";
import { addDays, fromUnixTime } from "date-fns";
import { useAllChainsData } from "hooks/chains/allChains";
@@ -119,22 +120,39 @@ function ClaimableModule(props: ModuleInstanceProps) {
claimConditionQuery.data?.startTimestamp === 0 &&
claimConditionQuery.data?.endTimestamp === 0;
const currencyContract = getContract({
address: claimConditionQuery.data?.currency || "",
chain: props.contract.chain,
client: props.contract.client,
});
const claimConditionCurrency = claimConditionQuery.data?.currency;
const shouldFetchTokenDecimals =
claimConditionQuery.data &&
claimConditionQuery.data?.currency !== ZERO_ADDRESS;
const shouldFetchCurrencyDecimals =
!!claimConditionCurrency && claimConditionCurrency !== ZERO_ADDRESS;
const currencyDecimalsQuery = useReadContract(decimals, {
contract: currencyContract,
queryOptions: {
enabled: shouldFetchTokenDecimals,
const currencyDecimalsQuery = useQuery({
queryKey: [
"currency-decimals",
{
contractAddress: props.contract.address,
chainId: props.contract.chain.id,
claimConditionCurrency,
},
],
queryFn: async () => {
if (!claimConditionCurrency) {
throw new Error();
}
const currencyContract = getContract({
address: claimConditionCurrency,
chain: props.contract.chain,
client: props.contract.client,
});
const decimalsVal = await decimals({
contract: currencyContract,
});
return decimalsVal;
},
enabled: shouldFetchCurrencyDecimals,
});
const tokenDecimalsQuery = useReadContract(decimals, {
contract: contract,
queryOptions: {
@@ -265,7 +283,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
// claim conditions data is present
claimConditionQuery.data &&
// token decimals is fetched if it should be fetched
(shouldFetchTokenDecimals ? currencyDecimalsQuery.isFetched : true)
(shouldFetchCurrencyDecimals ? currencyDecimalsQuery.isFetched : true)
? {
claimCondition: claimConditionQuery.data,
currencyDecimals: currencyDecimalsQuery.data,
@@ -276,7 +294,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
tokenId,
isLoading:
claimConditionQuery.isLoading ||
(!!shouldFetchTokenDecimals && currencyDecimalsQuery.isLoading),
(!!shouldFetchCurrencyDecimals && currencyDecimalsQuery.isLoading),
}}
isOwnerAccount={!!ownerAccount}
name={props.contractInfo.name}
@@ -565,6 +583,7 @@ function ClaimConditionSection(props: {
onClick={() => setAddClaimConditionButtonClicked(true)}
variant="outline"
className="w-full"
disabled={!props.isOwnerAccount}
>
Add Claim Condition
</Button>
@@ -30,19 +30,27 @@ export function SecondaryNavLinks() {
<ResourcesDropdownButton />
<Link
href="https://portal.thirdweb.com"
className="text-muted-foreground text-sm hover:text-foreground"
target="_blank"
href="https://thirdweb.com/support"
>
Docs
</Link>
<Link
target="_blank"
href="/support"
className="text-muted-foreground text-sm hover:text-foreground"
>
Support
</Link>
<Link
href="https://portal.thirdweb.com/"
className="text-muted-foreground text-sm hover:text-foreground"
target="_blank"
href="https://feedback.thirdweb.com"
className="text-muted-foreground text-sm hover:text-foreground"
>
Docs
Feedback
</Link>
</div>
);
@@ -159,6 +159,34 @@ export function MobileBurgerMenuButton(
</div>
<div className="mt-auto">
<div className="flex flex-col gap-5">
<Link
target="_blank"
href="https://portal.thirdweb.com"
className="text-muted-foreground hover:text-foreground "
>
Docs
</Link>
<Link
target="_blank"
href="/support"
className="text-muted-foreground hover:text-foreground "
>
Support
</Link>
<Link
target="_blank"
href="https://feedback.thirdweb.com"
className="text-muted-foreground hover:text-foreground "
>
Feedback
</Link>
</div>
<div className="h-6" />
<Separator />
<div className="h-6" />
@@ -25,8 +25,8 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
accentText: "hsl(var(--link-foreground))",
accentButtonBg: "hsl(var(--primary))",
accentButtonText: "hsl(var(--primary-foreground))",
primaryButtonBg: "hsl(var(--background))",
primaryButtonText: "hsl(var(--foreground))",
primaryButtonBg: "hsl(var(--inverted))",
primaryButtonText: "hsl(var(--inverted-foreground))",
secondaryButtonText: "hsl(var(--secondary-foreground))",
tooltipBg: "hsl(var(--popover))",
tooltipText: "hsl(var(--popover-foreground))",
-2
View File
@@ -1,5 +1,4 @@
import "@/styles/globals.css";
import { Toaster } from "@/components/ui/sonner";
import { DashboardRouterTopProgressBar } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
import type { Metadata } from "next";
@@ -82,7 +81,6 @@ export default function RootLayout({
<EnsureValidConnectedWalletLoginServer />
</Suspense>
</AppRouterProviders>
<Toaster richColors />
<DashboardRouterTopProgressBar />
<NextTopLoader
color="hsl(var(--primary))"
+12 -4
View File
@@ -55,7 +55,15 @@ export function LoginAndOnboardingPage(props: {
<ThirdwebMiniLogo className="size-7 md:size-8" />
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<Link
href="https://portal.thirdweb.com/"
className="px-2 text-muted-foreground text-sm hover:text-foreground"
target="_blank"
>
Docs
</Link>
<Link
href="/support"
target="_blank"
@@ -65,11 +73,11 @@ export function LoginAndOnboardingPage(props: {
</Link>
<Link
href="https://portal.thirdweb.com/"
className="px-2 text-muted-foreground text-sm hover:text-foreground"
target="_blank"
href="https://feedback.thirdweb.com"
className="px-2 text-muted-foreground text-sm hover:text-foreground"
>
Docs
Feedback
</Link>
<ColorModeToggle />
+8 -1
View File
@@ -1,8 +1,9 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider } from "next-themes";
import { ThemeProvider, useTheme } from "next-themes";
import { useEffect, useMemo } from "react";
import { Toaster } from "sonner";
import {
ThirdwebProvider,
useActiveAccount,
@@ -31,6 +32,7 @@ export function AppRouterProviders(props: { children: React.ReactNode }) {
enableSystem={false}
defaultTheme="dark"
>
<ToasterSetup />
<SanctionedAddressesChecker>
{props.children}
</SanctionedAddressesChecker>
@@ -40,6 +42,11 @@ export function AppRouterProviders(props: { children: React.ReactNode }) {
);
}
function ToasterSetup() {
const { theme } = useTheme();
return <Toaster richColors theme={theme === "light" ? "light" : "dark"} />;
}
function SyncChainDefinitionsToConnectionManager() {
const { allChainsV5 } = useAllChainsData();
const connectionManager = useConnectionManager();
@@ -6,6 +6,8 @@ import { Button } from "@/components/ui/button";
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
import { FormItem } from "@/components/ui/form";
import { Skeleton } from "@/components/ui/skeleton";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { engineKeys } from "@3rdweb-sdk/react";
import {
type BackendWallet,
useEngineBackendWalletBalance,
@@ -30,7 +32,7 @@ import {
type UseDisclosureReturn,
useDisclosure,
} from "@chakra-ui/react";
import { useQuery } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { type ColumnDef, createColumnHelper } from "@tanstack/react-table";
import { ChainIcon } from "components/icons/ChainIcon";
import { TWTable } from "components/shared/TWTable";
@@ -40,18 +42,21 @@ import { EngineBackendWalletOptions } from "lib/engine";
import { useV5DashboardChain } from "lib/v5-adapter";
import {
DownloadIcon,
ExternalLinkIcon,
PencilIcon,
RefreshCcwIcon,
TrashIcon,
TriangleAlertIcon,
UploadIcon,
} from "lucide-react";
import Link from "next/link";
import QRCode from "qrcode";
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { getAddress } from "thirdweb";
import { shortenAddress } from "thirdweb/utils";
import { FormHelperText, FormLabel, LinkButton, Text } from "tw-components";
import { FormHelperText, FormLabel, Text } from "tw-components";
import { prettyPrintCurrency } from "./utils";
interface BackendWalletsTableProps {
@@ -83,16 +88,17 @@ const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
authToken,
chainId,
}) => {
const { data: backendWalletBalance } = useEngineBackendWalletBalance({
instanceUrl: instanceUrl,
address,
authToken,
chainId,
});
const { data: backendWalletBalance, isFetching } =
useEngineBackendWalletBalance({
instanceUrl: instanceUrl,
address,
authToken,
chainId,
});
const chain = useV5DashboardChain(chainId);
if (!backendWalletBalance) {
return <Skeleton className="h-5 w-32 rounded-lg" />;
if (!backendWalletBalance || isFetching) {
return <Skeleton className="h-6 w-36 rounded-lg" />;
}
const balanceDisplay = prettyPrintCurrency({
@@ -100,9 +106,7 @@ const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
symbol: backendWalletBalance.symbol || chain.nativeCurrency?.symbol,
});
const balanceComponent = (
<div className="text-muted-foreground">{balanceDisplay}</div>
);
const balanceComponent = <span>{balanceDisplay}</span>;
const explorer = chain.blockExplorers?.[0];
if (!explorer) {
@@ -110,14 +114,14 @@ const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
}
return (
<LinkButton
variant="ghost"
isExternal
size="xs"
<Link
target="_blank"
href={`${explorer.url}/address/${address}`}
className="inline-flex items-center gap-2.5 rounded-lg px-2 py-1 text-muted-foreground hover:bg-muted hover:text-foreground"
>
{balanceComponent}
</LinkButton>
<ExternalLinkIcon className="size-4" />
</Link>
);
};
@@ -133,6 +137,7 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
const receiveDisclosure = useDisclosure();
const sendDisclosure = useDisclosure();
const deleteDisclosure = useDisclosure();
const queryClient = useQueryClient();
const columns = useMemo(() => {
return [
@@ -160,7 +165,31 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
},
}),
columnHelper.accessor("address", {
header: "Balance",
header: () => (
<div className="flex w-[180px] items-center gap-1.5">
Balance
<ToolTipLabel
label="Refresh Balance"
contentClassName="capitalize font-normal tracking-normal leading-normal"
>
<Button
className="z-20 h-auto p-1.5 [&[data-pending='true']_svg]:animate-spin"
variant="ghost"
size="sm"
onClick={async (e) => {
const buttonEl = e.currentTarget;
buttonEl.setAttribute("data-pending", "true");
await queryClient.invalidateQueries({
queryKey: engineKeys.backendWalletBalanceAll(),
});
buttonEl.setAttribute("data-pending", "false");
}}
>
<RefreshCcwIcon className="size-4" />
</Button>
</ToolTipLabel>
</div>
),
cell: (cell) => {
const address = cell.getValue();
return (
@@ -175,7 +204,7 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
id: "balance",
}),
];
}, [instanceUrl, authToken, chainId]);
}, [instanceUrl, authToken, chainId, queryClient]);
const [selectedBackendWallet, setSelectedBackendWallet] =
useState<BackendWallet>();
@@ -417,14 +417,9 @@ function AuthEndpointFields(props: {
canEditAdvancedFeatures: boolean;
}) {
const { form, canEditAdvancedFeatures } = props;
const customHeaderFields = useFieldArray({
control: form.control,
name: "customAuthEndpoint.customHeaders",
});
const expandCustomAuthEndpointField =
form.watch("customAuthEndpoint")?.authEndpoint !== undefined &&
canEditAdvancedFeatures;
form.watch("customAuthEndpoint") !== undefined && canEditAdvancedFeatures;
return (
<div>
@@ -464,88 +459,107 @@ function AuthEndpointFields(props: {
}}
/>
</SwitchContainer>
<AdvancedConfigurationContainer
show={expandCustomAuthEndpointField}
className="grid grid-cols-1 gap-6 lg:grid-cols-2"
>
<FormField
control={form.control}
name="customAuthEndpoint.authEndpoint"
render={({ field }) => (
<FormItem>
<FormLabel>Authentication Endpoint</FormLabel>
<FormControl>
<Input
{...field}
placeholder="https://example.com/your-auth-verifier"
/>
</FormControl>
<FormDescription>
Enter the URL of your server where we will send the user payload
for verification
</FormDescription>
<FormMessage />
</FormItem>
)}
{/* useFieldArray used on this component - it creates empty customAuthEndpoint.customHeaders array on mount */}
{/* So only mount if expandCustomAuthEndpointField is true */}
{expandCustomAuthEndpointField && (
<AuthEndpointFieldsContent
form={form}
canEditAdvancedFeatures={canEditAdvancedFeatures}
/>
)}
</div>
);
}
<div>
<Label className="mb-3 inline-block">Custom Headers</Label>
<div className="flex flex-col gap-4">
{customHeaderFields.fields.map((field, customHeaderIdx) => {
return (
<div className="flex gap-4" key={field.id}>
<Input
placeholder="Name"
type="text"
{...form.register(
`customAuthEndpoint.customHeaders.${customHeaderIdx}.key`,
)}
/>
<Input
placeholder="Value"
type="text"
{...form.register(
`customAuthEndpoint.customHeaders.${customHeaderIdx}.value`,
)}
/>
<Button
variant="outline"
aria-label="Remove header"
onClick={() => {
customHeaderFields.remove(customHeaderIdx);
}}
className="!w-auto px-3"
>
<Trash2Icon className="size-4 shrink-0 text-destructive-text" />
</Button>
</div>
);
})}
function AuthEndpointFieldsContent(props: {
form: UseFormReturn<ApiKeyEmbeddedWalletsValidationSchema>;
canEditAdvancedFeatures: boolean;
}) {
const { form } = props;
<Button
variant="outline"
className="w-full gap-2 bg-background"
onClick={() => {
customHeaderFields.append({
key: "",
value: "",
});
}}
>
<PlusIcon className="size-4" />
Add header
</Button>
</div>
const customHeaderFields = useFieldArray({
control: form.control,
name: "customAuthEndpoint.customHeaders",
});
<p className="mt-3 text-muted-foreground text-sm">
Set custom headers to be sent along the request with the payload to
the authentication endpoint above. This can be used to verify the
incoming requests
</p>
return (
<div className="mt-6 grid grid-cols-1 gap-6 lg:grid-cols-2">
<FormField
control={form.control}
name="customAuthEndpoint.authEndpoint"
render={({ field }) => (
<FormItem>
<FormLabel>Authentication Endpoint</FormLabel>
<FormControl>
<Input
{...field}
placeholder="https://example.com/your-auth-verifier"
/>
</FormControl>
<FormDescription>
Enter the URL of your server where we will send the user payload
for verification
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div>
<Label className="mb-3 inline-block">Custom Headers</Label>
<div className="flex flex-col gap-4">
{customHeaderFields.fields.map((field, customHeaderIdx) => {
return (
<div className="flex gap-4" key={field.id}>
<Input
placeholder="Name"
type="text"
{...form.register(
`customAuthEndpoint.customHeaders.${customHeaderIdx}.key`,
)}
/>
<Input
placeholder="Value"
type="text"
{...form.register(
`customAuthEndpoint.customHeaders.${customHeaderIdx}.value`,
)}
/>
<Button
variant="outline"
aria-label="Remove header"
onClick={() => {
customHeaderFields.remove(customHeaderIdx);
}}
className="!w-auto px-3"
>
<Trash2Icon className="size-4 shrink-0 text-destructive-text" />
</Button>
</div>
);
})}
<Button
variant="outline"
className="w-full gap-2 bg-background"
onClick={() => {
customHeaderFields.append({
key: "",
value: "",
});
}}
>
<PlusIcon className="size-4" />
Add header
</Button>
</div>
</AdvancedConfigurationContainer>
<p className="mt-3 text-muted-foreground text-sm">
Set custom headers to be sent along the request with the payload to
the authentication endpoint above. This can be used to verify the
incoming requests
</p>
</div>
</div>
);
}
+41
View File
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+10
View File
@@ -0,0 +1,10 @@
# thirdweb-login
## 0.1.1
### Patch Changes
- [#6137](https://github.com/thirdweb-dev/js/pull/6137) [`a6b7e8d`](https://github.com/thirdweb-dev/js/commit/a6b7e8d81868b5f32f1c8b7ff093bb1f06c734ca) Thanks [@jnsdls](https://github.com/jnsdls)! - updated dependencies
- Updated dependencies [[`a6b7e8d`](https://github.com/thirdweb-dev/js/commit/a6b7e8d81868b5f32f1c8b7ff093bb1f06c734ca), [`9d5828e`](https://github.com/thirdweb-dev/js/commit/9d5828eeab201960a720744ca3a59c85a0d8e548), [`b693b78`](https://github.com/thirdweb-dev/js/commit/b693b78645e2b214a5f8be0eec6d335d569ceb8c), [`08cc489`](https://github.com/thirdweb-dev/js/commit/08cc48910df351d068c1ce224d4102f40cb1dce1)]:
- thirdweb@5.87.3
+36
View File
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+16
View File
@@ -0,0 +1,16 @@
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"next": true,
"ignore": ["public/**"],
"project": ["src/**"],
"ignoreBinaries": ["only-allow", "biome"],
"ignoreDependencies": ["thirdweb", "eslint-config-next"]
}
+28
View File
@@ -0,0 +1,28 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
async headers() {
return [
{
source: "/api/:path*",
headers: [
{
key: "Access-Control-Allow-Origin",
value: "*", // Set your origin
},
{
key: "Access-Control-Allow-Methods",
value: "GET, POST, PUT, DELETE, OPTIONS",
},
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization",
},
],
},
];
},
};
export default nextConfig;
+36
View File
@@ -0,0 +1,36 @@
{
"name": "thirdweb-login",
"version": "0.1.1",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"format": "biome format ./src --write",
"lint": "biome check ./src && knip && eslint ./src",
"fix": "biome check ./src --fix && eslint ./src --fix",
"typecheck": "tsc --noEmit",
"knip": "knip"
},
"dependencies": {
"next": "15.1.6",
"react": "19.0.0",
"react-dom": "19.0.0",
"server-only": "^0.0.1",
"thirdweb": "workspace:*"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/node": "22.13.0",
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"eslint": "^9",
"eslint-config-next": "15.1.6",
"knip": "5.43.6",
"postcss": "8.5.1",
"postcss-load-config": "^6.0.1",
"tailwindcss": "3.4.17",
"typescript": "5.7.3"
}
}
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
+19
View File
@@ -0,0 +1,19 @@
<svg width="903" height="549" viewBox="0 0 903 549" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M475.061 0.925537C480.587 2.07406 485.804 4.39614 490.358 7.73528C495.457 11.7723 499.362 17.1238 501.656 23.2159C516.137 59.447 530.618 95.6561 545.099 131.843C561.124 172.089 577.229 212.341 593.414 252.601C601.849 273.709 610.241 294.839 618.807 315.903C620.629 320.212 621.568 324.844 621.568 329.523C621.568 334.202 620.629 338.834 618.807 343.142L545.208 526.743C540.335 538.895 531.506 546.449 518.526 548.42C502.377 550.851 487.933 542.88 482.098 528.713C468.287 494.752 454.782 460.682 441.168 426.633L371.94 253.433C351.442 202.036 330.915 150.652 310.359 99.2836C303.542 82.2264 296.942 65.0816 289.818 48.1558C287.856 43.5536 286.929 38.5755 287.1 33.5741C287.272 28.5726 288.539 23.6702 290.812 19.2139C293.084 14.7576 296.307 10.8566 300.251 7.78696C304.196 4.71736 308.766 2.55433 313.637 1.45105C314.227 1.31967 314.796 1.10071 315.364 0.925537H475.061Z" fill="url(#paint0_linear_4033_153464)"/>
<path d="M188.239 0.925964C190.052 1.42958 191.866 1.8675 193.636 2.4806C198.245 3.9207 202.472 6.37753 206.008 9.67041C209.544 12.9633 212.298 17.0085 214.068 21.5085C229.875 60.7466 245.609 100.014 261.27 139.31C277.12 178.928 292.97 218.553 308.821 258.185C316.571 277.615 324.358 297.037 332.181 316.451C333.932 320.609 334.834 325.076 334.834 329.589C334.834 334.102 333.932 338.569 332.181 342.727C323.44 364.536 314.699 386.337 305.958 408.131C289.933 448.026 273.908 487.921 257.882 527.816C255.367 534.028 251.065 539.351 245.525 543.109C239.985 546.867 233.455 548.891 226.765 548.924C219.677 549.049 212.723 546.977 206.856 542.991C200.988 539.005 196.493 533.299 193.986 526.656C179.17 489.06 164.048 451.573 149.057 414.043C128.968 363.229 108.776 312.452 88.4822 261.711C67.8243 210.065 47.1738 158.411 26.5305 106.751C18.7073 87.1973 10.9497 67.6001 3.01724 48.0686C-0.448593 39.9305 -0.616668 30.7602 2.54861 22.5001C5.71389 14.2399 11.9639 7.53886 19.9747 3.81627C22.881 2.63941 25.8679 1.67348 28.9124 0.925964L188.239 0.925964Z" fill="url(#paint1_linear_4033_153464)"/>
<path d="M902.82 334.858C898.45 349.638 891.894 363.586 886.322 377.841C870.195 418.393 853.98 458.923 837.678 499.431C833.941 508.715 830.139 517.977 826.511 527.305C823.958 533.679 819.554 539.138 813.87 542.976C808.185 546.815 801.482 548.855 794.628 548.834C787.773 548.813 781.083 546.731 775.422 542.857C769.761 538.984 765.391 533.497 762.877 527.108L692.119 350.142L630.932 197.284C610.959 147.375 591 97.4735 571.056 47.5791C569.16 43.017 568.286 38.0935 568.495 33.156C568.704 28.2184 569.992 23.387 572.267 19.0024C574.543 14.6177 577.751 10.7867 581.664 7.77934C585.578 4.77199 590.103 2.66159 594.919 1.59695C595.575 1.44368 596.208 1.22472 596.864 1.02765H756.562C757.064 1.18092 757.545 1.37799 758.048 1.48747C763.649 2.65867 768.864 5.22722 773.211 8.95523C777.558 12.6832 780.896 17.4502 782.916 22.8144C792.21 46.1705 801.52 69.4608 810.843 92.6855C835.245 153.674 859.625 214.662 883.983 275.651C890.386 291.723 897.51 307.51 902.755 324.02L902.82 334.858Z" fill="url(#paint2_linear_4033_153464)"/>
<defs>
<linearGradient id="paint0_linear_4033_153464" x1="262.328" y1="45.4188" x2="647.023" y2="429.344" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF00A8"/>
<stop offset="1" stop-color="#6200C6"/>
</linearGradient>
<linearGradient id="paint1_linear_4033_153464" x1="-24.342" y1="45.2878" x2="360.265" y2="429.125" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF00A8"/>
<stop offset="1" stop-color="#6200C6"/>
</linearGradient>
<linearGradient id="paint2_linear_4033_153464" x1="543.762" y1="45.28" x2="928.392" y2="429.14" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF00A8"/>
<stop offset="1" stop-color="#6200C6"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+193
View File
@@ -0,0 +1,193 @@
// biome-ignore lint/complexity/useArrowFunction: This is a self-executing function, we do not want to use an arrow function here.
(function () {
const globalSetup = getSetup();
const JWT_KEY = "tw.login:jwt";
const CODE_KEY = "tw.login:code";
// check if redirected first, this sets up the logged in state if it was from redirect
const result = parseURL(new URL(window.location));
if (
result &&
result.length === 2 &&
result[1] === localStorage.getItem(CODE_KEY)
) {
// reset the URL
window.location.hash = "";
window.location.search = "";
// write the jwt to local storage
localStorage.setItem(JWT_KEY, result[0]);
}
// always reset the code
localStorage.removeItem(CODE_KEY);
const jwt = localStorage.getItem(JWT_KEY);
if (jwt) {
// handle logged in state
handleIsLoggedIn();
} else {
// handle not logged in state
handleNotLoggedIn();
}
function handleIsLoggedIn() {
window.thirdweb = {
isLoggedIn: true,
getUser: async () => {
const res = await fetch(`${globalSetup.baseUrl}/api/user`, {
headers: {
Authorization: `Bearer ${localStorage.getItem(JWT_KEY)}`,
},
});
return res.json();
},
logout: () => {
window.localStorage.removeItem(JWT_KEY);
window.location.reload();
},
};
renderFloatingBubble(true);
}
function handleNotLoggedIn() {
window.thirdweb = { login: onLogin, isLoggedIn: false };
renderFloatingBubble(false);
}
function onLogin() {
const code = window.crypto.getRandomValues(new Uint8Array(16)).join("");
localStorage.setItem(CODE_KEY, code);
// redirect to the login page
const redirect = new URL(globalSetup.baseUrl);
redirect.searchParams.set("code", code);
redirect.searchParams.set("clientId", globalSetup.clientId);
redirect.searchParams.set(
"redirect",
window.location.origin + window.location.pathname,
);
window.location.href = redirect.href;
}
// utils
function getSetup() {
const el = document.currentScript;
if (!el) {
throw new Error("Could not find script element");
}
const baseUrl = new URL(el.src).origin;
const dataset = el.dataset;
const clientId = dataset.clientId;
const theme = dataset.theme || "dark";
if (!clientId) {
throw new Error("Missing client-id");
}
return { clientId, baseUrl, theme };
}
/**
* @param {URL} url
* @returns null | [string, string]
*/
function parseURL(url) {
try {
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
const code = url.searchParams.get("code");
if (!hash || !code) {
return null;
}
return [hash, code];
} catch {
// if this fails, invalid data -> return null
return null;
}
}
async function renderFloatingBubble(loggedIn) {
const el = document.createElement("div");
el.id = "tw-floating-bubble";
el.style.position = "fixed";
el.style.bottom = "24px";
el.style.right = "24px";
el.style.zIndex = "1000";
el.style.width = "138px";
el.style.height = "40px";
el.style.backgroundColor =
globalSetup.theme === "dark" ? "#131418" : "#ffffff";
el.style.color = globalSetup.theme === "dark" ? "white" : "black";
el.style.borderRadius = "8px";
el.style.placeItems = "center";
el.style.fontSize = loggedIn ? "12px" : "12px";
el.style.cursor = "pointer";
el.style.overflow = "hidden";
el.style.boxShadow = "1px 1px 10px rgba(0, 0, 0, 0.5)";
el.style.display = "flex";
el.style.alignItems = "center";
el.style.justifyContent = "space-around";
el.style.fontFamily = "sans-serif";
el.style.gap = "8px";
el.style.padding = "0px 8px";
el.onclick = () => {
if (loggedIn) {
window.thirdweb.logout();
} else {
window.thirdweb.login();
}
};
el.innerHTML = loggedIn ? await renderBlobbie() : renderThirdwebLogo();
document.body.appendChild(el);
}
function renderThirdwebLogo() {
const el = document.createElement("img");
el.src = `${globalSetup.baseUrl}/logo.svg`;
el.style.height = "16px";
el.style.objectFit = "contain";
el.style.flexShrink = "0";
el.style.marginLeft = "-4px";
return `${el.outerHTML} <span>Login</span><span></span>`;
}
async function renderBlobbie() {
const address = (await window.thirdweb.getUser()).address;
function hexToNumber(hex) {
if (typeof hex !== "string")
throw new Error(`hex string expected, got ${typeof hex}`);
return hex === "" ? _0n : BigInt(`0x${hex}`);
}
const COLOR_OPTIONS = [
["#fca5a5", "#b91c1c"],
["#fdba74", "#c2410c"],
["#fcd34d", "#b45309"],
["#fde047", "#a16207"],
["#a3e635", "#4d7c0f"],
["#86efac", "#15803d"],
["#67e8f9", "#0e7490"],
["#7dd3fc", "#0369a1"],
["#93c5fd", "#1d4ed8"],
["#a5b4fc", "#4338ca"],
["#c4b5fd", "#6d28d9"],
["#d8b4fe", "#7e22ce"],
["#f0abfc", "#a21caf"],
["#f9a8d4", "#be185d"],
["#fda4af", "#be123c"],
];
const colors =
COLOR_OPTIONS[
Number(hexToNumber(address.slice(2, 4))) % COLOR_OPTIONS.length
];
const el = document.createElement("div");
el.style.backgroundImage = `radial-gradient(ellipse at left bottom, ${colors[0]}, ${colors[1]})`;
el.style.width = "24px";
el.style.height = "24px";
el.style.borderRadius = "50%";
el.style.flexShrink = "0";
return `${el.outerHTML}<span>${address.slice(0, 6)}...${address.slice(-4)}</span><span></span>`;
}
})();
+1
View File
@@ -0,0 +1 @@
!function(){let e=function e(){let t=document.currentScript;if(!t)throw Error("Could not find script element");let a=new URL(t.src).origin,n=t.dataset,l=n.clientId,s=n.theme||"dark";if(!l)throw Error("Missing client-id");return{clientId:l,baseUrl:a,theme:s}}(),t="tw.login:jwt",a="tw.login:code",n=function e(t){try{let a=t.hash.startsWith("#")?t.hash.slice(1):t.hash,n=t.searchParams.get("code");if(!a||!n)return null;return[a,n]}catch{return null}}(new URL(window.location));n&&2===n.length&&n[1]===localStorage.getItem(a)&&(window.location.hash="",window.location.search="",localStorage.setItem(t,n[0])),localStorage.removeItem(a);let l=localStorage.getItem(t);function s(){let t=window.crypto.getRandomValues(new Uint8Array(16)).join("");localStorage.setItem(a,t);let n=new URL(e.baseUrl);n.searchParams.set("code",t),n.searchParams.set("clientId",e.clientId),n.searchParams.set("redirect",window.location.origin+window.location.pathname),window.location.href=n.href}async function i(t){let a=document.createElement("div");a.id="tw-floating-bubble",a.style.position="fixed",a.style.bottom="24px",a.style.right="24px",a.style.zIndex="1000",a.style.width="138px",a.style.height="40px",a.style.backgroundColor="dark"===e.theme?"#131418":"#ffffff",a.style.color="dark"===e.theme?"white":"black",a.style.borderRadius="8px",a.style.placeItems="center",a.style.fontSize="12px",a.style.cursor="pointer",a.style.overflow="hidden",a.style.boxShadow="1px 1px 10px rgba(0, 0, 0, 0.5)",a.style.display="flex",a.style.alignItems="center",a.style.justifyContent="space-around",a.style.fontFamily="sans-serif",a.style.gap="8px",a.style.padding="0px 8px",a.onclick=()=>{t?window.thirdweb.logout():window.thirdweb.login()},a.innerHTML=t?await r():function t(){let a=document.createElement("img");return a.src=`${e.baseUrl}/logo.svg`,a.style.height="16px",a.style.objectFit="contain",a.style.flexShrink="0",a.style.marginLeft="-4px",`${a.outerHTML} <span>Login</span><span></span>`}(),document.body.appendChild(a)}async function r(){let e=(await window.thirdweb.getUser()).address,t=[["#fca5a5","#b91c1c"],["#fdba74","#c2410c"],["#fcd34d","#b45309"],["#fde047","#a16207"],["#a3e635","#4d7c0f"],["#86efac","#15803d"],["#67e8f9","#0e7490"],["#7dd3fc","#0369a1"],["#93c5fd","#1d4ed8"],["#a5b4fc","#4338ca"],["#c4b5fd","#6d28d9"],["#d8b4fe","#7e22ce"],["#f0abfc","#a21caf"],["#f9a8d4","#be185d"],["#fda4af","#be123c"],],a=t[Number(function e(t){if("string"!=typeof t)throw Error(`hex string expected, got ${typeof t}`);return""===t?_0n:BigInt(`0x${t}`)}(e.slice(2,4)))%t.length],n=document.createElement("div");return n.style.backgroundImage=`radial-gradient(ellipse at left bottom, ${a[0]}, ${a[1]})`,n.style.width="24px",n.style.height="24px",n.style.borderRadius="50%",n.style.flexShrink="0",`${n.outerHTML}<span>${e.slice(0,6)}...${e.slice(-4)}</span><span></span>`}l?(window.thirdweb={isLoggedIn:!0,async getUser(){let a=await fetch(`${e.baseUrl}/api/user`,{headers:{Authorization:`Bearer ${localStorage.getItem(t)}`}});return a.json()},logout(){window.localStorage.removeItem(t),window.location.reload()}},i(!0)):(window.thirdweb={login:s,isLoggedIn:!1},i(!1))}();
+33
View File
@@ -0,0 +1,33 @@
import { type NextRequest, NextResponse } from "next/server";
import { verifyJWT } from "../../authorization/jwt";
export const GET = async (req: NextRequest) => {
const jwt = req.headers.get("Authorization")?.split("Bearer ")[1];
if (!jwt) {
return NextResponse.json(
{
message: "No JWT provided",
},
{
status: 401,
},
);
}
try {
const verifiedPayload = await verifyJWT(jwt);
return NextResponse.json({
address: verifiedPayload.sub,
});
} catch (e) {
console.error("failed", e);
return NextResponse.json(
{
message: "Invalid JWT",
},
{
status: 401,
},
);
}
};
+58
View File
@@ -0,0 +1,58 @@
"server only";
import "server-only";
import { createThirdwebClient } from "thirdweb";
import { verifyEOASignature } from "thirdweb/auth";
import { decodeJWT, encodeJWT, stringify } from "thirdweb/utils";
import { privateKeyToAccount, randomPrivateKey } from "thirdweb/wallets";
const client = createThirdwebClient({
clientId: "e9ba48c289e0cc3d06a23bfd370cc111",
});
const privateKey = process.env.THIRDWEB_ADMIN_PRIVATE_KEY || randomPrivateKey();
if (!privateKey) {
throw new Error("Missing THIRDWEB_ADMIN_PRIVATE_KEY");
}
const serverAccount = privateKeyToAccount({
client,
privateKey: privateKey,
});
export async function signJWT(data: {
address: string;
sessionKeySignerAddress: string;
code: string;
}) {
"use server";
return await encodeJWT({
account: serverAccount,
payload: {
iss: serverAccount.address,
sub: data.address,
aud: data.sessionKeySignerAddress,
exp: new Date(Date.now() + 1000 * 60 * 60),
nbf: new Date(),
iat: new Date(),
jti: data.code,
},
});
}
export async function verifyJWT(jwt: string) {
const { payload, signature } = decodeJWT(jwt);
console.log("payload", payload);
console.log("signature", signature);
// verify the signature
const verified = await verifyEOASignature({
message: stringify(payload),
signature,
address: serverAccount.address,
});
if (!verified) {
throw new Error("Invalid JWT signature");
}
return payload;
}
@@ -0,0 +1,186 @@
"use client";
import { useSearchParams } from "next/navigation";
import { createThirdwebClient, getContract } from "thirdweb";
import { baseSepolia } from "thirdweb/chains";
import { addSessionKey, isActiveSigner } from "thirdweb/extensions/erc4337";
import {
AccountAddress,
AccountBlobbie,
AccountProvider,
ConnectEmbed,
TransactionButton,
useActiveAccount,
} from "thirdweb/react";
import { isContractDeployed, shortenAddress } from "thirdweb/utils";
const sessionKeySignerAddress = "0x6f700ba0258886411D2536399624EAa7158d1742";
export function LoginPageInner({
generateJWT,
}: {
generateJWT: (opts: {
address: string;
sessionKeySignerAddress: string;
code: string;
}) => Promise<string>;
}) {
const account = useActiveAccount();
const searchParams = useSearchParams();
const code = searchParams.get("code");
// const clientId = searchParams.get("clientId");
const redirect = searchParams.get("redirect");
const client = createThirdwebClient({
clientId: "e9ba48c289e0cc3d06a23bfd370cc111",
});
return (
<div className="grid h-full w-full place-items-center">
{account ? (
<div
className="flex flex-col gap-4 rounded-lg border p-12"
style={{ borderColor: "rgba(255, 255, 255, 0.1)" }}
>
<AccountProvider address={account.address} client={client}>
<div
className="flex flex-row items-center gap-2 rounded-md border p-4"
style={{ borderColor: "rgba(255, 255, 255, 0.1)" }}
>
<AccountBlobbie className="h-12 w-12 rounded-full" />
<div className="flex flex-col gap-0">
<AccountAddress formatFn={shortenAddress} />
</div>
</div>
</AccountProvider>
<hr className="opacity-10" />
<div className="flex flex-col gap-0">
<h1 className="font-bold text-2xl">Grant Permissions</h1>
<p className="opacity-70">
<strong>App.xyz</strong> is asking you to grant it the following
permissions:
</p>
</div>
<hr className="opacity-10" />
<ul>
<li>
<span className="mr-4">✅</span>Read your wallet address.
</li>
</ul>
<hr className="opacity-10" />
<ul>
<li>
<span className="mr-4">⚠️</span>Interact with <strong>all</strong>{" "}
contracts on <strong>Base Sepolia</strong>
</li>
<li>
<span className="mr-4">⚠️</span>Spend <strong>all</strong> your
funds on <strong>Base Sepolia</strong>
</li>
<li>
<span className="mr-4">📆</span>Granted until{" "}
<strong>02/02/2025</strong>
</li>
</ul>
<hr className="opacity-10" />
<div className="flex flex-row gap-4">
<button
type="button"
className="w-[50%] rounded-lg border p-2 hover:bg-gray-950"
style={{ borderColor: "rgba(255, 255, 255, 0.1)" }}
onClick={() => {
window.location.href = redirect || "";
}}
>
Deny
</button>
<TransactionButton
className="!w-[50%]"
transaction={async () => {
if (!account) {
throw new Error("No account found");
}
if (!code) {
throw new Error("No code found");
}
if (!redirect) {
throw new Error("No redirect found");
}
const accountContract = getContract({
address: account.address,
// hard coded for now
chain: baseSepolia,
client,
});
let hasSessionKey = false;
// check if already added
const accountDeployed =
await isContractDeployed(accountContract);
if (accountDeployed) {
hasSessionKey = await isActiveSigner({
contract: accountContract,
signer: sessionKeySignerAddress,
});
}
// if not added, send tx to add the session key
if (!hasSessionKey) {
return addSessionKey({
account,
contract: accountContract,
sessionKeyAddress: sessionKeySignerAddress,
// hard coded for now
permissions: { approvedTargets: "*" },
});
}
throw "already-added";
}}
onError={async (e) => {
if (!code) {
throw new Error("No code found");
}
if (!redirect) {
throw new Error("No redirect found");
}
if (typeof e === "string" && e === "already-added") {
// sign jwt
const jwt = await generateJWT({
address: account.address,
sessionKeySignerAddress,
code,
});
// redirect
window.location.href = `${redirect}?code=${code}#${jwt}`;
} else {
console.error(e);
}
}}
onTransactionConfirmed={async () => {
if (!code) {
throw new Error("No code found");
}
if (!redirect) {
throw new Error("No redirect found");
}
// sign jwt
const jwt = await generateJWT({
address: account.address,
sessionKeySignerAddress,
code,
});
// redirect
window.location.href = `${redirect}?code=${code}#${jwt}`;
}}
>
Approve
</TransactionButton>
</div>
</div>
) : (
<ConnectEmbed
client={client}
accountAbstraction={{ chain: baseSepolia, sponsorGas: true }}
/>
)}
</div>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+163
View File
@@ -0,0 +1,163 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
/* scroll-behavior: smooth; */
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
font-feature-settings: "rlig" 1, "calt" 0;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
font-variation-settings: normal;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}
@layer base {
:root {
--radius: 0.5rem;
--sticky-top-height: 70px;
}
:root,
[data-theme="dark"] {
/* bg - neutral */
--background: 0 0% 0%;
--card: 0 0% 3.92%;
--popover: 0 0% 0%;
--secondary: 0 0% 11%;
--muted: 0 0% 11%;
--accent: 0 0% 11%;
--inverted: 0 0% 100%;
/* bg - colorful */
--primary: 221 83% 54%;
--destructive: 360 72% 51%;
/* Text */
--foreground: 0 0% 98%;
--card-foreground: 0 0% 98%;
--popover-foreground: 0 0% 98%;
--primary-foreground: 0 0% 100%;
--secondary-foreground: 0 0% 98%;
--muted-foreground: 0 0% 63%;
--accent-foreground: 0 0% 98%;
--destructive-foreground: 0 0% 100%;
--link-foreground: 215.88 100% 65%;
--warning-text: 38 92% 50%;
--destructive-text: 360 72% 55%;
--success-text: 142 75% 50%;
--inverted-foreground: 0 0% 0%;
/* Borders */
--border: 0 0% 15%;
--active-border: 0 0% 22%;
--ring: 0 0% 30%;
--input: 0 0% 15%;
}
[data-theme="light"] {
/* bg - neutral */
--background: 0 0% 98%;
--popover: 0 0% 100%;
--card: 0 0% 100%;
--secondary: 0 0% 90%;
--muted: 0 0% 93%;
--accent: 0 0% 93%;
--inverted: 0 0 0%;
/* bg - colorful */
--primary: 221 83% 54%;
--destructive: 360 72% 51%;
/* Text */
--foreground: 0 0% 4%;
--card-foreground: 0 0% 4%;
--popover-foreground: 240 10% 4%;
--primary-foreground: 0 0% 100%;
--secondary-foreground: 0 0% 4%;
--muted-foreground: 0 0% 40%;
--accent-foreground: 0 0% 9%;
--destructive-foreground: 0 0% 100%;
--inverted-foreground: 0 0% 100%;
--link-foreground: 221.21deg 83.19% 53.33%;
--success-text: 142.09 70.56% 35.29%;
--warning-text: 38 92% 40%;
--destructive-text: 357.15deg 100% 68.72%;
/* Borders */
--border: 0 0% 85%;
--active-border: 0 0% 70%;
--input: 0 0% 85%;
--ring: 0 0% 80%;
}
}
/* If no data-theme is added in body, its in dark theme */
body:not([data-theme="light"]) .light-only {
display: none;
}
[data-theme="light"] .dark-only {
display: none;
}
code span {
color: var(--code-dark-color);
}
body[data-theme="light"] code span {
color: var(--code-light-color);
}
/* @layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
} */
.styled-scrollbar::-webkit-scrollbar {
width: 0.5rem;
height: 0.5rem;
}
@media (max-width: 640px) {
.styled-scrollbar::-webkit-scrollbar {
width: 0;
height: 0;
}
}
.styled-scrollbar::-webkit-scrollbar-thumb {
border-radius: 0.5rem;
transition: color 200ms ease;
background: var(--border);
}
.styled-scrollbar::-webkit-scrollbar-thumb:hover {
background: hsl(var(--foreground));
}
.styled-scrollbar::-webkit-scrollbar-track {
background-color: transparent;
}
button {
-webkit-tap-highlight-color: transparent;
}
::selection {
background: hsl(var(--foreground));
color: hsl(var(--background));
}
.hide-scrollbar {
scrollbar-width: none; /* Firefox */
}
.hide-scrollbar::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
+40
View File
@@ -0,0 +1,40 @@
import type { Metadata } from "next";
import { Fira_Code, Inter } from "next/font/google";
import { Suspense } from "react";
import "./globals.css";
import { Providers } from "./providers";
const sansFont = Inter({
subsets: ["latin"],
variable: "--font-sans",
weight: "variable",
});
const monoFont = Fira_Code({
subsets: ["latin"],
variable: "--font-mono",
weight: "variable",
});
export const metadata: Metadata = {
title: "login with thirdweb",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className="h-full w-full bg-black text-white">
<body
className={`${sansFont.variable} ${monoFont.variable} h-full w-full font-sans antialiased`}
>
<Providers>
<Suspense>{children}</Suspense>
</Providers>
</body>
</html>
);
}
+6
View File
@@ -0,0 +1,6 @@
import { signJWT } from "./authorization/jwt";
import { LoginPageInner } from "./components/LoginPageInner";
export default function Page() {
return <LoginPageInner generateJWT={signJWT} />;
}
+11
View File
@@ -0,0 +1,11 @@
"use client";
import { ThirdwebProvider } from "thirdweb/react";
export const Providers = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <ThirdwebProvider>{children}</ThirdwebProvider>;
};
+18
View File
@@ -0,0 +1,18 @@
import type { Config } from "tailwindcss";
export default {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
},
},
},
plugins: [],
} satisfies Config;
+11
View File
@@ -0,0 +1,11 @@
<html>
<body>
<h1>Test Page</h1>
<!-- load the script -->
<script src="http://localhost:3000/twl.js" data-client-id="testfoo1234"></script>
</body>
</html>
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
+4 -4
View File
@@ -13,8 +13,8 @@
"fix": "eslint ./src --fix"
},
"dependencies": {
"@abstract-foundation/agw-client": "^1.3.0",
"@abstract-foundation/agw-react": "^1.4.2",
"@abstract-foundation/agw-client": "^1.4.0",
"@abstract-foundation/agw-react": "^1.5.0",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dialog": "1.1.5",
@@ -29,7 +29,7 @@
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-tooltip": "1.1.7",
"@tanstack/react-query": "5.65.0",
"@tanstack/react-query": "5.66.0",
"@thirdweb-dev/engine": "^0.0.18",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -48,7 +48,7 @@
"use-debounce": "^10.0.4"
},
"devDependencies": {
"@types/node": "22.10.10",
"@types/node": "22.13.0",
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"eslint": "8.57.0",
+3 -3
View File
@@ -27,7 +27,7 @@
"@radix-ui/react-select": "^2.1.5",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.2",
"@tanstack/react-query": "5.65.0",
"@tanstack/react-query": "5.66.0",
"@tryghost/content-api": "^1.11.21",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -56,7 +56,7 @@
"@next/eslint-plugin-next": "15.1.6",
"@types/flexsearch": "^0.7.6",
"@types/mdx": "^2.0.13",
"@types/node": "22.10.10",
"@types/node": "22.13.0",
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"@types/react-html-parser": "^2.0.6",
@@ -69,7 +69,7 @@
"eslint-plugin-mdx": "^3.1.5",
"eslint-plugin-svg-jsx": "^1.2.4",
"eslint-plugin-tailwindcss": "^3.18.0",
"knip": "5.43.5",
"knip": "5.43.6",
"next-sitemap": "^4.2.3",
"postcss": "8.5.1",
"tailwindcss": "3.4.17",
+4 -4
View File
@@ -15,7 +15,7 @@
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.5",
"@radix-ui/react-slot": "^1.1.1",
"@tanstack/react-query": "5.65.0",
"@tanstack/react-query": "5.66.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
@@ -26,7 +26,7 @@
"react-dom": "19.0.0",
"react-hook-form": "7.54.2",
"server-only": "^0.0.1",
"sonner": "^1.7.2",
"sonner": "^1.7.4",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"thirdweb": "workspace:*",
@@ -35,7 +35,7 @@
},
"devDependencies": {
"@next/eslint-plugin-next": "15.1.6",
"@types/node": "22.10.10",
"@types/node": "22.13.0",
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"@typescript-eslint/eslint-plugin": "7.14.1",
@@ -43,7 +43,7 @@
"eslint": "8.57.0",
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
"knip": "5.43.5",
"knip": "5.43.6",
"postcss": "8.5.1",
"postcss-load-config": "^6.0.1",
"tailwindcss": "3.4.17",
+4 -4
View File
@@ -12,9 +12,9 @@
},
"scripts": {
"preinstall": "npx only-allow pnpm",
"lint": "pnpm dlx sherif@latest -i remark-gfm && turbo run lint",
"lint": "pnpm dlx sherif@latest -i remark-gfm -i eslint && turbo run lint",
"storybook:react": "turbo run storybook --filter=./packages/*",
"prefix": "pnpm dlx sherif@latest -i remark-gfm --fix",
"prefix": "pnpm dlx sherif@latest -i remark-gfm -i eslint --fix",
"fix": "turbo run fix",
"wallet-ui": "turbo run dev --filter=./apps/wallet-ui --filter=./packages/thirdweb",
"wallet-ui:build": "turbo run build --filter=./apps/wallet-ui --filter=./packages/thirdweb",
@@ -43,8 +43,8 @@
"@changesets/changelog-github": "0.5.0",
"@changesets/cli": "2.27.12",
"@manypkg/get-packages": "2.2.2",
"@types/node": "22.10.10",
"turbo": "2.3.4",
"@types/node": "22.13.0",
"turbo": "2.4.0",
"typescript": "5.7.3"
},
"pnpm": {
+30
View File
@@ -1,5 +1,35 @@
# @thirdweb-dev/service-utils
## 0.8.4
### Patch Changes
- [#6137](https://github.com/thirdweb-dev/js/pull/6137) [`a6b7e8d`](https://github.com/thirdweb-dev/js/commit/a6b7e8d81868b5f32f1c8b7ff093bb1f06c734ca) Thanks [@jnsdls](https://github.com/jnsdls)! - updated dependencies
- [#6139](https://github.com/thirdweb-dev/js/pull/6139) [`f4f93e6`](https://github.com/thirdweb-dev/js/commit/f4f93e6353d3951c580df25b757b1ac20fdcf763) Thanks [@arcoraven](https://github.com/arcoraven)! - [service-utils] Update to ESM for compression typrse
- [#6131](https://github.com/thirdweb-dev/js/pull/6131) [`a76e653`](https://github.com/thirdweb-dev/js/commit/a76e6534c49480f30ec1fd1e404f76d04947cd49) Thanks [@arcoraven](https://github.com/arcoraven)! - [service-utils] switch to lz4js for compression lib
## 0.8.3
### Patch Changes
- [#6121](https://github.com/thirdweb-dev/js/pull/6121) [`6ed2fd1`](https://github.com/thirdweb-dev/js/commit/6ed2fd13de1638bcf1c28fd4fdbeb00c0d449994) Thanks [@arcoraven](https://github.com/arcoraven)! - [service-utils] import LZ4 codec
- [#6120](https://github.com/thirdweb-dev/js/pull/6120) [`a22457a`](https://github.com/thirdweb-dev/js/commit/a22457a69cc6667ac13599d10ebddc41ceafa0ff) Thanks [@arcoraven](https://github.com/arcoraven)! - [service-utils] Expose kafkajs config
## 0.8.2
### Patch Changes
- [#6113](https://github.com/thirdweb-dev/js/pull/6113) [`6818cba`](https://github.com/thirdweb-dev/js/commit/6818cbabf25ce73d09dfc971ae793cd9844ceca7) Thanks [@arcoraven](https://github.com/arcoraven)! - Change UsageV2 to per-product topics
## 0.8.1
### Patch Changes
- [#6106](https://github.com/thirdweb-dev/js/pull/6106) [`3361140`](https://github.com/thirdweb-dev/js/commit/33611409b0efd58803be4a96194f5872ddf4bde0) Thanks [@jnsdls](https://github.com/jnsdls)! - updated dependencies
## 0.8.0
### Minor Changes
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/service-utils",
"version": "0.8.0",
"version": "0.8.4",
"type": "module",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
@@ -47,11 +47,13 @@
"dependencies": {
"aws4fetch": "1.0.20",
"kafkajs": "2.2.4",
"lz4js": "^0.2.0",
"zod": "3.24.1"
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250124.3",
"@types/node": "22.10.10",
"@cloudflare/workers-types": "4.20250129.0",
"@types/lz4js": "^0.2.1",
"@types/node": "22.13.0",
"typescript": "5.7.3",
"vitest": "3.0.4"
},
@@ -1,3 +1,4 @@
import type { ServiceName } from "../core/services.js";
import type { UsageV2Event } from "../core/usageV2.js";
/**
@@ -19,6 +20,7 @@ export async function sendUsageV2Events(
events: UsageV2Event[],
options: {
environment: "development" | "production";
productName: ServiceName;
serviceKey: string;
},
): Promise<void> {
@@ -27,7 +29,7 @@ export async function sendUsageV2Events(
? "https://u.thirdweb.com"
: "https://u.thirdweb-dev.com";
const resp = await fetch(`${baseUrl}/usage-v2/raw-events`, {
const resp = await fetch(`${baseUrl}/usage-v2/${options.productName}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
+9 -7
View File
@@ -1,3 +1,5 @@
import type { ServiceName } from "../node/index.js";
export interface UsageV2Event {
/**
* A unique identifier for the event. Defaults to a random UUID.
@@ -8,10 +10,6 @@ export interface UsageV2Event {
* The event timestamp. Defaults to now().
*/
created_at?: Date;
/**
* The source of the event. Example: "storage"
*/
source: string;
/**
* The action of the event. Example: "upload"
*/
@@ -49,8 +47,12 @@ export interface UsageV2Event {
*/
product_version?: string;
/**
* An object of service-specific data. Example: "file_size_bytes"
* It is safe to pass any new JSON-serializable data here before updating the usageV2 schema.
* An object of arbitrary key-value pairs.
* Values can be boolean, number, string, Date, or null.
*/
data: Record<string, unknown>;
[key: string]: boolean | number | string | Date | null | undefined;
}
export function getTopicName(productName: ServiceName) {
return `usage_v2.raw_${productName}`;
}
+73 -21
View File
@@ -1,9 +1,15 @@
import { randomUUID } from "node:crypto";
import { checkServerIdentity } from "node:tls";
import { Kafka, type Producer } from "kafkajs";
import type { UsageV2Event } from "../core/usageV2.js";
const TOPIC_USAGE_V2 = "usage_v2.raw_events";
import {
CompressionCodecs,
CompressionTypes,
Kafka,
type Producer,
type ProducerConfig,
} from "kafkajs";
import { compress, decompress } from "lz4js";
import type { ServiceName } from "../core/services.js";
import { type UsageV2Event, getTopicName } from "../core/usageV2.js";
/**
* Creates a UsageV2Producer which opens a persistent TCP connection.
@@ -21,6 +27,8 @@ const TOPIC_USAGE_V2 = "usage_v2.raw_events";
export class UsageV2Producer {
private kafka: Kafka;
private producer: Producer | null = null;
private topic: string;
private compression: CompressionTypes;
constructor(config: {
/**
@@ -31,14 +39,31 @@ export class UsageV2Producer {
* The environment the service is running in.
*/
environment: "development" | "production";
/**
* The product "source" where usage is coming from.
*/
productName: ServiceName;
/**
* Whether to compress the events.
*/
shouldCompress?: boolean;
username: string;
password: string;
}) {
const {
producerName,
environment,
productName,
shouldCompress = true,
username,
password,
} = config;
this.kafka = new Kafka({
clientId: `${config.producerName}-${config.environment}`,
clientId: `${producerName}-${environment}`,
brokers:
config.environment === "production"
environment === "production"
? ["warpstream.thirdweb.xyz:9092"]
: ["warpstream-dev.thirdweb.xyz:9092"],
ssl: {
@@ -48,19 +73,42 @@ export class UsageV2Producer {
},
sasl: {
mechanism: "plain",
username: config.username,
password: config.password,
username,
password,
},
});
this.topic = getTopicName(productName);
this.compression = shouldCompress
? CompressionTypes.LZ4
: CompressionTypes.None;
}
/**
* Connect the producer.
* This must be called before calling `sendEvents()`.
*/
async init() {
async init(configOverrides?: ProducerConfig) {
if (this.compression === CompressionTypes.LZ4) {
CompressionCodecs[CompressionTypes.LZ4] = () => ({
// biome-ignore lint/style/noRestrictedGlobals: kafkajs expects a Buffer
compress: (encoder: { buffer: Buffer }) => {
const compressed = compress(encoder.buffer);
// biome-ignore lint/style/noRestrictedGlobals: kafkajs expects a Buffer
return Buffer.from(compressed);
},
// biome-ignore lint/style/noRestrictedGlobals: kafkajs expects a Buffer
decompress: (buffer: Buffer) => {
const decompressed = decompress(buffer);
// biome-ignore lint/style/noRestrictedGlobals: kafkajs expects a Buffer
return Buffer.from(decompressed);
},
});
}
this.producer = this.kafka.producer({
allowAutoTopicCreation: false,
...configOverrides,
});
await this.producer.connect();
}
@@ -75,37 +123,41 @@ export class UsageV2Producer {
*
* @param events - The events to send.
*/
async sendEvents(events: UsageV2Event[]): Promise<void> {
async sendEvents(
events: UsageV2Event[],
/**
* Reference: https://kafka.js.org/docs/producing#producing-messages
*/
configOverrides?: {
acks?: number;
timeout?: number;
},
): Promise<void> {
if (!this.producer) {
throw new Error("Producer not initialized. Call `init()` first.");
}
const parsedEvents = events.map((event) => {
return {
...event,
id: event.id ?? randomUUID(),
created_at: event.created_at ?? new Date(),
source: event.source,
action: event.action,
// Remove the "team_" prefix, if any.
team_id: event.team_id.startsWith("team_")
? event.team_id.slice(5)
: event.team_id,
project_id: event.project_id,
sdk_name: event.sdk_name,
sdk_platform: event.sdk_platform,
sdk_version: event.sdk_version,
sdk_os: event.sdk_os,
product_name: event.product_name,
product_version: event.product_version,
data: JSON.stringify(event.data),
};
});
await this.producer.send({
topic: TOPIC_USAGE_V2,
topic: this.topic,
messages: parsedEvents.map((event) => ({
value: JSON.stringify(event),
})),
acks: -1, // All brokers must acknowledge
timeout: 10_000, // 10 seconds
compression: this.compression,
...configOverrides,
});
}
+26
View File
@@ -1,5 +1,31 @@
# thirdweb
## 5.87.3
### Patch Changes
- [#6137](https://github.com/thirdweb-dev/js/pull/6137) [`a6b7e8d`](https://github.com/thirdweb-dev/js/commit/a6b7e8d81868b5f32f1c8b7ff093bb1f06c734ca) Thanks [@jnsdls](https://github.com/jnsdls)! - updated dependencies
- [#6116](https://github.com/thirdweb-dev/js/pull/6116) [`9d5828e`](https://github.com/thirdweb-dev/js/commit/9d5828eeab201960a720744ca3a59c85a0d8e548) Thanks [@alecananian](https://github.com/alecananian)! - Skip factory entrypoint lookup for ZKsync chains
- [#6119](https://github.com/thirdweb-dev/js/pull/6119) [`b693b78`](https://github.com/thirdweb-dev/js/commit/b693b78645e2b214a5f8be0eec6d335d569ceb8c) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Fix: Ecosystem smart wallets now properly trigger switch chain on their admin wallets
- [#6093](https://github.com/thirdweb-dev/js/pull/6093) [`08cc489`](https://github.com/thirdweb-dev/js/commit/08cc48910df351d068c1ce224d4102f40cb1dce1) Thanks [@kumaryash90](https://github.com/kumaryash90)! - 7702 delegation designator
## 5.87.2
### Patch Changes
- [#6104](https://github.com/thirdweb-dev/js/pull/6104) [`bf43196`](https://github.com/thirdweb-dev/js/commit/bf431961ec685deefc0089b8d644c35f849fbf86) Thanks [@MananTank](https://github.com/MananTank)! - Fix thirdweb Pay Modal logging react-query error when opened
## 5.87.1
### Patch Changes
- [#6106](https://github.com/thirdweb-dev/js/pull/6106) [`3361140`](https://github.com/thirdweb-dev/js/commit/33611409b0efd58803be4a96194f5872ddf4bde0) Thanks [@jnsdls](https://github.com/jnsdls)! - updated dependencies
- [#6105](https://github.com/thirdweb-dev/js/pull/6105) [`ab55aec`](https://github.com/thirdweb-dev/js/commit/ab55aec10ccb078add40bd774d157bc0f19ab0bf) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Allow connecting wallets without setting them as active with useConnect
## 5.87.0
### Minor Changes
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "thirdweb",
"version": "5.87.0",
"version": "5.87.3",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
@@ -218,7 +218,7 @@
"@radix-ui/react-focus-scope": "1.1.1",
"@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-tooltip": "1.1.7",
"@tanstack/react-query": "5.65.0",
"@tanstack/react-query": "5.66.0",
"@walletconnect/ethereum-provider": "2.17.5",
"@walletconnect/sign-client": "2.17.5",
"abitype": "1.0.8",
@@ -228,7 +228,7 @@
"mipd": "0.0.7",
"ox": "0.6.9",
"uqr": "0.1.2",
"viem": "2.22.15"
"viem": "2.22.17"
},
"peerDependencies": {
"@aws-sdk/client-lambda": "^3",
@@ -361,8 +361,8 @@
"ethers6": "npm:ethers@6",
"expo-linking": "7.0.3",
"expo-web-browser": "14.0.1",
"happy-dom": "16.7.3",
"knip": "5.43.5",
"happy-dom": "16.8.1",
"knip": "5.43.6",
"msw": "2.7.0",
"prettier": "3.3.3",
"react": "19.0.0",
@@ -83,7 +83,7 @@ export function prepareDirectDeployTransaction(
* ## Deploying a regular contract from ABI and bytecode
*
* ```ts
* import { deployContract } from "thirdweb/deployContract";
* import { deployContract } from "thirdweb/deploys";
*
* const address = await deployContract({
* client,
@@ -101,7 +101,7 @@ export function prepareDirectDeployTransaction(
* ## Deploying a contract deterministically
*
* ```ts
* import { deployContract } from "thirdweb/deployContract";
* import { deployContract } from "thirdweb/deploys";
*
* const address = await deployContract({
* client,
@@ -25,4 +25,5 @@ export const ZKSYNC_WETH: Record<number, string> = {
[11124]: "0x9EDCde0257F2386Ce177C3a7FCdd97787F0D841d",
[37111]: "0xaA91D645D7a6C1aeaa5988e0547267B77d33fe16",
[555271]: "0xb0b8b267d44c64BA6dD1Daf442949887c85199f6",
[2741]: "0x3439153EB7AF838Ad19d56E1571FBD09333C2809",
};
@@ -3,7 +3,6 @@
import type React from "react";
import { createContext, useContext } from "react";
import type { ThirdwebClient } from "../../../client/client.js";
import { isAddress } from "../../../utils/address.js";
/**
* Props for the <AccountProvider /> component
* @component
@@ -53,9 +52,6 @@ export function AccountProvider(
"AccountProvider: No address passed. Ensure an address is always provided to the AccountProvider",
);
}
if (!isAddress(props.address)) {
throw new Error(`AccountProvider: Invalid address: ${props.address}`);
}
return (
<AccountProviderContext.Provider value={props}>
{props.children}
@@ -24,7 +24,7 @@ export function useWalletName(props: {
/**
* @internal Exported for tests only
*/
function getQueryKeys(props: {
export function getQueryKeys(props: {
id: WalletId;
formatFn?: (str: string) => string;
}) {
@@ -41,7 +41,7 @@ function getQueryKeys(props: {
/**
* @internal Exported for tests only
*/
async function fetchWalletName(props: {
export async function fetchWalletName(props: {
id: WalletId;
formatFn?: (str: string) => string;
}) {
@@ -37,7 +37,9 @@ export function TransactionModal(props: ModalProps) {
useQuery({
queryKey: ["transaction-modal-event"],
queryFn: () => {
if (!account || !wallet) return;
if (!account || !wallet) {
throw new Error(); // never happens, because enabled is false
}
trackPayEvent({
client: props.client,
walletAddress: account.address,
@@ -45,8 +47,10 @@ export function TransactionModal(props: ModalProps) {
dstChainId: props.tx.chain.id,
event: "open_pay_transaction_modal",
});
return null;
},
enabled: !!wallet,
enabled: !!wallet && !!account,
});
return (
+3 -3
View File
@@ -25,7 +25,7 @@ export type DownloadOptions = Prettify<
* Download a file from IPFS:
* ```ts
* import { download } from "thirdweb/storage";
* import { createThirdwebClient } from "@thirdweb-dev/sdk";
* import { createThirdwebClient } from "thirdweb";
*
* const client = createThirdwebClient({ clientId: "YOUR_CLIENT_ID" });
*
@@ -38,7 +38,7 @@ export type DownloadOptions = Prettify<
* Download a file from Arweave:
* ```ts
* import { download } from "thirdweb/storage";
* import { createThirdwebClient } from "@thirdweb-dev/sdk";
* import { createThirdwebClient } from "thirdweb";
*
* const client = createThirdwebClient({ clientId: "YOUR_CLIENT_ID" });
*
@@ -51,7 +51,7 @@ export type DownloadOptions = Prettify<
* Download a file from HTTP:
* ```ts
* import { download } from "thirdweb/storage";
* import { createThirdwebClient } from "@thirdweb-dev/sdk";
* import { createThirdwebClient } from "thirdweb";
*
* const client = createThirdwebClient({ clientId: "YOUR_CLIENT_ID" });
*
@@ -51,6 +51,12 @@ describe("extractMinimalProxyImplementationAddress", () => {
expect(result).toBe("0xbebebebebebebebebebebebebebebebebebebebe");
});
it("should extract address from EIP-7702 delegation designator", () => {
const bytecode = "0xef0100bebebebebebebebebebebebebebebebebebebebe";
const result = extractMinimalProxyImplementationAddress(bytecode);
expect(result).toBe("0xbebebebebebebebebebebebebebebebebebebebe");
});
it("should return undefined for non-matching bytecode", () => {
const bytecode =
"0x60806040526000805534801561001457600080fd5b50610150806100246000396000f3fe";
@@ -54,5 +54,11 @@ export function extractMinimalProxyImplementationAddress(
return `0x${implementationAddress}`;
}
// EIP-7702 - https://eips.ethereum.org/EIPS/eip-7702#abstract
if (bytecode.length === 48 && bytecode.startsWith("0xef0100")) {
const implementationAddress = bytecode.slice(8, 48);
return `0x${implementationAddress}`;
}
return undefined;
}
@@ -4,7 +4,6 @@ import { isCoinbaseSDKWallet } from "../coinbase/coinbase-web.js";
import { isInAppWallet } from "../in-app/core/wallet/index.js";
import { getInjectedProvider } from "../injected/index.js";
import type { Wallet } from "../interfaces/wallet.js";
import { isSmartWallet } from "../smart/index.js";
import { isWalletConnect } from "../wallet-connect/controller.js";
import type { WalletId } from "../wallet-types.js";
import type { WalletCapabilities, WalletCapabilitiesRecord } from "./types.js";
@@ -47,7 +46,7 @@ export async function getCapabilities<const ID extends WalletId = WalletId>({
};
}
if (isSmartWallet(wallet)) {
if (wallet.id === "smart") {
const { smartWalletGetCapabilities } = await import(
"../smart/lib/smart-wallet-capabilities.js"
);
@@ -1,10 +1,13 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { TEST_CLIENT } from "../../../test/src/test-clients.js";
import { TEST_ACCOUNT_A } from "../../../test/src/test-wallets.js";
import { baseSepolia } from "../../chains/chain-definitions/base-sepolia.js";
import { sepolia } from "../../chains/chain-definitions/sepolia.js";
import type { ThirdwebClient } from "../../client/client.js";
import type { AsyncStorage } from "../../utils/storage/AsyncStorage.js";
import { inAppWallet } from "../in-app/web/in-app.js";
import type { Account, Wallet } from "../interfaces/wallet.js";
import { smartWallet } from "../smart/smart-wallet.js";
import type { SmartWalletOptions } from "../smart/types.js";
import {
createConnectionManager,
@@ -157,6 +160,46 @@ describe.runIf(process.env.TW_SECRET_KEY)("Connection Manager", () => {
expect(wallet.switchChain).toHaveBeenCalledWith(newChain);
});
it("should switch admin wallet for smart wallet if available", async () => {
const manager = createConnectionManager(storage);
const adminAccount = TEST_ACCOUNT_A;
const adminWallet = inAppWallet();
adminWallet.getAccount = () => adminAccount;
const _wallet = smartWallet({
chain: baseSepolia,
sponsorGas: true,
});
await _wallet.connect({
client,
personalAccount: adminAccount,
});
await manager.handleConnection(adminWallet, { client });
await manager.handleConnection(_wallet, { client });
const newChain = {
id: 2,
name: "New Chain",
rpc: "https://rpc.example.com",
};
// Mock storage and wallet setup
storage.getItem = vi.fn().mockResolvedValue("inApp");
adminWallet.id = "inApp";
_wallet.switchChain = vi.fn();
adminWallet.switchChain = vi.fn();
// Add wallets to connected wallets store
manager.addConnectedWallet(adminWallet);
manager.addConnectedWallet(_wallet);
await manager.switchActiveWalletChain(newChain);
expect(_wallet.switchChain).toHaveBeenCalledWith(newChain);
expect(adminWallet.switchChain).toHaveBeenCalledWith(newChain);
});
it("should define chains", async () => {
const manager = createConnectionManager(storage);
await manager.handleConnection(wallet, { client });
+11 -3
View File
@@ -9,6 +9,7 @@ import { stringify } from "../../utils/json.js";
import type { AsyncStorage } from "../../utils/storage/AsyncStorage.js";
import { deleteConnectParamsFromStorage } from "../../utils/storage/walletStorage.js";
import type { Account, Wallet } from "../interfaces/wallet.js";
import { isSmartWallet } from "../smart/index.js";
import { smartWallet } from "../smart/smart-wallet.js";
import type { SmartWalletOptions } from "../smart/types.js";
import type { WalletId } from "../wallet-types.js";
@@ -24,6 +25,7 @@ export type ConnectionManager = ReturnType<typeof createConnectionManager>;
export type ConnectManagerOptions = {
client: ThirdwebClient;
accountAbstraction?: SmartWalletOptions;
setWalletAsActive?: boolean;
onConnect?: (wallet: Wallet) => void;
};
@@ -146,7 +148,9 @@ export function createConnectionManager(storage: AsyncStorage) {
// add personal wallet to connected wallets list even if it's not the active one
addConnectedWallet(wallet);
handleSetActiveWallet(activeWallet);
if (options?.setWalletAsActive !== false) {
handleSetActiveWallet(activeWallet);
}
wallet.subscribe("accountChanged", async () => {
// We reimplement connect here to prevent memory leaks
@@ -254,7 +258,7 @@ export function createConnectionManager(storage: AsyncStorage) {
throw new Error("Wallet does not support switching chains");
}
if (wallet.id === "smart") {
if (isSmartWallet(wallet)) {
// also switch personal wallet
const personalWalletId = await getStoredActiveWalletId(storage);
if (personalWalletId) {
@@ -263,10 +267,14 @@ export function createConnectionManager(storage: AsyncStorage) {
.find((w) => w.id === personalWalletId);
if (personalWallet) {
await personalWallet.switchChain(chain);
await wallet.switchChain(chain);
// reset the active wallet as switch chain recreates a new smart account
handleSetActiveWallet(wallet);
return;
}
}
// If we couldn't find the personal wallet, just switch the smart wallet
await wallet.switchChain(chain);
// reset the active wallet as switch chain recreates a new smart account
handleSetActiveWallet(wallet);
} else {
await wallet.switchChain(chain);
@@ -0,0 +1,67 @@
import { describe, expect, it } from "vitest";
import { optimism } from "../../chains/chain-definitions/optimism.js";
import type { Wallet } from "../interfaces/wallet.js";
import { getSmartWallet } from "./get-smart-wallet-config.js";
import type { SmartWalletOptions } from "./types.js";
describe("getSmartWallet", () => {
const mockSmartWalletConfig: SmartWalletOptions = {
chain: optimism,
sponsorGas: false,
};
it("should return config for smart wallet ID", () => {
const wallet = {
id: "smart",
getConfig: () => mockSmartWalletConfig,
} as Wallet<"smart">;
expect(getSmartWallet(wallet)).toBe(mockSmartWalletConfig);
});
it("should return smartAccount config for wallet with smartAccount", () => {
const wallet = {
id: "inApp",
getConfig: () => ({
smartAccount: mockSmartWalletConfig,
}),
} as Wallet;
expect(getSmartWallet(wallet)).toBe(mockSmartWalletConfig);
});
it("should throw error for non-smart wallet", () => {
const wallet = {
id: "inApp",
getConfig: () => ({}),
} as Wallet;
expect(() => getSmartWallet(wallet)).toThrow(
"Wallet is not a smart wallet",
);
});
it("should throw error when getConfig returns null", () => {
const wallet = {
id: "inApp",
getConfig: () => null,
// biome-ignore lint/suspicious/noExplicitAny: Testing invalid config
} as any as Wallet;
expect(() => getSmartWallet(wallet)).toThrow(
"Wallet is not a smart wallet",
);
});
it("should throw error when smartAccount is null", () => {
const wallet = {
id: "inApp",
getConfig: () => ({ smartAccount: null }),
// biome-ignore lint/suspicious/noExplicitAny: Testing invalid config
} as any as Wallet;
expect(() => getSmartWallet(wallet)).toThrow(
"Wallet is not a smart wallet",
);
});
});
@@ -0,0 +1,24 @@
import type { Wallet } from "../interfaces/wallet.js";
import type { SmartWalletOptions } from "./types.js";
/**
* Gets the smart wallet configuration for a given wallet.
*
* @param {Wallet} wallet - The wallet to check.
* @returns {SmartWalletOptions} The smart wallet configuration.
*
* @throws {Error} If the wallet is not a smart wallet.
* @internal
*/
export function getSmartWallet(wallet: Wallet): SmartWalletOptions {
if (wallet.id === "smart") {
return (wallet as Wallet<"smart">).getConfig();
}
const config = wallet.getConfig();
if (!!config && "smartAccount" in config && !!config?.smartAccount) {
return config.smartAccount;
}
throw new Error("Wallet is not a smart wallet");
}
+15 -31
View File
@@ -20,12 +20,7 @@ import type { Hex } from "../../utils/encoding/hex.js";
import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js";
import { parseTypedData } from "../../utils/signatures/helpers/parse-typed-data.js";
import { type SignableMessage, maxUint96 } from "../../utils/types.js";
import type {
Account,
SendTransactionOption,
Wallet,
} from "../interfaces/wallet.js";
import type { WalletId } from "../wallet-types.js";
import type { Account, SendTransactionOption } from "../interfaces/wallet.js";
import {
broadcastZkTransaction,
bundleUserOp,
@@ -58,17 +53,7 @@ import type {
UserOperationV06,
UserOperationV07,
} from "./types.js";
/**
* Checks if the provided wallet is a smart wallet.
*
* @param wallet - The wallet to check.
* @returns True if the wallet is a smart wallet, false otherwise.
*/
export function isSmartWallet(
wallet: Wallet<WalletId>,
): wallet is Wallet<"smart"> {
return wallet.id === "smart";
}
export { isSmartWallet } from "./is-smart-wallet.js";
/**
* For in-app wallets, the smart wallet creation is implicit so we track these to be able to retrieve the personal account for a smart account on the wallet API.
@@ -95,6 +80,19 @@ export async function connectSmartAccount(
const options = creationOptions;
const chain = connectChain ?? options.chain;
const sponsorGas =
"gasless" in options ? options.gasless : options.sponsorGas;
if (await isZkSyncChain(chain)) {
return [
createZkSyncAccount({
creationOptions,
connectionOptions,
chain,
sponsorGas,
}),
chain,
];
}
// if factory is passed, but no entrypoint, try to resolve entrypoint from factory
if (options.factoryAddress && !options.overrides?.entrypointAddress) {
@@ -125,20 +123,6 @@ export async function connectSmartAccount(
const factoryAddress =
options.factoryAddress ??
getDefaultAccountFactory(options.overrides?.entrypointAddress);
const sponsorGas =
"gasless" in options ? options.gasless : options.sponsorGas;
if (await isZkSyncChain(chain)) {
return [
createZkSyncAccount({
creationOptions,
connectionOptions,
chain,
sponsorGas,
}),
chain,
];
}
const factoryContract = getContract({
client: client,
@@ -0,0 +1,58 @@
import { describe, expect, it } from "vitest";
import type { Wallet } from "../interfaces/wallet.js";
import { isSmartWallet } from "./is-smart-wallet.js";
describe("isSmartWallet", () => {
it("should return true for smart wallet ID", () => {
const wallet = {
id: "smart",
} as Wallet;
expect(isSmartWallet(wallet)).toBe(true);
});
it("should return true for wallet with smartAccount config", () => {
const wallet = {
id: "inApp",
getConfig: () => ({
smartAccount: {
chain: { id: 1, name: "test", rpc: "test" },
},
}),
} as Wallet;
expect(isSmartWallet(wallet)).toBe(true);
});
it("should return false for non-smart wallet", () => {
const wallet = {
id: "inApp",
getConfig: () => ({}),
} as Wallet;
expect(isSmartWallet(wallet)).toBe(false);
});
it("should return false when getConfig returns null", () => {
const wallet = {
id: "inApp",
getConfig: () => null,
// biome-ignore lint/suspicious/noExplicitAny: Testing invalid config
} as any as Wallet;
expect(isSmartWallet(wallet)).toBe(false);
});
it("should return false when getConfig returns undefined", () => {
const wallet = {
id: "inApp",
getConfig: () => undefined,
} as Wallet;
expect(isSmartWallet(wallet)).toBe(false);
});
it("should return false when smartAccount is null", () => {
const wallet = {
id: "inApp",
// biome-ignore lint/suspicious/noExplicitAny: Testing invalid config
getConfig: () => ({ smartAccount: null }) as any,
} as Wallet;
expect(isSmartWallet(wallet)).toBe(false);
});
});
@@ -0,0 +1,21 @@
import type { Wallet } from "../interfaces/wallet.js";
/**
* Checks if the given wallet is a smart wallet.
*
* @param {Wallet} wallet - The wallet to check.
* @returns {boolean} True if the wallet is a smart wallet, false otherwise.
* @internal
*/
export function isSmartWallet(wallet: Wallet): boolean {
if (wallet.id === "smart") {
return true;
}
const config = wallet.getConfig();
if (!!config && "smartAccount" in config && !!config.smartAccount) {
return true;
}
return false;
}
@@ -4,7 +4,7 @@ import type { Wallet } from "../../interfaces/wallet.js";
* @internal
*/
export function smartWalletGetCapabilities(args: {
wallet: Wallet<"smart">;
wallet: Wallet;
}) {
const { wallet } = args;
+12
View File
@@ -1,5 +1,17 @@
# @thirdweb-dev/wagmi-adapter
## 0.2.12
## 0.2.11
### Patch Changes
- [#6117](https://github.com/thirdweb-dev/js/pull/6117) [`ae675db`](https://github.com/thirdweb-dev/js/commit/ae675db8c86a9e4cddb6eb208cd132e0eeaa1978) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Fixes getProvider autoconnections with account factory
## 0.2.10
## 0.2.9
## 0.2.8
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/wagmi-adapter",
"version": "0.2.8",
"version": "0.2.12",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
+3 -1
View File
@@ -148,7 +148,9 @@ export function inAppWalletConnector(
},
getProvider: async (params) => {
const lastChainId = await config.storage?.getItem("tw.lastChainId");
const chain = defineChain(params?.chainId || lastChainId || 1);
const chain = defineChain(
params?.chainId || args.smartAccount?.chain?.id || lastChainId || 1,
);
if (!wallet.getAccount()) {
const { autoConnect } = await import("thirdweb/wallets");
await autoConnect({
+1408 -946
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -14,6 +14,10 @@
"outputs": ["typedoc/**"],
"dependsOn": ["^build"]
},
"thirdweb-login#build": {
"outputs": [".next/**", "!.next/cache/**"],
"dependsOn": ["^build"]
},
"thirdweb-dashboard#build": {
"env": [
"SENTRY_AUTH_TOKEN",