Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca6c913361 | ||
|
|
cf166e4c04 | ||
|
|
a51192ffbd | ||
|
|
6fd36aabc7 | ||
|
|
08cc48910d | ||
|
|
f03ded300f | ||
|
|
b651bae4ba | ||
|
|
66833a832c | ||
|
|
9d5828eeab | ||
|
|
b191658fcd | ||
|
|
a6b7e8d818 | ||
|
|
f4f93e6353 | ||
|
|
b693b78645 | ||
|
|
daceeed2ec | ||
|
|
5f7c726917 | ||
|
|
2405456e5f | ||
|
|
a76e6534c4 | ||
|
|
0fb586db52 | ||
|
|
f8ebbe54d9 | ||
|
|
ca756819db | ||
|
|
7217e774b2 | ||
|
|
5de72c5ecd | ||
|
|
60c5c26583 | ||
|
|
dc74c3b4b9 | ||
|
|
69a921d01f | ||
|
|
9abdb92028 | ||
|
|
6ed2fd13de | ||
|
|
a22457a69c | ||
|
|
ae675db8c8 | ||
|
|
54c9333fc9 | ||
|
|
541cf0b3e9 | ||
|
|
c65022174d | ||
|
|
6818cbabf2 | ||
|
|
a7fb844d87 | ||
|
|
bf431961ec | ||
|
|
c5d4cab8f8 | ||
|
|
1ee0dffec4 | ||
|
|
33611409b0 | ||
|
|
ab55aec10c |
+11
-11
@@ -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();
|
||||
|
||||
+225
-21
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
-6
@@ -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`,
|
||||
|
||||
-35
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
-613
@@ -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'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>
|
||||
);
|
||||
};
|
||||
-42
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+33
-14
@@ -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))",
|
||||
|
||||
@@ -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))"
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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();
|
||||
|
||||
+49
-20
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -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 |
@@ -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>`;
|
||||
}
|
||||
})();
|
||||
Vendored
+1
@@ -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))}();
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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 |
@@ -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 */
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { signJWT } from "./authorization/jwt";
|
||||
import { LoginPageInner } from "./components/LoginPageInner";
|
||||
|
||||
export default function Page() {
|
||||
return <LoginPageInner generateJWT={signJWT} />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { ThirdwebProvider } from "thirdweb/react";
|
||||
|
||||
export const Providers = ({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) => {
|
||||
return <ThirdwebProvider>{children}</ThirdwebProvider>;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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({
|
||||
|
||||
Generated
+1408
-946
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,10 @@
|
||||
"outputs": ["typedoc/**"],
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"thirdweb-login#build": {
|
||||
"outputs": [".next/**", "!.next/cache/**"],
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"thirdweb-dashboard#build": {
|
||||
"env": [
|
||||
"SENTRY_AUTH_TOKEN",
|
||||
|
||||
Reference in New Issue
Block a user