Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d28988253 | ||
|
|
5352152835 | ||
|
|
decbbb8f7a | ||
|
|
ecd5ae1feb | ||
|
|
1caf6dd460 | ||
|
|
19204e4654 | ||
|
|
674c8d5e6c | ||
|
|
1eb0cd9314 | ||
|
|
b7dfde2d68 | ||
|
|
b50e304e0c | ||
|
|
9baa251bb1 | ||
|
|
7f36303b0b | ||
|
|
13a46b7ff6 | ||
|
|
b9d93e44ca |
@@ -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];
|
||||
|
||||
+19
-2
@@ -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"
|
||||
/>
|
||||
|
||||
+101
-2
@@ -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=""
|
||||
|
||||
+27
-2
@@ -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"
|
||||
|
||||
+15
@@ -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
|
||||
|
||||
+2
@@ -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"],
|
||||
|
||||
+2
@@ -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
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"name": "wagmi-inapp",
|
||||
"private": true,
|
||||
"version": "0.0.49",
|
||||
"version": "0.0.52",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @thirdweb-dev/wagmi-adapter
|
||||
|
||||
## 0.2.207
|
||||
|
||||
## 0.2.206
|
||||
|
||||
## 0.2.205
|
||||
|
||||
## 0.2.204
|
||||
|
||||
## 0.2.203
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user