Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1d28988253 Version Packages (#8687)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-24 22:39:15 +13:00
5352152835 Connect modal approval changeset (#8686)
Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Yash <[email protected]>
2026-02-24 22:36:53 +13:00
decbbb8f7a Connect modal require approval (#8683)
Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Yash <[email protected]>
2026-02-24 22:01:29 +13:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ecd5ae1feb Version Packages (#8672)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-13 23:39:01 +07:00
FirekeeperandGitHub 1caf6dd460 changeset / fmt (#8671) 2026-02-13 23:34:21 +07:00
19204e4654 [SDK] Fix Safe wallet WalletConnect connection on Berachain (#8670)
Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-13 23:24:37 +07:00
Manan TankandGitHub 674c8d5e6c portal: Fix sidebar spacing on top (#8666) 2026-02-11 17:09:29 +13:00
1eb0cd9314 Feat/partner logo branding (#8668)
Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-10 14:52:06 -08:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
b7dfde2d68 Version Packages (#8665)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-04 19:54:15 -08:00
Joaquim VergesandGitHub b50e304e0c changeset (#8664) 2026-02-04 17:56:41 -08:00
9baa251bb1 [SDK] Fix SiteLink and SiteEmbed stripping URL hash fragments (#8663)
Co-authored-by: Claude Opus 4.5 <[email protected]>
2026-02-04 17:55:11 -08:00
Manan TankandGitHub 7f36303b0b portal: OpenApiEndpoint card UI tweaks (#8661) 2026-02-04 17:50:30 -08:00
Manan TankandGitHub 13a46b7ff6 portal: Update SDK Reference breadcrumbs UI (#8662) 2026-02-04 17:50:07 -08:00
Manan TankandGitHub b9d93e44ca portal: landing page ui improvements (#8647) 2026-01-30 12:09:55 -08:00
30 changed files with 645 additions and 240 deletions
@@ -105,6 +105,7 @@ type PartnerPermission = "PROMPT_USER_V1" | "FULL_CONTROL_V1";
export type Partner = {
id: string;
name: string;
imageUrl?: string;
allowlistedDomains: string[];
allowlistedBundleIds: string[];
permissions: [PartnerPermission];
@@ -3,6 +3,7 @@ import { useParams } from "next/navigation";
import { toast } from "sonner";
import type { ThirdwebClient } from "thirdweb";
import type { Ecosystem, Partner } from "@/api/team/ecosystems";
import { useDashboardStorageUpload } from "@/hooks/useDashboardStorageUpload";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useAddPartner } from "../../hooks/use-add-partner";
import { PartnerForm, type PartnerFormValues } from "./partner-form.client";
@@ -23,6 +24,8 @@ export function AddPartnerForm({
const teamSlug = params.team_slug as string;
const ecosystemSlug = params.slug as string;
const storageUpload = useDashboardStorageUpload({ client });
const { mutateAsync: addPartner, isPending } = useAddPartner(
{
authToken,
@@ -48,10 +51,23 @@ export function AddPartnerForm({
},
);
const handleSubmit = (
const isUploading = storageUpload.isPending;
const handleSubmit = async (
values: PartnerFormValues,
finalAccessControl: Partner["accessControl"] | null,
) => {
let imageUrl: string | undefined;
if (values.logo) {
try {
const [uri] = await storageUpload.mutateAsync([values.logo]);
imageUrl = uri;
} catch {
toast.error("Failed to upload logo");
return;
}
}
addPartner({
accessControl: finalAccessControl,
allowlistedBundleIds: values.bundleIds
@@ -61,6 +77,7 @@ export function AddPartnerForm({
.split(/,| /)
.filter((d) => d.length > 0),
ecosystem,
imageUrl,
name: values.name,
});
};
@@ -68,7 +85,7 @@ export function AddPartnerForm({
return (
<PartnerForm
client={client}
isSubmitting={isPending}
isSubmitting={isPending || isUploading}
onSubmit={handleSubmit}
submitLabel="Add"
/>
@@ -1,12 +1,13 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon, Trash2Icon } from "lucide-react";
import { useId } from "react";
import { PencilIcon, PlusIcon, Trash2Icon, XIcon } from "lucide-react";
import { useId, useRef } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import type { ThirdwebClient } from "thirdweb";
import type { z } from "zod";
import type { Partner } from "@/api/team/ecosystems";
import { Img } from "@/components/blocks/Img";
import { Button } from "@/components/ui/button";
import {
Form,
@@ -17,11 +18,13 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { ImageUpload } from "@/components/ui/image-upload";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Spinner } from "@/components/ui/Spinner";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
import { partnerFormSchema } from "../../constants";
import { AllowedOperationsSection } from "./allowed-operations-section";
@@ -118,6 +121,7 @@ export function PartnerForm({
const accessControlId = useId();
const serverVerifierId = useId();
const fileInputRef = useRef<HTMLInputElement>(null);
return (
<Form {...form}>
@@ -150,6 +154,101 @@ export function PartnerForm({
</FormItem>
)}
/>
<FormField
control={form.control}
name="logo"
render={() => {
const removeLogo = form.watch("removeLogo");
const logoFile = form.watch("logo");
const newFilePreview = logoFile
? URL.createObjectURL(logoFile)
: undefined;
const existingImageUrl =
partner?.imageUrl && !removeLogo
? resolveSchemeWithErrorHandler({
client,
uri: partner.imageUrl,
})
: undefined;
const displayUrl = newFilePreview || existingImageUrl;
return (
<FormItem>
<FormLabel>Partner Logo</FormLabel>
<FormControl>
<div>
<input
ref={fileInputRef}
accept="image/png,image/jpeg,image/webp"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
form.setValue("logo", file, {
shouldValidate: true,
});
form.setValue("removeLogo", false);
}
e.target.value = "";
}}
type="file"
/>
{displayUrl ? (
<div className="relative inline-block">
<Img
alt={partner?.name ?? "Partner logo"}
className="size-20 rounded-md border object-contain object-center"
src={displayUrl}
/>
<Button
aria-label="Remove logo"
className="absolute -top-2 -right-2 size-6 rounded-full bg-background p-0 hover:bg-accent"
onClick={() => {
form.setValue("logo", undefined);
form.setValue("removeLogo", true);
}}
size="icon"
type="button"
variant="ghost"
>
<XIcon className="size-3" />
</Button>
<Button
aria-label="Change logo"
className="absolute -bottom-2 -right-2 size-6 rounded-full bg-background p-0 hover:bg-accent"
onClick={() => fileInputRef.current?.click()}
size="icon"
type="button"
variant="ghost"
>
<PencilIcon className="size-3" />
</Button>
</div>
) : (
<ImageUpload
accept="image/png, image/jpeg, image/webp"
className="bg-background"
onUpload={(files) => {
if (files[0]) {
form.setValue("logo", files[0], {
shouldValidate: true,
});
form.setValue("removeLogo", false);
}
}}
/>
)}
</div>
</FormControl>
<FormDescription>
Optional logo for this partner. Used in OTP emails sent to
users authenticating through this partner.
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
defaultValue=""
@@ -3,6 +3,7 @@ import { useParams } from "next/navigation";
import { toast } from "sonner";
import type { ThirdwebClient } from "thirdweb";
import type { Ecosystem, Partner } from "@/api/team/ecosystems";
import { useDashboardStorageUpload } from "@/hooks/useDashboardStorageUpload";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useUpdatePartner } from "../../hooks/use-update-partner";
import { PartnerForm, type PartnerFormValues } from "./partner-form.client";
@@ -25,6 +26,8 @@ export function UpdatePartnerForm({
const teamSlug = params.team_slug as string;
const ecosystemSlug = params.slug as string;
const storageUpload = useDashboardStorageUpload({ client });
const { mutateAsync: updatePartner, isPending } = useUpdatePartner(
{
authToken,
@@ -50,10 +53,31 @@ export function UpdatePartnerForm({
},
);
const handleSubmit = (
const isUploading = storageUpload.isPending;
const handleSubmit = async (
values: PartnerFormValues,
finalAccessControl: Partner["accessControl"] | null,
) => {
// Determine imageUrl based on three states:
// 1. New file uploaded → upload and use new URI
// 2. Explicit removal → send null to clear
// 3. No change → preserve existing partner imageUrl
let imageUrl: string | null | undefined;
if (values.logo) {
try {
const [uri] = await storageUpload.mutateAsync([values.logo]);
imageUrl = uri;
} catch {
toast.error("Failed to upload logo");
return;
}
} else if (values.removeLogo) {
imageUrl = null;
} else {
imageUrl = partner.imageUrl;
}
updatePartner({
accessControl: finalAccessControl,
allowlistedBundleIds: values.bundleIds
@@ -63,6 +87,7 @@ export function UpdatePartnerForm({
.split(/,| /)
.filter((d) => d.length > 0),
ecosystem,
imageUrl,
name: values.name,
partnerId: partner.id,
});
@@ -71,7 +96,7 @@ export function UpdatePartnerForm({
return (
<PartnerForm
client={client}
isSubmitting={isPending}
isSubmitting={isPending || isUploading}
onSubmit={handleSubmit}
partner={partner}
submitLabel="Update"
@@ -154,6 +154,21 @@ const allowedOperationsSchema = z.discriminatedUnion("signMethod", [
export const partnerFormSchema = z
.object({
logo: z
.instanceof(File, {
message: "Please select an image file",
})
.refine(
(file) => ["image/png", "image/jpeg", "image/webp"].includes(file.type),
{
message: "Only PNG, JPG or WEBP images are allowed",
},
)
.refine((file) => file.size <= 500 * 1024, {
message: "Logo size must be less than 500KB",
})
.optional(),
removeLogo: z.boolean().default(false),
accessControl: z
.object({
allowedOperations: z
@@ -8,6 +8,7 @@ import type { Ecosystem, Partner } from "@/api/team/ecosystems";
type AddPartnerParams = {
ecosystem: Ecosystem;
name: string;
imageUrl?: string;
allowlistedDomains: string[];
allowlistedBundleIds: string[];
accessControl?: Partner["accessControl"] | null;
@@ -37,6 +38,7 @@ export function useAddPartner(
accessControl: params.accessControl ?? undefined,
allowlistedBundleIds: params.allowlistedBundleIds,
allowlistedDomains: params.allowlistedDomains,
imageUrl: params.imageUrl,
name: params.name,
// TODO - remove the requirement for permissions in API endpoint
permissions: ["FULL_CONTROL_V1"],
@@ -9,6 +9,7 @@ type UpdatePartnerParams = {
partnerId: string;
ecosystem: Ecosystem;
name: string;
imageUrl?: string | null;
allowlistedDomains: string[];
allowlistedBundleIds: string[];
accessControl?: {
@@ -43,6 +44,7 @@ export function useUpdatePartner(
accessControl: params.accessControl,
allowlistedBundleIds: params.allowlistedBundleIds,
allowlistedDomains: params.allowlistedDomains,
imageUrl: params.imageUrl,
name: params.name,
}),
+166 -151
View File
@@ -2,35 +2,29 @@ import {
ArchiveIcon,
ArrowUpRightIcon,
BrainIcon,
BringToFrontIcon,
CoinsIcon,
GlobeIcon,
ZapIcon,
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { Heading } from "@/components/Document";
import { ChatButton } from "../components/AI/chat-button";
import { Button } from "../components/ui/button";
import {
DotNetIcon,
ExternalLinkIcon,
ReactIcon,
TypeScriptIcon,
UnityIcon,
UnrealEngineIcon,
} from "../icons";
import { ConnectIcon } from "../icons/products/ConnectIcon";
import DocsHeroDark from "./_images/docs-hero-dark.png";
import DocsHeroLight from "./_images/docs-hero-light.png";
export default function Page() {
return (
<main
className="container max-w-5xl grow pb-[4.75rem] scale-y-[0.95] origin-top"
data-noindex
>
<main className="container max-w-5xl grow pb-24" data-noindex>
<Hero />
<div className="space-y-8">
<LearningResourcesSection />
<div className="space-y-24">
<ProductsSection />
<ReferenceSection />
<ArchiveSection />
</div>
@@ -40,36 +34,95 @@ export default function Page() {
function Hero() {
return (
<section className="grid gap-4 pt-14 pb-6 lg:grid-cols-[1fr_420px] lg:py-0">
{/* Left */}
<div className="flex flex-col justify-center">
<div>
<h1 className="mb-3 font-bold text-4xl tracking-tighter lg:text-6xl">
thirdweb Documentation
</h1>
<p className="mb-6 max-w-lg text-base lg:text-lg text-muted-foreground leading-normal text-pretty">
Platform for building the next generation of internet products
</p>
<div className="flex gap-3">
<ChatButton />
<Button
asChild
className="flex items-center gap-2 rounded-full bg-card"
variant="outline"
>
<Link href="https://playground.thirdweb.com">
Playground
<ArrowUpRightIcon className="size-4" />
</Link>
</Button>
</div>
</div>
<section className="relative flex flex-col items-center justify-center text-center py-20 lg:py-24">
{/* Badge */}
<div className="mb-6 inline-flex items-center gap-2 rounded-full border backdrop-blur-sm px-4 py-1.5 text-sm text-muted-foreground">
Developer Documentation
</div>
{/* right */}
<div className="hidden justify-center lg:flex">
<Image alt="" className="dark-only w-full" src={DocsHeroDark} />
<Image alt="" className="light-only w-full" src={DocsHeroLight} />
{/* Title */}
<h1 className="mb-2 lg:mb-4 font-semibold lg:font-bold text-3xl tracking-tighter lg:text-6xl max-w-2xl bg-gradient-to-t dark:bg-gradient-to-b from-foreground to-foreground/70 bg-clip-text text-transparent">
Build the next generation of internet products
</h1>
{/* Subtitle */}
<p className="mb-8 text-base lg:text-lg text-muted-foreground max-w-xl">
Where users and AI can spend, earn and transact autonomously.
</p>
{/* CTAs */}
<div className="flex flex-wrap justify-center gap-2">
<ChatButton className="bg-card rounded-xl px-6" />
<Button
asChild
variant="outline"
className="gap-2 bg-card rounded-xl px-6"
>
<Link href="https://playground.thirdweb.com" target="_blank">
Playground
<ArrowUpRightIcon className="size-4 text-muted-foreground" />
</Link>
</Button>
</div>
</section>
);
}
function SectionHeader(props: { title: string; description: string }) {
return (
<div className="text-center mb-8">
<h2 className="font-semibold text-3xl tracking-tight mb-1">
{props.title}
</h2>
<p className="text-muted-foreground">{props.description}</p>
</div>
);
}
function ReferenceSection() {
return (
<section>
<SectionHeader
title="SDKs"
description="Build on any platform with our SDKs"
/>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card
title="TypeScript"
description="SDK for web and node.js applications"
href="/references/typescript/v5"
icon={TypeScriptIcon}
/>
<Card
title="React"
description="Hooks and components for React apps"
href="/references/typescript/v5"
icon={ReactIcon}
/>
<Card
title="React Native"
description="SDK for React Native apps"
href="/references/typescript/v5"
icon={ReactIcon}
/>
<Card
title=".NET"
description="SDK for .NET applications"
href="/dotnet"
icon={DotNetIcon}
/>
<Card
title="Unity"
description="SDK for Unity games"
href="/unity"
icon={UnityIcon}
/>
<Card
title="Unreal Engine"
description="SDK for Unreal Engine games"
href="/unreal-engine"
icon={UnrealEngineIcon}
/>
</div>
</section>
);
@@ -78,183 +131,145 @@ function Hero() {
function ArchiveSection() {
return (
<section>
<SectionTitle anchorId="client" title="Archived Documentation" />
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<ArticleCardIndex
<p className="font-semibold text-muted-foreground text-3xl tracking-tighter mb-8 text-center">
Archived Documentation
</p>
<div className="grid gap-4 md:grid-cols-2">
<MinimalCard
title="Insight API"
description="Insight API Documentation"
href="https://insight.thirdweb.com/reference"
icon={ArchiveIcon}
title="Insight API"
external
/>
<ArticleCardIndex
<MinimalCard
title="Payments API"
description="Payments API Documentation"
href="https://bridge.thirdweb.com/reference"
icon={ArchiveIcon}
title="Payments API"
external
/>
<ArticleCardIndex
<MinimalCard
title="Transactions"
description="Transactions knowledge base and guides"
href="/engine"
icon={ArchiveIcon}
title="Transactions"
external
/>
<ArticleCardIndex
<MinimalCard
title="Contracts"
description="Contracts knowledge base and guides"
href="/contracts"
icon={ArchiveIcon}
title="Contracts"
external
/>
</div>
</section>
);
}
function ReferenceSection() {
function ProductsSection() {
return (
<section>
<SectionTitle anchorId="client" title="Client libraries" />
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<SDKCard
href="/references/typescript/v5"
icon={TypeScriptIcon}
title="TypeScript"
/>
<SDKCard
href="/references/typescript/v5"
icon={ReactIcon}
title="React"
/>
<SDKCard
href="/references/typescript/v5"
icon={ReactIcon}
title="React Native"
/>
<SDKCard href="/dotnet" icon={DotNetIcon} title="DotNet" />
<SDKCard href="/unity" icon={UnityIcon} title="Unity" />
<SDKCard
href="/unreal-engine"
icon={UnrealEngineIcon}
title="Unreal Engine"
/>
</div>
</section>
);
}
function LearningResourcesSection() {
return (
<section>
<SectionTitle anchorId="learning" title="Documentation" />
<div className="grid gap-4 md:grid-cols-1 lg:grid-cols-3">
<ArticleCardIndex
description="Create wallets to read and transact."
<Card
title="Wallets"
description="Create wallets for your users with flexible authentication options"
href="/wallets"
icon={ConnectIcon}
title="Wallets"
/>
<ArticleCardIndex
description="Create internet native payments with x402"
<Card
title="x402"
description="Internet-native payments with the HTTP 402 protocol"
href="/x402"
icon={ZapIcon}
title="x402"
/>
<ArticleCardIndex
description="Swap and bridge tokens across chains"
href="/bridge"
icon={CoinsIcon}
<Card
title="Bridge"
description="Swap and bridge tokens seamlessly across chains"
href="/bridge"
icon={BringToFrontIcon}
/>
<ArticleCardIndex
description="Launch tokens and markets"
<Card
title="Tokens"
description="Launch and manage tokens and create markets on any blockchain"
href="/tokens"
icon={CoinsIcon}
title="Tokens"
/>
<ArticleCardIndex
<Card
title="Blockchain LLM"
description="Read and write onchain via natural language"
href="/ai/chat"
icon={BrainIcon}
title="AI"
/>
<ArticleCardIndex
description="Build products with our HTTP API"
href="/reference"
icon={ConnectIcon}
<Card
title="HTTP API"
description="Build products with our comprehensive HTTP API"
href="/reference"
icon={GlobeIcon}
/>
</div>
</section>
);
}
function SectionTitle(props: { title: string; anchorId: string }) {
return (
<Heading anchorId={props.anchorId} anchorClassName="mb-2" level={2}>
{props.title}
</Heading>
);
}
/***
* This component is only for the index page
*/
function ArticleCardIndex(props: {
function Card(props: {
title: string;
description: string;
description?: string;
href: string;
icon: React.FC<{ className?: string }>;
external?: boolean;
}) {
return (
<div className="rounded-lg border bg-card p-4 hover:border-active-border relative">
<div className="flex mb-3">
<div className="p-2 rounded-full border bg-background">
<props.icon className="size-4 text-muted-foreground" />
<Link
href={props.href}
target={props.external ? "_blank" : undefined}
className="group relative flex flex-col rounded-2xl border bg-card px-6 py-8 md:py-10 transition-all duration-300 hover:border-active-border"
>
{/* Icon */}
<div className="items-center justify-start flex mb-5">
<div className="rounded-full bg-background border p-2.5">
<props.icon className="size-5 text-muted-foreground transition-colors" />
</div>
</div>
<h3 className="mb-0.5 font-medium text-lg tracking-tight">
<Link
className="before:absolute before:inset-0"
href={props.href}
target={props.external ? "_blank" : undefined}
>
{props.title}
</Link>
</h3>
<p className="text-sm text-muted-foreground">{props.description}</p>
</div>
{/* Content */}
<div className="space-y-1">
<h3 className="font-semibold text-xl tracking-tight">{props.title}</h3>
{props.description && (
<p className="text-sm text-muted-foreground text-pretty">
{props.description}
</p>
)}
</div>
</Link>
);
}
function SDKCard(props: {
function MinimalCard(props: {
title: string;
description?: string;
href: string;
icon: React.FC<{ className?: string }>;
external?: boolean;
}) {
return (
<div className="relative flex items-center gap-3 rounded-lg border bg-card p-4 transition-colors hover:border-active-border text-foreground/80 hover:text-foreground">
<div className="p-2 rounded-full border bg-background">
<props.icon className="size-4 shrink-0" />
<Link
href={props.href}
target={props.external ? "_blank" : undefined}
className="group relative items-center flex rounded-2xl border bg-card p-6 transition-all duration-300 hover:border-active-border gap-4"
>
<div className="rounded-full bg-background border p-3">
<props.icon className="size-4 text-muted-foreground" />
</div>
<div className="flex flex-col">
<h3 className="font-semibold text-base text-foreground mb-0.5">
<Link
href={props.href}
target={props.href.includes("http") ? "_blank" : undefined}
className="before:absolute before:inset-0"
>
{props.title}
</Link>
</h3>
<p className="inline-flex items-center gap-1 text-muted-foreground text-xs">
View docs
<ExternalLinkIcon className="size-3.5 shrink-0 text-muted-foreground/50" />
</p>
{/* Content */}
<div className="space-y-0.5">
<h3 className="font-medium text-lg">{props.title}</h3>
{props.description && (
<p className="text-sm text-muted-foreground text-pretty">
{props.description}
</p>
)}
</div>
</div>
</Link>
);
}
@@ -52,12 +52,27 @@ export function getTDocPage(options: {
// category pages
if (docSlug in subgroups) {
return (
<CategoryPage
doc={doc}
packageSlug={packageSlug}
slug={docSlug as keyof typeof subgroups}
version={version}
/>
<div>
<Breadcrumb
crumbs={[
{
href: `/references/${packageSlug}/${version}`,
name: sdkTitle,
},
{
href: `/references/${packageSlug}/${version}/${docSlug}`,
name: docSlug,
},
]}
/>
<CategoryPage
doc={doc}
packageSlug={packageSlug}
slug={docSlug as keyof typeof subgroups}
version={version}
/>
</div>
);
}
@@ -74,7 +89,7 @@ export function getTDocPage(options: {
crumbs={[
{
href: `/references/${packageSlug}/${version}`,
name: "References",
name: sdkTitle,
},
{
href: `/references/${packageSlug}/${version}/${selectedDoc.name}`,
@@ -209,8 +224,8 @@ async function IndexContent(props: {
return (
<div>
<Heading anchorId="reference" level={1}>
{props.sdkTitle} Reference
<Heading anchorId="reference" level={1} className="mb-6 mt-2">
{props.sdkTitle}
</Heading>
<div className="flex flex-col gap-3">
@@ -49,6 +49,8 @@ Required headers:
<OpenApiEndpoint path="/v1/wallets/user" method="GET" />
<br/>
For ecosystem wallets, include the ecosystem headers in your requests.
</TabsContent>
@@ -10,7 +10,7 @@ const Chat = lazy(() =>
import("./chat").then((mod) => ({ default: mod.Chat })),
);
export function ChatButton() {
export function ChatButton(props: { className?: string }) {
const [isOpen, setIsOpen] = useState(false);
const [hasBeenOpened, setHasBeenOpened] = useState(false);
const closeModal = useCallback(() => setIsOpen(false), []);
@@ -20,7 +20,7 @@ export function ChatButton() {
<>
{/* Inline Button (not floating) */}
<Button
className="gap-2 rounded-full bg-background"
className={cn("gap-2 rounded-full bg-background", props.className)}
onClick={() => {
setIsOpen(true);
setHasBeenOpened(true);
@@ -130,12 +130,12 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) {
hasMultipleExamples={true}
/>
) : (
<div className="rounded-lg border">
<div className="rounded-lg border bg-card">
<RequestExample
codeExamples={requestExamples.map((example) => ({
code: (
<CodeBlock
className="rounded-none border-none"
className="rounded-none border-none !bg-card"
code={example.code}
containerClassName="m-0"
lang={example.lang}
@@ -190,16 +190,17 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) {
>
Response
</Heading>
<div className="overflow-hidden rounded-lg border">
<div className="overflow-hidden rounded-lg border bg-card">
<Tabs className="w-full" defaultValue={responseKeys[0]}>
<TabsList className="w-full">
<TabsList className="w-full pt-1">
{responseKeys.map((status) => (
<TabsTrigger key={status} value={status}>
<div className="flex items-center gap-2 py-1">
<div className="flex items-center gap-1.5 px-0.5">
<span
className={cn("h-2 w-2 rounded-full", {
"bg-green-500": status.startsWith("2"),
"bg-red-500": status.startsWith("4"),
"bg-orange-500": status.startsWith("3"),
"bg-yellow-500": status.startsWith("5"),
})}
/>
@@ -212,7 +213,7 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) {
{responseKeys.map((status) => (
<TabsContent className="m-0" key={status} value={status}>
<CodeBlock
className="rounded-none border-none"
className="rounded-none border-none !bg-card"
code={responseExamples[status] as string}
containerClassName="m-0"
lang="json"
@@ -237,15 +238,16 @@ function ParameterSection(props: {
summary={
<div className="flex items-center gap-3">
<span className="text-sm font-medium">{props.title}</span>
<span className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded">
<span className="text-xs text-muted-foreground bg-muted size-6 rounded-full inline-flex justify-center items-center">
{props.parameters.length}
</span>
</div>
}
accordionItemClassName="border-0 my-0"
accordionTriggerClassName="p-4 hover:bg-muted/50 transition-colors"
headingClassName="!no-underline"
>
<div className="px-4 pb-4">
<div className="px-4 pb-4 space-y-2">
{props.parameters
.sort((a, b) => {
if (a.required === b.required) {
@@ -266,16 +268,16 @@ function InlineParameterItem({ param }: { param: APIParameter }) {
return (
<div className="flex flex-col gap-2 p-3">
<div className="flex items-center gap-2 flex-wrap">
<code className="text-foreground text-sm font-mono bg-background px-2 py-1 rounded border">
<code className="text-foreground text-base font-mono font-medium">
{param.name}
</code>
{param.type && (
<span className="text-xs text-muted-foreground px-2 py-1 bg-muted rounded">
<span className="text-xs text-muted-foreground px-2 py-1 bg-muted rounded-lg">
{param.type}
</span>
)}
{param.required && (
<span className="text-xs text-warning-text px-2 py-1 rounded border border-warning-text">
<span className="text-xs text-warning-text px-2 py-1 rounded-lg border border-warning-text/30 bg-warning-text/15">
Required
</span>
)}
@@ -289,6 +291,7 @@ function InlineParameterItem({ param }: { param: APIParameter }) {
<div className="text-sm flex flex-col gap-2">
<span className="text-muted-foreground">Example: </span>
<CodeBlock
className="!bg-card"
code={
typeof param.example === "object"
? JSON.stringify(param.example)
@@ -29,16 +29,16 @@ function InlineParameterItem({ param }: { param: APIParameter }) {
return (
<div className="flex flex-col gap-2 p-3 rounded-lg">
<div className="flex items-center gap-2 flex-wrap">
<code className="text-foreground text-sm font-mono bg-background px-2 py-1 rounded border">
<code className="text-foreground text-base font-mono font-medium">
{param.name}
</code>
{param.type && (
<span className="text-xs text-muted-foreground px-2 py-1 bg-muted rounded">
<span className="text-xs text-muted-foreground px-2 py-1 bg-muted rounded-lg">
{param.type}
</span>
)}
{param.required && (
<span className="text-xs text-warning-text px-2 py-1 rounded border border-warning-text">
<span className="text-xs text-warning-text px-2 py-1 rounded-lg border border-warning-text/30 bg-warning-text/15">
Required
</span>
)}
@@ -49,15 +49,15 @@ function InlineParameterItem({ param }: { param: APIParameter }) {
)}
{param.example !== undefined && (
<div className="text-sm flex flex-col gap-2">
<span className="text-muted-foreground">Example: </span>
<div className="text-sm flex flex-col gap-2 my-3">
<span className="text-muted-foreground">Example </span>
<CodeClient
code={
typeof param.example === "object"
? JSON.stringify(param.example)
: String(param.example)
}
className="rounded-none border-none"
className="rounded-lg"
lang="json"
scrollableContainerClassName="m-0"
scrollableClassName="max-h-[200px]"
@@ -78,10 +78,11 @@ function ParameterSection(props: {
<div className="border-b last:border-b-0">
<Details
key={props.title}
headingClassName="!no-underline"
summary={
<div className="flex items-center gap-3">
<span className="text-sm font-medium">{props.title}</span>
<span className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded">
<span className="text-xs text-muted-foreground bg-muted size-6 rounded-full inline-flex justify-center items-center">
{props.parameters.length}
</span>
</div>
@@ -138,7 +139,7 @@ export function DynamicRequestExample(props: DynamicRequestExampleProps) {
return (
<QueryClientProvider client={queryClient}>
<div className="rounded-lg border">
<div className="rounded-lg border bg-card">
<RequestExample
codeExamples={props.requestExamples.map((example) => ({
code: (
@@ -1,3 +1,4 @@
import { Spinner } from "@workspace/ui/components/spinner";
import type { OpenAPIV3_1 } from "openapi-types";
import { cache, Suspense } from "react";
import {
@@ -455,10 +456,8 @@ function transformOpenApiToApiEndpointMeta(
function LoadingFallback() {
return (
<div className="flex items-center justify-center p-8">
<div className="text-sm text-muted-foreground">
Loading API documentation...
</div>
<div className="flex items-center justify-center p-8 border rounded-lg min-h-[300px] bg-card">
<Spinner className="size-8 text-muted-foreground" />
</div>
);
}
@@ -109,7 +109,7 @@ export function RequestExample(props: {
}}
value={selectedFormat}
>
<SelectTrigger className="w-[130px] rounded-full bg-card">
<SelectTrigger className="w-[130px] rounded-xl bg-card ">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-background rounded-xl">
@@ -144,7 +144,7 @@ export function RequestExample(props: {
}}
value={selectedExampleType}
>
<SelectTrigger className="w-[130px] rounded-full bg-card">
<SelectTrigger className="md:w-[250px] rounded-xl bg-card text-left">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-background rounded-xl">
@@ -169,7 +169,7 @@ export function RequestExample(props: {
}}
value={selectedExample?.label}
>
<SelectTrigger className="w-[130px] rounded-full bg-card">
<SelectTrigger className="w-[150px] rounded-xl bg-card">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-background rounded-xl">
@@ -57,12 +57,7 @@ export function DocLayout(props: DocLayoutProps) {
data-no-llm={props.noLLM}
data-noindex={props.noIndex}
>
<div className="grow xl:mt-6">
<h5 className="mb-2 text-sm text-muted-foreground">
{props.sideBar.name}
</h5>
{props.children}
</div>
<div className="grow xl:mt-6">{props.children}</div>
<div className="mt-16 xl:mt-20">
<PageFooter
editPageButton={props.editPageButton}
+30 -6
View File
@@ -51,18 +51,28 @@ type ReferenceSideBarProps = {
onLinkClick?: () => void;
header?: React.ReactNode;
name: string;
className?: string;
};
export function DocSidebar(props: ReferenceSideBarProps) {
return (
<div className="flex h-full flex-col pb-10 pt-6 text-muted-foreground text-sm">
<div
className={cn(
"flex h-full flex-col pb-10 pt-6 text-muted-foreground text-sm",
props.className,
)}
>
{/* Side bar Name */}
{props.header}
<ul className="styled-scrollbar transform-gpu space-y-1">
{props.links.map((link, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: TODO - fix this
<li key={i}>
<SidebarItem link={link} onLinkClick={props.onLinkClick} />
<SidebarItem
link={link}
onLinkClick={props.onLinkClick}
isFirst={i === 0}
/>
</li>
))}
</ul>
@@ -70,7 +80,11 @@ export function DocSidebar(props: ReferenceSideBarProps) {
);
}
function SidebarItem(props: { link: SidebarLink; onLinkClick?: () => void }) {
function SidebarItem(props: {
link: SidebarLink;
onLinkClick?: () => void;
isFirst: boolean;
}) {
const pathname = usePathname();
if ("separator" in props.link) {
@@ -86,6 +100,7 @@ function SidebarItem(props: { link: SidebarLink; onLinkClick?: () => void }) {
if (link.isCollapsible === false) {
return (
<DocSidebarNonCollapsible
isFirst={props.isFirst}
key={link.name}
linkGroup={link}
onLinkClick={props.onLinkClick}
@@ -139,13 +154,14 @@ function SidebarItem(props: { link: SidebarLink; onLinkClick?: () => void }) {
function DocSidebarNonCollapsible(props: {
linkGroup: LinkGroup;
onLinkClick?: () => void;
isFirst: boolean;
}) {
const pathname = usePathname();
const { href, name, links, icon } = props.linkGroup;
const isCategoryActive = href ? isSamePage(pathname, href) : false;
return (
<div className="my-4">
<div className={cn("my-4", props.isFirst && "mt-0")}>
<div className="mb-2 flex items-center gap-2 rounded-lg text-foreground">
{icon && <SidebarIcon icon={icon} />}
{href ? (
@@ -167,7 +183,11 @@ function DocSidebarNonCollapsible(props: {
return (
// biome-ignore lint/suspicious/noArrayIndexKey: TODO - fix this
<li key={i}>
<SidebarItem link={link} onLinkClick={props.onLinkClick} />
<SidebarItem
link={link}
onLinkClick={props.onLinkClick}
isFirst={i === 0}
/>
</li>
);
})}
@@ -232,7 +252,11 @@ function DocSidebarCategory(props: {
return (
// biome-ignore lint/suspicious/noArrayIndexKey: TODO - fix this
<li key={i}>
<SidebarItem link={link} onLinkClick={props.onLinkClick} />
<SidebarItem
link={link}
onLinkClick={props.onLinkClick}
isFirst={i === 0}
/>
</li>
);
})}
+24
View File
@@ -1,5 +1,29 @@
# wagmi-inapp
## 0.0.52
### Patch Changes
- Updated dependencies [[`5352152`](https://github.com/thirdweb-dev/js/commit/53521528353a00691e82405460d4fb869ac986de)]:
- thirdweb@5.119.0
- @thirdweb-dev/wagmi-adapter@0.2.207
## 0.0.51
### Patch Changes
- Updated dependencies [[`1caf6dd`](https://github.com/thirdweb-dev/js/commit/1caf6dd4609ac72a06999ef0d70f31a371e5108a)]:
- thirdweb@5.118.2
- @thirdweb-dev/wagmi-adapter@0.2.206
## 0.0.50
### Patch Changes
- Updated dependencies [[`b50e304`](https://github.com/thirdweb-dev/js/commit/b50e304e0ccd1ac7eae551c3d49e82312ffc82d1)]:
- thirdweb@5.118.1
- @thirdweb-dev/wagmi-adapter@0.2.205
## 0.0.49
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wagmi-inapp",
"private": true,
"version": "0.0.49",
"version": "0.0.52",
"type": "module",
"scripts": {
"dev": "vite",
+21
View File
@@ -1,5 +1,26 @@
# @thirdweb-dev/nebula
## 0.2.110
### Patch Changes
- Updated dependencies [[`5352152`](https://github.com/thirdweb-dev/js/commit/53521528353a00691e82405460d4fb869ac986de)]:
- thirdweb@5.119.0
## 0.2.109
### Patch Changes
- Updated dependencies [[`1caf6dd`](https://github.com/thirdweb-dev/js/commit/1caf6dd4609ac72a06999ef0d70f31a371e5108a)]:
- thirdweb@5.118.2
## 0.2.108
### Patch Changes
- Updated dependencies [[`b50e304`](https://github.com/thirdweb-dev/js/commit/b50e304e0ccd1ac7eae551c3d49e82312ffc82d1)]:
- thirdweb@5.118.1
## 0.2.107
### Patch Changes
+1 -1
View File
@@ -57,5 +57,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "0.2.107"
"version": "0.2.110"
}
+18
View File
@@ -1,5 +1,23 @@
# thirdweb
## 5.119.0
### Minor Changes
- [#8686](https://github.com/thirdweb-dev/js/pull/8686) [`5352152`](https://github.com/thirdweb-dev/js/commit/53521528353a00691e82405460d4fb869ac986de) Thanks [@Yash094](https://github.com/Yash094)! - Add `requireApproval` option to `useConnectModal` hook to require terms of service and privacy policy acceptance before connecting an in-app wallet
## 5.118.2
### Patch Changes
- [#8671](https://github.com/thirdweb-dev/js/pull/8671) [`1caf6dd`](https://github.com/thirdweb-dev/js/commit/1caf6dd4609ac72a06999ef0d70f31a371e5108a) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - fix walletconnect not working on berachain and unallowlisted chains
## 5.118.1
### Patch Changes
- [#8664](https://github.com/thirdweb-dev/js/pull/8664) [`b50e304`](https://github.com/thirdweb-dev/js/commit/b50e304e0ccd1ac7eae551c3d49e82312ffc82d1) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix SiteLink not handling hashed URL paths
## 5.118.0
### Minor Changes
+1 -1
View File
@@ -431,5 +431,5 @@
}
},
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "5.118.0"
"version": "5.119.0"
}
@@ -118,6 +118,7 @@ function Modal(
termsOfServiceUrl: props.termsOfServiceUrl,
title: props.title,
titleIconUrl: props.titleIcon,
requireApproval: props.requireApproval,
};
}, [
props.privacyPolicyUrl,
@@ -125,6 +126,7 @@ function Modal(
props.termsOfServiceUrl,
props.title,
props.titleIcon,
props.requireApproval,
]);
return (
@@ -449,6 +451,22 @@ export type UseConnectModalOptions = {
* Refer to the [`SiweAuthOptions`](https://portal.thirdweb.com/references/typescript/v5/SiweAuthOptions) for more details
*/
auth?: SiweAuthOptions;
/**
* Require terms of service and privacy policy to be accepted before connecting an in-app wallet.
*
* By default it's `false`
* @example
* ```tsx
* function Example() {
* const { connect } = useConnectModal();
* return <button onClick={() => connect({ client, requireApproval: true })}>
* Connect
* </button>
* }
* ```
*/
requireApproval?: boolean;
};
// TODO: consilidate Button/Embed/Modal props into one type with extras
@@ -62,4 +62,25 @@ describe("SiteLink", () => {
expect(anchor).toBeTruthy();
await waitFor(() => expect(anchor?.href).toContain("walletId=inApp"));
});
it("preserves hash fragment for hash-routed URLs", async () => {
const testUrl = "https://snapshot.org/#/s:wampei.eth";
const { container } = render(
<SiteLink client={TEST_CLIENT} href={testUrl}>
Test Link
</SiteLink>,
{
setConnectedWallet: true,
},
);
const anchor = container.querySelector("a");
expect(anchor).toBeTruthy();
await waitFor(() => {
const href = anchor?.href ?? "";
// Hash fragment must be preserved in the URL
expect(href).toContain("#/s:wampei.eth");
expect(href).toContain("walletId=");
});
});
});
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getUrlToken } from "./get-url-token.js";
describe.runIf(global.window !== undefined)("getUrlToken", () => {
@@ -48,9 +48,9 @@ describe.runIf(global.window !== undefined)("getUrlToken", () => {
const result = getUrlToken();
expect(result).toEqual({
authCookie: null,
authFlow: null,
authProvider: null,
authCookie: undefined,
authFlow: undefined,
authProvider: undefined,
authResult: { token: "abc" },
walletId: "123",
});
@@ -63,8 +63,8 @@ describe.runIf(global.window !== undefined)("getUrlToken", () => {
expect(result).toEqual({
authCookie: "myCookie",
authFlow: null,
authProvider: null,
authFlow: undefined,
authProvider: undefined,
authResult: undefined,
walletId: "123",
});
@@ -81,7 +81,7 @@ describe.runIf(global.window !== undefined)("getUrlToken", () => {
expect(result).toEqual({
authCookie: "myCookie",
authFlow: null,
authFlow: undefined,
authProvider: "provider1",
authResult: { token: "xyz" },
walletId: "123",
@@ -92,4 +92,60 @@ describe.runIf(global.window !== undefined)("getUrlToken", () => {
"?walletId=123&authResult=%7B%22token%22%3A%22xyz%22%7D&authProvider=provider1&authCookie=myCookie",
);
});
it("should preserve hash fragment when cleaning up URL", () => {
Object.defineProperty(window, "location", {
value: {
...window.location,
search: "?walletId=123&authCookie=myCookie",
hash: "#/s:wampei.eth",
pathname: "/",
},
writable: true,
});
const pushStateSpy = vi.spyOn(window.history, "pushState");
const result = getUrlToken();
expect(result).toEqual({
authCookie: "myCookie",
authFlow: undefined,
authProvider: undefined,
authResult: undefined,
walletId: "123",
});
// Verify pushState was called with the hash preserved
expect(pushStateSpy).toHaveBeenCalledWith({}, "", "/#/s:wampei.eth");
pushStateSpy.mockRestore();
});
it("should parse auth params embedded inside the hash fragment", () => {
Object.defineProperty(window, "location", {
value: {
...window.location,
search: "",
hash: "#/s:wampei.eth?walletId=123&authCookie=myCookie",
pathname: "/",
},
writable: true,
});
const pushStateSpy = vi.spyOn(window.history, "pushState");
const result = getUrlToken();
expect(result).toEqual({
authCookie: "myCookie",
authFlow: undefined,
authProvider: undefined,
authResult: undefined,
walletId: "123",
});
// Verify pushState preserves hash path but strips auth params from it
expect(pushStateSpy).toHaveBeenCalledWith({}, "", "/#/s:wampei.eth");
pushStateSpy.mockRestore();
});
});
@@ -19,18 +19,40 @@ export function getUrlToken():
return undefined;
}
const queryString = window.location.search;
const params = new URLSearchParams(queryString);
const authResultString = params.get("authResult");
const walletId = params.get("walletId") as WalletId | undefined;
const authProvider = params.get("authProvider") as AuthOption | undefined;
const authCookie = params.get("authCookie") as string | undefined;
const authFlow = params.get("authFlow") as "connect" | "link" | undefined;
// Read params from the standard query string
const params = new URLSearchParams(window.location.search);
// Also check for params embedded inside the hash fragment (e.g. #/route?walletId=...)
// This supports hash-routed apps where params may be placed after the hash path
let hashParams: URLSearchParams | undefined;
const hash = window.location.hash || "";
let cleanHash = hash;
const hashQueryIndex = hash.indexOf("?");
if (hashQueryIndex !== -1) {
hashParams = new URLSearchParams(hash.substring(hashQueryIndex));
cleanHash = hash.substring(0, hashQueryIndex);
}
const walletId = (params.get("walletId") ??
hashParams?.get("walletId") ??
undefined) as WalletId | undefined;
const authResultString =
params.get("authResult") ?? hashParams?.get("authResult") ?? undefined;
const authProvider = (params.get("authProvider") ??
hashParams?.get("authProvider") ??
undefined) as AuthOption | undefined;
const authCookie = (params.get("authCookie") ??
hashParams?.get("authCookie") ??
undefined) as string | undefined;
const authFlow = (params.get("authFlow") ??
hashParams?.get("authFlow") ??
undefined) as "connect" | "link" | undefined;
if ((authCookie || authResultString) && walletId) {
const authResult = (() => {
if (authResultString) {
params.delete("authResult");
hashParams?.delete("authResult");
return JSON.parse(decodeURIComponent(authResultString));
}
})();
@@ -38,10 +60,27 @@ export function getUrlToken():
params.delete("authProvider");
params.delete("authCookie");
params.delete("authFlow");
hashParams?.delete("walletId");
hashParams?.delete("authProvider");
hashParams?.delete("authCookie");
hashParams?.delete("authFlow");
const remainingSearch = params.toString();
const searchString = remainingSearch ? `?${remainingSearch}` : "";
// Reconstruct hash, preserving the hash path and any remaining non-auth params
let hashString = cleanHash;
if (hashParams) {
const remainingHashParams = hashParams.toString();
if (remainingHashParams) {
hashString = `${cleanHash}?${remainingHashParams}`;
}
}
window.history.pushState(
{},
"",
`${window.location.pathname}?${params.toString()}`,
`${window.location.pathname}${searchString}${hashString}`,
);
return { authCookie, authFlow, authProvider, authResult, walletId };
}
@@ -110,14 +110,11 @@ export async function connectWC(
}
let optionalChains: Chain[] | undefined = wcOptions?.optionalChains;
let chainToRequest = options.chain;
const chainToRequest = options.chain;
// ignore the given options chains - and set the safe supported chains
if (walletId === "global.safe") {
optionalChains = chainsToRequestForSafe.map(getCachedChain);
if (chainToRequest && !optionalChains.includes(chainToRequest)) {
chainToRequest = undefined;
}
}
// For UniversalProvider, we still need chain configuration for session management
@@ -386,17 +383,6 @@ async function initProvider(
"@walletconnect/universal-provider"
);
let optionalChains: Chain[] | undefined = wcOptions?.optionalChains;
let chainToRequest = options.chain;
// ignore the given options chains - and set the safe supported chains
if (walletId === "global.safe") {
optionalChains = chainsToRequestForSafe.map(getCachedChain);
if (chainToRequest && !optionalChains.includes(chainToRequest)) {
chainToRequest = undefined;
}
}
const provider = await UniversalProvider.init({
metadata: {
description:
@@ -736,4 +722,5 @@ const chainsToRequestForSafe = [
1101, // Polygon zkEVM Mainnet
324, // zkSync Era mainnet
534352, // Scroll mainnet
80094, // Berachain mainnet
];
+6
View File
@@ -1,5 +1,11 @@
# @thirdweb-dev/wagmi-adapter
## 0.2.207
## 0.2.206
## 0.2.205
## 0.2.204
## 0.2.203
+1 -1
View File
@@ -56,5 +56,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "0.2.204"
"version": "0.2.207"
}