Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc892bd136 | ||
|
|
3d7c009d72 | ||
|
|
6dbed0d4fc | ||
|
|
adee1903d5 | ||
|
|
a1d785b8ed | ||
|
|
ba92cd2cbe | ||
|
|
c3d0ea0059 | ||
|
|
35c5f57179 | ||
|
|
2a1f4be48b | ||
|
|
0121ccd702 | ||
|
|
12bcfc596a | ||
|
|
a4ff738268 | ||
|
|
b49c191355 | ||
|
|
06f0765665 | ||
|
|
12adbd8314 | ||
|
|
48e014d426 | ||
|
|
826748bb5d | ||
|
|
d4f5a9db12 | ||
|
|
3b827ea32c | ||
|
|
2231db7980 | ||
|
|
f9872f442c | ||
|
|
473a25237e | ||
|
|
2d4c5ff07c | ||
|
|
ab738eb7ba |
@@ -1,5 +1,5 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
import "@/styles/globals.css";
|
||||
import "../src/global.css";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { ThemeProvider, useTheme } from "next-themes";
|
||||
|
||||
@@ -19,30 +19,7 @@ export function MobileSidebar(props: {
|
||||
triggerClassName?: string;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
const activeLink = useMemo(() => {
|
||||
function isActive(link: SidebarBaseLink) {
|
||||
if (link.exactMatch) {
|
||||
return link.href === pathname;
|
||||
}
|
||||
return pathname?.startsWith(link.href);
|
||||
}
|
||||
|
||||
for (const link of props.links) {
|
||||
if ("group" in link) {
|
||||
for (const subLink of link.links) {
|
||||
if (isActive(subLink)) {
|
||||
return subLink;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isActive(link)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [props.links, pathname]);
|
||||
const activeLink = useActiveSidebarLink(props.links);
|
||||
|
||||
const defaultTrigger = (
|
||||
<Button
|
||||
@@ -75,3 +52,32 @@ export function MobileSidebar(props: {
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function useActiveSidebarLink(links: SidebarLink[]) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const activeLink = useMemo(() => {
|
||||
function isActive(link: SidebarBaseLink) {
|
||||
if (link.exactMatch) {
|
||||
return link.href === pathname;
|
||||
}
|
||||
return pathname?.startsWith(link.href);
|
||||
}
|
||||
|
||||
for (const link of links) {
|
||||
if ("group" in link) {
|
||||
for (const subLink of link.links) {
|
||||
if (isActive(subLink)) {
|
||||
return subLink;
|
||||
}
|
||||
}
|
||||
} else if ("href" in link) {
|
||||
if (isActive(link)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [links, pathname]);
|
||||
|
||||
return activeLink;
|
||||
}
|
||||
|
||||
@@ -18,17 +18,39 @@ export function MultiNetworkSelector(props: {
|
||||
onChange: (chainIds: number[]) => void;
|
||||
disableChainId?: boolean;
|
||||
className?: string;
|
||||
priorityChains?: number[];
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
const options = useMemo(() => {
|
||||
return allChains.map((chain) => {
|
||||
let sortedChains = allChains;
|
||||
|
||||
if (props.priorityChains) {
|
||||
const priorityChainsSet = new Set();
|
||||
for (const chainId of props.priorityChains || []) {
|
||||
priorityChainsSet.add(chainId);
|
||||
}
|
||||
|
||||
const priorityChains = (props.priorityChains || [])
|
||||
.map((chainId) => {
|
||||
return idToChain.get(chainId);
|
||||
})
|
||||
.filter((v) => !!v);
|
||||
|
||||
const otherChains = allChains.filter(
|
||||
(chain) => !priorityChainsSet.has(chain.chainId),
|
||||
);
|
||||
|
||||
sortedChains = [...priorityChains, ...otherChains];
|
||||
}
|
||||
|
||||
return sortedChains.map((chain) => {
|
||||
return {
|
||||
label: cleanChainName(chain.name),
|
||||
value: String(chain.chainId),
|
||||
};
|
||||
});
|
||||
}, [allChains]);
|
||||
}, [allChains, props.priorityChains, idToChain]);
|
||||
|
||||
const searchFn = useCallback(
|
||||
(option: Option, searchValue: string) => {
|
||||
|
||||
@@ -2,11 +2,13 @@ import { ExternalLinkIcon } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { NavLink } from "../ui/NavLink";
|
||||
import { Separator } from "../ui/separator";
|
||||
|
||||
export type SidebarBaseLink = {
|
||||
href: string;
|
||||
label: React.ReactNode;
|
||||
exactMatch?: boolean;
|
||||
icon?: React.FC<{ className?: string }>;
|
||||
tracking?: {
|
||||
category: string;
|
||||
action: string;
|
||||
@@ -19,6 +21,9 @@ export type SidebarLink =
|
||||
| {
|
||||
group: string;
|
||||
links: SidebarBaseLink[];
|
||||
}
|
||||
| {
|
||||
separator: true;
|
||||
};
|
||||
|
||||
type SidebarContentProps = {
|
||||
@@ -27,21 +32,18 @@ type SidebarContentProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function Sidebar(props: SidebarContentProps) {
|
||||
export function CustomSidebar(props: SidebarContentProps) {
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
"sticky top-0 hidden w-[230px] flex-shrink-0 self-start lg:block",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<div className="py-7">
|
||||
{props.header}
|
||||
<div className="flex flex-col gap-1">
|
||||
<RenderSidebarLinks links={props.links} />
|
||||
<div className={cn("hidden w-[230px] shrink-0 lg:block", props.className)}>
|
||||
<aside className="sticky top-0 self-start">
|
||||
<div className="py-7">
|
||||
{props.header}
|
||||
<div className="flex flex-col gap-1">
|
||||
<RenderSidebarLinks links={props.links} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,6 +63,10 @@ export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
if ("separator" in link) {
|
||||
return <Separator className="my-2" />;
|
||||
}
|
||||
|
||||
const isExternal = link.href.startsWith("http");
|
||||
return (
|
||||
<NavLink
|
||||
@@ -68,9 +74,10 @@ export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
|
||||
key={i}
|
||||
href={link.href}
|
||||
className="flex items-center gap-2 rounded-md px-3 py-2 text-muted-foreground text-sm hover:bg-accent"
|
||||
activeClassName="text-foreground"
|
||||
activeClassName="text-foreground bg-accent"
|
||||
exactMatch={link.exactMatch}
|
||||
>
|
||||
{link.icon && <link.icon className="size-4" />}
|
||||
{link.label}
|
||||
{isExternal && <ExternalLinkIcon className="size-3" />}
|
||||
</NavLink>
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { MobileSidebar } from "./MobileSidebar";
|
||||
import { Sidebar, type SidebarLink } from "./Sidebar";
|
||||
import { NavLink } from "../ui/NavLink";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { MobileSidebar, useActiveSidebarLink } from "./MobileSidebar";
|
||||
import { CustomSidebar, type SidebarLink } from "./Sidebar";
|
||||
|
||||
export function SidebarLayout(props: {
|
||||
sidebarLinks: SidebarLink[];
|
||||
@@ -17,7 +33,10 @@ export function SidebarLayout(props: {
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<Sidebar links={sidebarLinks} className={props.desktopSidebarClassName} />
|
||||
<CustomSidebar
|
||||
links={sidebarLinks}
|
||||
className={props.desktopSidebarClassName}
|
||||
/>
|
||||
<MobileSidebar
|
||||
links={sidebarLinks}
|
||||
triggerClassName={props.mobileSidebarClassName}
|
||||
@@ -29,3 +48,118 @@ export function SidebarLayout(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FullWidthSidebarLayout(props: {
|
||||
contentSidebarLinks: SidebarLink[];
|
||||
footerSidebarLinks?: SidebarLink[];
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
footer?: React.ReactNode;
|
||||
}) {
|
||||
const { contentSidebarLinks, children, footerSidebarLinks } = props;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex w-full flex-1 overflow-y-hidden",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{/* left - sidebar */}
|
||||
<Sidebar collapsible="icon" side="left">
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
<RenderSidebarGroup
|
||||
sidebarLinks={contentSidebarLinks}
|
||||
groupName={undefined}
|
||||
/>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
|
||||
{footerSidebarLinks && (
|
||||
<SidebarFooter className="pb-3">
|
||||
<RenderSidebarGroup
|
||||
sidebarLinks={footerSidebarLinks}
|
||||
groupName={undefined}
|
||||
/>
|
||||
</SidebarFooter>
|
||||
)}
|
||||
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
|
||||
{/* right - content */}
|
||||
<div className="flex h-full flex-grow flex-col overflow-y-auto">
|
||||
<MobileSidebarTrigger
|
||||
links={[...contentSidebarLinks, ...(footerSidebarLinks || [])]}
|
||||
/>
|
||||
|
||||
<main className="container z-0 flex min-w-0 max-w-[1280px] grow flex-col pb-20 max-sm:w-full lg:pt-6">
|
||||
{children}
|
||||
</main>
|
||||
{props.footer}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderSidebarGroup(props: {
|
||||
sidebarLinks: SidebarLink[];
|
||||
groupName: string | undefined;
|
||||
}) {
|
||||
const { sidebarLinks } = props;
|
||||
const sidebar = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarMenu className="gap-1.5">
|
||||
{sidebarLinks.map((link) => {
|
||||
if ("href" in link) {
|
||||
return (
|
||||
<SidebarMenuItem key={link.href}>
|
||||
<SidebarMenuButton asChild>
|
||||
<NavLink
|
||||
href={link.href}
|
||||
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent"
|
||||
activeClassName="text-foreground bg-accent"
|
||||
exactMatch={link.exactMatch}
|
||||
tracking={link.tracking}
|
||||
onClick={() => {
|
||||
sidebar.setOpenMobile(false);
|
||||
}}
|
||||
>
|
||||
{link.icon && <link.icon className="size-4" />}
|
||||
<span>{link.label}</span>
|
||||
</NavLink>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if ("separator" in link) {
|
||||
return <SidebarSeparator className="my-1" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RenderSidebarGroup
|
||||
sidebarLinks={link.links}
|
||||
groupName={link.group}
|
||||
key={link.group}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileSidebarTrigger(props: { links: SidebarLink[] }) {
|
||||
const activeLink = useActiveSidebarLink(props.links);
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex items-center gap-3 border-b px-4 py-4 lg:hidden">
|
||||
<SidebarTrigger className="size-4" />
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
{activeLink && <span className="text-sm">{activeLink.label}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export type NavButtonProps = {
|
||||
action: string;
|
||||
label: string;
|
||||
};
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export function NavLink(props: React.PropsWithChildren<NavButtonProps>) {
|
||||
@@ -30,7 +31,9 @@ export function NavLink(props: React.PropsWithChildren<NavButtonProps>) {
|
||||
href={props.href}
|
||||
className={cn(props.className, isActive && props.activeClassName)}
|
||||
target={props.href.startsWith("http") ? "_blank" : undefined}
|
||||
prefetch={false}
|
||||
onClick={() => {
|
||||
props.onClick?.();
|
||||
if (props.tracking) {
|
||||
track({
|
||||
category: props.tracking.category,
|
||||
|
||||
@@ -96,7 +96,10 @@ const BreadcrumbEllipsis = ({
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
className={cn(
|
||||
"relative flex h-9 w-9 items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
|
||||
@@ -94,7 +94,10 @@ const PaginationEllipsis = ({
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
className={cn(
|
||||
"relative flex h-9 w-9 items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
|
||||
@@ -0,0 +1,773 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import { PanelLeft } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = "16rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed";
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
openMobile: boolean;
|
||||
setOpenMobile: (open: boolean) => void;
|
||||
isMobile: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext);
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const SidebarProvider = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const isMobile = useIsMobile();
|
||||
const [openMobile, setOpenMobile] = React.useState(false);
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||
const open = openProp ?? _open;
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value;
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState);
|
||||
} else {
|
||||
_setOpen(openState);
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
[setOpenProp, open],
|
||||
);
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile
|
||||
? setOpenMobile((open) => !open)
|
||||
: setOpen((open) => !open);
|
||||
}, [isMobile, setOpen]);
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [toggleSidebar]);
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed";
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, toggleSidebar],
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarProvider.displayName = "SidebarProvider";
|
||||
|
||||
const Sidebar = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right";
|
||||
variant?: "sidebar" | "floating" | "inset";
|
||||
collapsible?: "offcanvas" | "icon" | "none";
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-mobile="true"
|
||||
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="group peer relative hidden flex-shrink-0 text-sidebar-foreground md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-y-0 z-10 hidden h-full w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Sidebar.displayName = "Sidebar";
|
||||
|
||||
const SidebarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof Button>,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, onClick, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
data-sidebar="trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("h-7 w-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeft />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
SidebarTrigger.displayName = "SidebarTrigger";
|
||||
|
||||
const SidebarRail = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button">
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
data-sidebar="rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"-translate-x-1/2 group-data-[side=left]:-right-4 absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=right]:left-0 sm:flex",
|
||||
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:after:left-full",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarRail.displayName = "SidebarRail";
|
||||
|
||||
const SidebarInset = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"main">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<main
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full flex-1 flex-col bg-background",
|
||||
"md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarInset.displayName = "SidebarInset";
|
||||
|
||||
const SidebarInput = React.forwardRef<
|
||||
React.ElementRef<typeof Input>,
|
||||
React.ComponentProps<typeof Input>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Input
|
||||
ref={ref}
|
||||
data-sidebar="input"
|
||||
className={cn(
|
||||
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarInput.displayName = "SidebarInput";
|
||||
|
||||
const SidebarHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="header"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarHeader.displayName = "SidebarHeader";
|
||||
|
||||
const SidebarFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="footer"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarFooter.displayName = "SidebarFooter";
|
||||
|
||||
const SidebarSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof Separator>,
|
||||
React.ComponentProps<typeof Separator>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Separator
|
||||
ref={ref}
|
||||
data-sidebar="separator"
|
||||
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarSeparator.displayName = "SidebarSeparator";
|
||||
|
||||
const SidebarContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarContent.displayName = "SidebarContent";
|
||||
|
||||
const SidebarGroup = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="group"
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarGroup.displayName = "SidebarGroup";
|
||||
|
||||
const SidebarGroupLabel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"flex h-8 shrink-0 items-center rounded-md px-2 font-medium text-sidebar-foreground/70 text-xs outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarGroupLabel.displayName = "SidebarGroupLabel";
|
||||
|
||||
const SidebarGroupAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:-inset-2 after:absolute after:md:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarGroupAction.displayName = "SidebarGroupAction";
|
||||
|
||||
const SidebarGroupContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="group-content"
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarGroupContent.displayName = "SidebarGroupContent";
|
||||
|
||||
const SidebarMenu = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
data-sidebar="menu"
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenu.displayName = "SidebarMenu";
|
||||
|
||||
const SidebarMenuItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
data-sidebar="menu-item"
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenuItem.displayName = "SidebarMenuItem";
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const SidebarMenuButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>
|
||||
>(
|
||||
(
|
||||
{
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button;
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarMenuButton.displayName = "SidebarMenuButton";
|
||||
|
||||
const SidebarMenuAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}
|
||||
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:-inset-2 after:absolute after:md:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarMenuAction.displayName = "SidebarMenuAction";
|
||||
|
||||
const SidebarMenuBadge = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 font-medium text-sidebar-foreground text-xs tabular-nums",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenuBadge.displayName = "SidebarMenuBadge";
|
||||
|
||||
const SidebarMenuSkeleton = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean;
|
||||
}
|
||||
>(({ className, showIcon = false, ...props }, ref) => {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className="size-4 rounded-md"
|
||||
data-sidebar="menu-skeleton-icon"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 max-w-[--skeleton-width] flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
"--skeleton-width": width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
|
||||
|
||||
const SidebarMenuSub = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-sidebar-border border-l px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenuSub.displayName = "SidebarMenuSub";
|
||||
|
||||
const SidebarMenuSubItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ ...props }, ref) => <li ref={ref} {...props} />);
|
||||
SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
|
||||
|
||||
const SidebarMenuSubButton = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentProps<"a"> & {
|
||||
asChild?: boolean;
|
||||
size?: "sm" | "md";
|
||||
isActive?: boolean;
|
||||
}
|
||||
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"-translate-x-px flex h-7 min-w-0 items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile;
|
||||
}
|
||||
+3
-2
@@ -28,7 +28,7 @@ export function BuyFundsSection(props: { chain: ChainMetadata }) {
|
||||
<div className="h-6" />
|
||||
|
||||
<h2 className="px-4 text-center font-semibold text-lg tracking-tight">
|
||||
Buy Funds on {sanitizedChainName} using thirdweb Pay
|
||||
Buy Funds on {sanitizedChainName} using Universal Bridge
|
||||
</h2>
|
||||
|
||||
<div className="h-2" />
|
||||
@@ -50,8 +50,9 @@ export function BuyFundsSection(props: { chain: ChainMetadata }) {
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/connect/pay/overview"
|
||||
className="inline-flex items-center gap-1.5 text-muted-foreground text-sm hover:text-foreground"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more about thirdweb Pay
|
||||
Learn more about Universal Bridge
|
||||
<ExternalLinkIcon className="size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 406 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 255 KiB |
@@ -47,6 +47,8 @@ import metalBanner from "./temp-assets/metalBanner.png";
|
||||
import plumeBanner from "./temp-assets/plumeBanner.png";
|
||||
|
||||
import plumeCTA from "./temp-assets/plumeCTA.png";
|
||||
import reactiveBanner from "./temp-assets/reactiveBanner.jpg";
|
||||
import reactiveCTA from "./temp-assets/reactiveCTA.jpg";
|
||||
import rivalzBanner from "./temp-assets/rivalzBanner.png";
|
||||
import rivalzCTA from "./temp-assets/rivalzCTA.png";
|
||||
import rootStockBanner from "./temp-assets/rootstock-banner.png";
|
||||
@@ -575,6 +577,18 @@ const chainMetaRecord = {
|
||||
buttonText: "Learn More",
|
||||
},
|
||||
},
|
||||
//Reactive Mainnet
|
||||
1597: {
|
||||
headerImgUrl: reactiveBanner.src,
|
||||
about:
|
||||
"Web3’s first IFTTT infrastructure. Automate multi-chain workflows with event-driven logic – execute actions without compromising decentralization.",
|
||||
cta: {
|
||||
backgroundImageUrl: reactiveCTA.src,
|
||||
title: "Turn Smart Contracts Reactive",
|
||||
buttonLink: "https://dev.reactive.network",
|
||||
buttonText: "Learn More",
|
||||
},
|
||||
},
|
||||
//Saakuru Mainnet
|
||||
7225878: {
|
||||
headerImgUrl: saakuruBanner.src,
|
||||
|
||||
@@ -13,7 +13,7 @@ export function DeployedContractsPageHeader(props: {
|
||||
const [importModalOpen, setImportModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="border-b">
|
||||
<div>
|
||||
<ImportModal
|
||||
isOpen={importModalOpen}
|
||||
onClose={() => {
|
||||
@@ -23,27 +23,25 @@ export function DeployedContractsPageHeader(props: {
|
||||
projectId={props.projectId}
|
||||
/>
|
||||
|
||||
<div className="container">
|
||||
<div className="flex flex-col gap-3 py-10 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h1 className="font-semibold text-3xl tracking-tight">Contracts</h1>
|
||||
</div>
|
||||
<div className="flex gap-3 [&>*]:grow">
|
||||
<Button
|
||||
className="gap-2 bg-card"
|
||||
variant="outline"
|
||||
onClick={() => setImportModalOpen(true)}
|
||||
>
|
||||
<DownloadIcon className="size-4" />
|
||||
Import contract
|
||||
</Button>
|
||||
<Button asChild className="gap-2">
|
||||
<Link href="/explore">
|
||||
<PlusIcon className="size-4" />
|
||||
Deploy contract
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 pb-6 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h1 className="font-semibold text-3xl tracking-tight">Contracts</h1>
|
||||
</div>
|
||||
<div className="flex gap-3 [&>*]:grow">
|
||||
<Button
|
||||
className="gap-2 bg-card"
|
||||
variant="outline"
|
||||
onClick={() => setImportModalOpen(true)}
|
||||
>
|
||||
<DownloadIcon className="size-4" />
|
||||
Import contract
|
||||
</Button>
|
||||
<Button asChild className="gap-2">
|
||||
<Link href="/explore">
|
||||
<PlusIcon className="size-4" />
|
||||
Deploy contract
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,13 +12,12 @@ export function DeployedContractsPage(props: {
|
||||
authToken: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex grow flex-col pb-20">
|
||||
<div className="flex grow flex-col">
|
||||
<DeployedContractsPageHeader
|
||||
teamId={props.teamId}
|
||||
projectId={props.projectId}
|
||||
/>
|
||||
<div className="h-10" />
|
||||
<div className="container flex grow flex-col">
|
||||
<div className="flex grow flex-col">
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DeployedContractsPageAsync {...props} />
|
||||
</Suspense>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AppFooter } from "@/components/blocks/app-footer";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type React from "react";
|
||||
import { TabPathLinks } from "../../@/components/ui/tabs";
|
||||
import { AnnouncementBanner } from "../../components/notices/AnnouncementBanner";
|
||||
import { getAuthTokenWalletAddress } from "../api/lib/getAuthToken";
|
||||
import { TWAutoConnect } from "../components/autoconnect";
|
||||
import { loginRedirect } from "../login/loginRedirect";
|
||||
@@ -53,6 +54,7 @@ async function HeaderAndNav(props: {
|
||||
|
||||
return (
|
||||
<div className="bg-card">
|
||||
<AnnouncementBanner />
|
||||
<AccountHeader
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
account={props.twAccount}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "@/styles/globals.css";
|
||||
import "../global.css";
|
||||
import { DashboardRouterTopProgressBar } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Metadata } from "next";
|
||||
@@ -6,7 +6,6 @@ import PlausibleProvider from "next-plausible";
|
||||
import { Inter } from "next/font/google";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
import { Suspense } from "react";
|
||||
import { OrganizeContractsToProjectsBanner } from "../components/notices/AnnouncementBanner";
|
||||
import { OpCreditsGrantedModalWrapperServer } from "../components/onboarding/OpCreditsGrantedModalWrapperServer";
|
||||
import { PosthogIdentifierServer } from "../components/wallets/PosthogIdentifierServer";
|
||||
import { EnsureValidConnectedWalletLoginServer } from "./components/EnsureValidConnectedWalletLogin/EnsureValidConnectedWalletLoginServer";
|
||||
@@ -72,7 +71,6 @@ export default function RootLayout({
|
||||
fontSans.variable,
|
||||
)}
|
||||
>
|
||||
<OrganizeContractsToProjectsBanner />
|
||||
<AppRouterProviders>
|
||||
{children}
|
||||
<Suspense fallback={null}>
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { type NebulaContext, promptNebula } from "../api/chat";
|
||||
import { createSession, updateSession } from "../api/session";
|
||||
import type { SessionInfo } from "../api/types";
|
||||
@@ -32,7 +32,6 @@ export function ChatPageContent(props: {
|
||||
initialPrompt: string | undefined;
|
||||
}) {
|
||||
const address = useActiveAccount()?.address;
|
||||
const activeChain = useActiveWalletChain();
|
||||
const client = useThirdwebClient(props.authToken);
|
||||
const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false);
|
||||
const [messages, setMessages] = useState<Array<ChatMessage>>(() => {
|
||||
@@ -133,14 +132,9 @@ export function ChatPageContent(props: {
|
||||
// ignore local storage errors
|
||||
}
|
||||
|
||||
// else - use the active chain
|
||||
updatedContextFilters.chainIds = activeChain
|
||||
? [activeChain.id.toString()]
|
||||
: [];
|
||||
|
||||
return updatedContextFilters;
|
||||
});
|
||||
}, [address, isNewSession, hasUserUpdatedContextFilters, activeChain]);
|
||||
}, [address, isNewSession, hasUserUpdatedContextFilters]);
|
||||
|
||||
const [sessionId, _setSessionId] = useState<string | undefined>(
|
||||
props.session?.id,
|
||||
|
||||
@@ -199,6 +199,17 @@ export function ContextFiltersForm(props: {
|
||||
onChange={(values) => {
|
||||
form.setValue("chainIds", values.join(","));
|
||||
}}
|
||||
priorityChains={[
|
||||
1, // ethereum
|
||||
56, // bnb smart chain mainnet (bsc)
|
||||
42161, // arbitrum one mainnet
|
||||
8453, // base mainnet
|
||||
43114, // avalanche mainnet
|
||||
146, // sonic
|
||||
137, // polygon
|
||||
80094, // berachain mainnet
|
||||
10, // optimism
|
||||
]}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { getProjects } from "@/api/projects";
|
||||
import { getTeamNebulaWaitList, getTeams } from "@/api/team";
|
||||
import { AppFooter } from "@/components/blocks/app-footer";
|
||||
import { TabPathLinks } from "@/components/ui/tabs";
|
||||
import { redirect } from "next/navigation";
|
||||
import { AnnouncementBanner } from "../../../../components/notices/AnnouncementBanner";
|
||||
import { getValidAccount } from "../../../account/settings/getAccount";
|
||||
import { getAuthTokenWalletAddress } from "../../../api/lib/getAuthToken";
|
||||
import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client";
|
||||
@@ -42,6 +44,7 @@ export default async function TeamLayout(props: {
|
||||
|
||||
return (
|
||||
<div className="flex h-full grow flex-col">
|
||||
<AnnouncementBanner />
|
||||
<div className="bg-card">
|
||||
<TeamHeaderLoggedIn
|
||||
currentTeam={team}
|
||||
@@ -92,6 +95,7 @@ export default async function TeamLayout(props: {
|
||||
</div>
|
||||
|
||||
<main className="flex grow flex-col">{props.children}</main>
|
||||
<AppFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
import { FullWidthSidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import { AppFooter } from "@/components/blocks/app-footer";
|
||||
import {
|
||||
BookTextIcon,
|
||||
BoxIcon,
|
||||
HomeIcon,
|
||||
SettingsIcon,
|
||||
WalletIcon,
|
||||
} from "lucide-react";
|
||||
import { ContractIcon } from "../../../../(dashboard)/(chain)/components/server/icons/ContractIcon";
|
||||
import { InsightIcon } from "../../../../(dashboard)/(chain)/components/server/icons/InsightIcon";
|
||||
import { PayIcon } from "../../../../(dashboard)/(chain)/components/server/icons/PayIcon";
|
||||
import { SmartAccountIcon } from "../../../../(dashboard)/(chain)/components/server/icons/SmartAccountIcon";
|
||||
|
||||
export function ProjectSidebarLayout(props: {
|
||||
layoutPath: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { layoutPath, children } = props;
|
||||
|
||||
const tracking = (label: string) => ({
|
||||
category: "project-sidebar",
|
||||
action: "click",
|
||||
label,
|
||||
});
|
||||
|
||||
return (
|
||||
<FullWidthSidebarLayout
|
||||
footer={<AppFooter />}
|
||||
contentSidebarLinks={[
|
||||
{
|
||||
href: layoutPath,
|
||||
exactMatch: true,
|
||||
label: "Overview",
|
||||
icon: HomeIcon,
|
||||
tracking: tracking("overview"),
|
||||
},
|
||||
{
|
||||
label: "In-App Wallets",
|
||||
href: `${layoutPath}/connect/in-app-wallets`,
|
||||
icon: WalletIcon,
|
||||
tracking: tracking("in-app-wallets"),
|
||||
},
|
||||
{
|
||||
label: "Account Abstraction",
|
||||
href: `${layoutPath}/connect/account-abstraction`,
|
||||
icon: SmartAccountIcon,
|
||||
tracking: tracking("account-abstraction"),
|
||||
},
|
||||
{
|
||||
label: "Universal Bridge",
|
||||
href: `${layoutPath}/connect/universal-bridge`,
|
||||
icon: PayIcon,
|
||||
tracking: tracking("universal-bridge"),
|
||||
},
|
||||
{
|
||||
href: `${layoutPath}/contracts`,
|
||||
label: "Contracts",
|
||||
icon: ContractIcon,
|
||||
tracking: tracking("contracts"),
|
||||
},
|
||||
{
|
||||
href: `${layoutPath}/insight`,
|
||||
label: "Insight",
|
||||
icon: InsightIcon,
|
||||
tracking: tracking("insight"),
|
||||
},
|
||||
]}
|
||||
footerSidebarLinks={[
|
||||
{
|
||||
href: `${layoutPath}/settings`,
|
||||
label: "Project Settings",
|
||||
icon: SettingsIcon,
|
||||
tracking: tracking("project-settings"),
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com",
|
||||
label: "Documentation",
|
||||
icon: BookTextIcon,
|
||||
tracking: tracking("documentation"),
|
||||
},
|
||||
{
|
||||
href: "https://playground.thirdweb.com/connect/sign-in/button",
|
||||
label: "Playground",
|
||||
icon: BoxIcon,
|
||||
tracking: tracking("playground"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</FullWidthSidebarLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import type { SidebarLink } from "@/components/blocks/Sidebar";
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
|
||||
export default async function Layout(props: {
|
||||
params: Promise<{
|
||||
team_slug: string;
|
||||
project_slug: string;
|
||||
}>;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { team_slug, project_slug } = await props.params;
|
||||
|
||||
const links: SidebarLink[] = [
|
||||
{
|
||||
label: "In-App Wallets",
|
||||
href: `/team/${team_slug}/${project_slug}/connect/in-app-wallets`,
|
||||
},
|
||||
{
|
||||
label: "Account Abstraction",
|
||||
href: `/team/${team_slug}/${project_slug}/connect/account-abstraction`,
|
||||
},
|
||||
{
|
||||
label: "Universal Bridge",
|
||||
href: `/team/${team_slug}/${project_slug}/connect/universal-bridge`,
|
||||
},
|
||||
{
|
||||
label: "Playground",
|
||||
href: "https://playground.thirdweb.com/connect/sign-in/button",
|
||||
},
|
||||
];
|
||||
|
||||
return <SidebarLayout sidebarLinks={links}>{props.children}</SidebarLayout>;
|
||||
}
|
||||
@@ -10,15 +10,11 @@ import Link from "next/link";
|
||||
export default async function Page() {
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
<div className="border-border border-b py-10">
|
||||
<div className="container flex max-w-[1000px] flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h1 className="font-semibold text-2xl tracking-tight sm:text-3xl">
|
||||
Insight
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="mb-6 font-semibold text-2xl tracking-tight sm:text-3xl">
|
||||
Insight
|
||||
</h1>
|
||||
|
||||
<div className="container max-w-[1000px] py-8 pb-20">
|
||||
<div className="pb-20">
|
||||
<BlueprintCard />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { getProjects } from "@/api/projects";
|
||||
import { getTeams } from "@/api/team";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { redirect } from "next/navigation";
|
||||
import { AnnouncementBanner } from "../../../../components/notices/AnnouncementBanner";
|
||||
import { getValidAccount } from "../../../account/settings/getAccount";
|
||||
import { getAuthTokenWalletAddress } from "../../../api/lib/getAuthToken";
|
||||
import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client";
|
||||
import { ProjectSidebarLayout } from "./components/ProjectSidebarLayout";
|
||||
import { SaveLastUsedProject } from "./components/SaveLastUsedProject";
|
||||
import { ProjectTabs } from "./tabs";
|
||||
|
||||
export default async function TeamLayout(props: {
|
||||
children: React.ReactNode;
|
||||
@@ -47,22 +49,26 @@ export default async function TeamLayout(props: {
|
||||
redirect(`/team/${params.team_slug}`);
|
||||
}
|
||||
|
||||
const layoutPath = `/team/${params.team_slug}/${params.project_slug}`;
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
<div className="bg-card">
|
||||
<TeamHeaderLoggedIn
|
||||
currentProject={project}
|
||||
currentTeam={team}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
account={account}
|
||||
accountAddress={accountAddress}
|
||||
/>
|
||||
<ProjectTabs
|
||||
layoutPath={`/team/${params.team_slug}/${params.project_slug}`}
|
||||
/>
|
||||
<SidebarProvider>
|
||||
<div className="flex h-dvh min-w-0 grow flex-col">
|
||||
<div className="sticky top-0 z-10 border-border border-b bg-card">
|
||||
<AnnouncementBanner />
|
||||
<TeamHeaderLoggedIn
|
||||
currentProject={project}
|
||||
currentTeam={team}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
account={account}
|
||||
accountAddress={accountAddress}
|
||||
/>
|
||||
</div>
|
||||
<ProjectSidebarLayout layoutPath={layoutPath}>
|
||||
{props.children}
|
||||
</ProjectSidebarLayout>
|
||||
</div>
|
||||
<div className="flex grow flex-col">{props.children}</div>
|
||||
<SaveLastUsedProject projectId={project.id} teamId={team.id} />
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,13 +29,14 @@ import {
|
||||
getChainMetadata,
|
||||
} from "thirdweb/chains";
|
||||
import { type WalletId, getWalletInfo } from "thirdweb/wallets";
|
||||
import { AnalyticsHeader } from "../../components/Analytics/AnalyticsHeader";
|
||||
import { CombinedBarChartCard } from "../../components/Analytics/CombinedBarChartCard";
|
||||
import { PieChartCard } from "../../components/Analytics/PieChartCard";
|
||||
import { ProjectFTUX } from "./components/ProjectFTUX/ProjectFTUX";
|
||||
import { RpcMethodBarChartCard } from "./components/RpcMethodBarChartCard";
|
||||
import { TransactionsCharts } from "./components/Transactions";
|
||||
|
||||
import { RangeSelector } from "components/analytics/range-selector";
|
||||
|
||||
interface PageParams {
|
||||
team_slug: string;
|
||||
project_slug: string;
|
||||
@@ -80,8 +81,8 @@ export default async function ProjectOverviewPage(props: PageProps) {
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
<div className="w-full border-border border-b">
|
||||
<AnalyticsHeader
|
||||
<div className="w-full">
|
||||
<Header
|
||||
title={project.name}
|
||||
interval={interval}
|
||||
range={range}
|
||||
@@ -90,11 +91,11 @@ export default async function ProjectOverviewPage(props: PageProps) {
|
||||
</div>
|
||||
|
||||
{!isActive ? (
|
||||
<div className="container py-8 pb-20">
|
||||
<div className="pt-6">
|
||||
<ProjectFTUX project={project} teamSlug={params.team_slug} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="container flex grow flex-col py-6">
|
||||
<div className="flex grow flex-col pt-6">
|
||||
<Suspense fallback={<GenericLoadingPage />}>
|
||||
<ProjectAnalytics
|
||||
project={project}
|
||||
@@ -189,7 +190,7 @@ async function ProjectAnalytics(props: {
|
||||
link="https://portal.thirdweb.com/connect/quickstart"
|
||||
/>
|
||||
)}
|
||||
<div className="grid gap-6 max-md:px-6 md:grid-cols-2">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{walletConnections.status === "fulfilled" &&
|
||||
walletConnections.value.length > 0 ? (
|
||||
<WalletDistributionCard data={walletConnections.value} />
|
||||
@@ -461,3 +462,23 @@ async function TotalSponsoredCard({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Header(props: {
|
||||
title: string;
|
||||
interval: "day" | "week";
|
||||
range: Range;
|
||||
showRangeSelector: boolean;
|
||||
}) {
|
||||
const { title, interval, range, showRangeSelector } = props;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-3 md:flex-row md:items-center">
|
||||
<div className="flex-1">
|
||||
<h1 className="font-semibold text-2xl tracking-tight md:text-3xl">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
{showRangeSelector && <RangeSelector interval={interval} range={range} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,14 +7,10 @@ export default async function Layout(props: {
|
||||
}) {
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
<div className="border-border border-b py-10">
|
||||
<div className="container max-w-[1000px]">
|
||||
<h1 className="font-semibold text-3xl tracking-tight">
|
||||
Project Settings
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="container max-w-[1000px] py-6">{props.children}</div>
|
||||
<h1 className="font-semibold text-3xl tracking-tight">
|
||||
Project Settings
|
||||
</h1>
|
||||
<div className="py-6">{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { TabPathLinks } from "@/components/ui/tabs";
|
||||
|
||||
export function ProjectTabs(props: {
|
||||
layoutPath: string;
|
||||
}) {
|
||||
const { layoutPath } = props;
|
||||
|
||||
return (
|
||||
<TabPathLinks
|
||||
tabContainerClassName="px-4 lg:px-6"
|
||||
links={[
|
||||
{
|
||||
path: layoutPath,
|
||||
exactMatch: true,
|
||||
name: "Overview",
|
||||
},
|
||||
{
|
||||
// directly link to /in-app-wallets to skip 1 redirect and use `isActive` for checking if the tab is active or not
|
||||
path: `${layoutPath}/connect/in-app-wallets`,
|
||||
name: "Connect",
|
||||
isActive: (path) => path.startsWith(`${layoutPath}/connect`),
|
||||
},
|
||||
{
|
||||
path: `${layoutPath}/contracts`,
|
||||
name: "Contracts",
|
||||
},
|
||||
|
||||
{
|
||||
path: `${layoutPath}/insight`,
|
||||
name: "Insight",
|
||||
},
|
||||
{
|
||||
path: `${layoutPath}/settings`,
|
||||
name: "Settings",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { AppFooter } from "@/components/blocks/app-footer";
|
||||
import { redirect } from "next/navigation";
|
||||
import { isTeamOnboardingComplete } from "../../login/onboarding/isOnboardingRequired";
|
||||
import { SaveLastVisitedTeamPage } from "../components/last-visited-page/SaveLastVisitedPage";
|
||||
@@ -37,7 +36,6 @@ export default async function RootTeamLayout(props: {
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
<AppFooter />
|
||||
<SaveLastVisitedTeamPage />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { useLocalStorage } from "hooks/useLocalStorage";
|
||||
import { CircleAlertIcon, XIcon } from "lucide-react";
|
||||
import { useSelectedLayoutSegment } from "next/navigation";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
export function AnnouncementBanner(props: {
|
||||
function AnnouncementBannerUI(props: {
|
||||
href: string;
|
||||
label: string;
|
||||
trackingLabel: string;
|
||||
}) {
|
||||
const layoutSegment = useSelectedLayoutSegment();
|
||||
const [hasDismissedAnnouncement, setHasDismissedAnnouncement] =
|
||||
useLocalStorage(`dismissed-${props.trackingLabel}`, false, true);
|
||||
|
||||
if (
|
||||
layoutSegment === "/_not-found" ||
|
||||
hasDismissedAnnouncement ||
|
||||
layoutSegment === "login" ||
|
||||
layoutSegment === "nebula-app" ||
|
||||
layoutSegment === "join" ||
|
||||
layoutSegment === "get-started"
|
||||
) {
|
||||
if (hasDismissedAnnouncement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -34,7 +26,6 @@ export function AnnouncementBanner(props: {
|
||||
target={props.href.startsWith("http") ? "_blank" : undefined}
|
||||
className="ontainer flex cursor-pointer items-center gap-2 lg:justify-center "
|
||||
>
|
||||
<CircleAlertIcon className="hidden size-5 shrink-0 lg:block" />
|
||||
<span className="inline-block font-medium leading-normal hover:underline">
|
||||
{props.label}
|
||||
</span>
|
||||
@@ -53,12 +44,12 @@ export function AnnouncementBanner(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function OrganizeContractsToProjectsBanner() {
|
||||
export function AnnouncementBanner() {
|
||||
return (
|
||||
<AnnouncementBanner
|
||||
href="https://blog.thirdweb.com/organize-contracts/"
|
||||
label="Your contracts are now organized into projects. Learn more"
|
||||
trackingLabel="organize-contracts-to-projects"
|
||||
<AnnouncementBannerUI
|
||||
href="https://playground.thirdweb.com/connect/pay"
|
||||
label="Let users pay with whatever they have without leaving your app"
|
||||
trackingLabel="ub-launch"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -139,9 +139,8 @@ export const PayConfig: React.FC<PayConfigProps> = (props) => {
|
||||
Fee Sharing
|
||||
</h3>
|
||||
<p className="mt-1.5 mb-4 text-foreground text-sm">
|
||||
thirdweb collects a 1% fee per end user transaction through{" "}
|
||||
<strong>Buy With Crypto</strong>. We share 70% of this fee with
|
||||
you.{" "}
|
||||
thirdweb collects a 0.3% protocol fee on swap transactions. You
|
||||
may set your own developer fee in addition to this fee.
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/connect/pay/fee-sharing"
|
||||
target="_blank"
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Toaster } from "sonner";
|
||||
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
|
||||
import { type ActiveCouponResponse, ApplyCouponCardUI } from "./CouponCard";
|
||||
|
||||
const meta = {
|
||||
title: "Billing/Coupons/ApplyCoupon",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function statusStub(status: number) {
|
||||
return async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const data: ActiveCouponResponse | null =
|
||||
status === 200
|
||||
? {
|
||||
id: "xyz",
|
||||
start: 1727992716,
|
||||
end: 1759528716,
|
||||
coupon: {
|
||||
id: "XYZTEST",
|
||||
name: "TEST COUPON",
|
||||
duration: "repeating",
|
||||
duration_in_months: 12,
|
||||
},
|
||||
}
|
||||
: null;
|
||||
return {
|
||||
status,
|
||||
data,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[1100px] flex-col gap-10 py-10">
|
||||
<BadgeContainer label="Prefill code - XYZ, Success - 200">
|
||||
<ApplyCouponCardUI
|
||||
submit={statusStub(200)}
|
||||
onCouponApplied={undefined}
|
||||
prefillPromoCode="XYZ"
|
||||
isPaymentSetup={true}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No Valid Payment setup">
|
||||
<ApplyCouponCardUI
|
||||
submit={statusStub(200)}
|
||||
onCouponApplied={undefined}
|
||||
isPaymentSetup={false}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Success - 200">
|
||||
<ApplyCouponCardUI
|
||||
submit={statusStub(200)}
|
||||
onCouponApplied={undefined}
|
||||
isPaymentSetup={true}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Invalid - 400">
|
||||
<ApplyCouponCardUI
|
||||
submit={statusStub(400)}
|
||||
onCouponApplied={undefined}
|
||||
isPaymentSetup={true}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Not Authorized - 401">
|
||||
<ApplyCouponCardUI
|
||||
submit={statusStub(401)}
|
||||
onCouponApplied={undefined}
|
||||
isPaymentSetup={true}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Already applied - 409">
|
||||
<ApplyCouponCardUI
|
||||
submit={statusStub(409)}
|
||||
onCouponApplied={undefined}
|
||||
isPaymentSetup={true}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Rate Limited - 429">
|
||||
<ApplyCouponCardUI
|
||||
submit={statusStub(429)}
|
||||
onCouponApplied={undefined}
|
||||
isPaymentSetup={true}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Other - 500">
|
||||
<ApplyCouponCardUI
|
||||
submit={statusStub(500)}
|
||||
onCouponApplied={undefined}
|
||||
isPaymentSetup={true}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,360 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
import { DangerSettingCard } from "@/components/blocks/DangerSettingCard";
|
||||
import { SettingsCard } from "@/components/blocks/SettingsCard";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { format, fromUnixTime } from "date-fns";
|
||||
import { TagIcon } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { z } from "zod";
|
||||
|
||||
export type ActiveCouponResponse = {
|
||||
id: string;
|
||||
start: number;
|
||||
end: number | null;
|
||||
coupon: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
duration: "forever" | "once" | "repeating";
|
||||
duration_in_months: number | null;
|
||||
};
|
||||
};
|
||||
|
||||
function ApplyCouponCard(props: {
|
||||
teamId: string | undefined;
|
||||
onCouponApplied: (data: ActiveCouponResponse) => void;
|
||||
isPaymentSetup: boolean;
|
||||
}) {
|
||||
const searchParams = useSearchParams();
|
||||
const couponCode = searchParams?.get("coupon");
|
||||
return (
|
||||
<ApplyCouponCardUI
|
||||
onCouponApplied={props.onCouponApplied}
|
||||
prefillPromoCode={couponCode || undefined}
|
||||
scrollIntoView={!!couponCode}
|
||||
isPaymentSetup={props.isPaymentSetup}
|
||||
submit={async (promoCode: string) => {
|
||||
const res = await apiServerProxy<{ data: ActiveCouponResponse }>({
|
||||
method: "POST",
|
||||
pathname: "/v1/coupons/redeem",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
promoCode,
|
||||
teamId: props.teamId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const json = res.data;
|
||||
return {
|
||||
status: 200,
|
||||
data: json.data,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
data: null,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const couponFormSchema = z.object({
|
||||
promoCode: z.string().min(1, "Coupon code is required"),
|
||||
});
|
||||
|
||||
export function ApplyCouponCardUI(props: {
|
||||
submit: (promoCode: string) => Promise<{
|
||||
status: number;
|
||||
data: null | ActiveCouponResponse;
|
||||
}>;
|
||||
onCouponApplied: ((data: ActiveCouponResponse) => void) | undefined;
|
||||
prefillPromoCode?: string;
|
||||
scrollIntoView?: boolean;
|
||||
isPaymentSetup: boolean;
|
||||
}) {
|
||||
const containerRef = useRef<HTMLFormElement | null>(null);
|
||||
const form = useForm<z.infer<typeof couponFormSchema>>({
|
||||
resolver: zodResolver(couponFormSchema),
|
||||
defaultValues: {
|
||||
promoCode: props.prefillPromoCode || "",
|
||||
},
|
||||
});
|
||||
|
||||
const scrolled = useRef(false);
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (props.scrollIntoView && !scrolled.current) {
|
||||
const el = containerRef.current;
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
el.querySelector("input")?.focus();
|
||||
scrolled.current = true;
|
||||
}
|
||||
}
|
||||
}, [props.scrollIntoView]);
|
||||
|
||||
const applyCoupon = useMutation({
|
||||
mutationFn: (promoCode: string) => props.submit(promoCode),
|
||||
});
|
||||
|
||||
async function onSubmit(values: z.infer<typeof couponFormSchema>) {
|
||||
if (!props.isPaymentSetup) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await applyCoupon.mutateAsync(values.promoCode);
|
||||
switch (res.status) {
|
||||
case 200: {
|
||||
toast.success("Coupon applied successfully");
|
||||
if (res.data) {
|
||||
props.onCouponApplied?.(res.data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 400: {
|
||||
toast.error("Coupon code is invalid");
|
||||
break;
|
||||
}
|
||||
case 401: {
|
||||
toast.error("You are not authorized to apply coupons", {
|
||||
description: "Login to dashboard and try again",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 409: {
|
||||
toast.error("Coupon already applied");
|
||||
break;
|
||||
}
|
||||
case 429: {
|
||||
toast.error("Too many coupons applied in a short period", {
|
||||
description: "Please try again after some time",
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
toast.error("Failed to apply coupon");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
toast.error("Failed to apply coupon");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} ref={containerRef}>
|
||||
<SettingsCard
|
||||
header={{
|
||||
title: "Apply Coupon",
|
||||
description:
|
||||
"Enter your coupon code to apply discounts or free trials on thirdweb products",
|
||||
}}
|
||||
bottomText={
|
||||
props.isPaymentSetup
|
||||
? ""
|
||||
: "A valid payment method must be added to apply a coupon."
|
||||
}
|
||||
saveButton={{
|
||||
variant: "outline",
|
||||
disabled: !props.isPaymentSetup,
|
||||
isPending: applyCoupon.isPending,
|
||||
label: "Apply Coupon",
|
||||
type: "submit",
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
errorText={
|
||||
form.getFieldState("promoCode", form.formState).error?.message
|
||||
}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="promoCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Coupon Code</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="lg:max-w-[450px]"
|
||||
disabled={!props.isPaymentSetup}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsCard>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export function CouponDetailsCardUI(props: {
|
||||
activeCoupon: ActiveCouponResponse;
|
||||
deleteCoupon: {
|
||||
mutateAsync: () => Promise<void>;
|
||||
isPending: boolean;
|
||||
};
|
||||
}) {
|
||||
const { activeCoupon, deleteCoupon } = props;
|
||||
return (
|
||||
<DangerSettingCard
|
||||
className="border-border"
|
||||
footerClassName="!bg-transparent !border-border"
|
||||
title="Coupon Applied"
|
||||
description="To apply another coupon, you must remove this coupon"
|
||||
buttonLabel="Remove Coupon"
|
||||
buttonOnClick={() => {
|
||||
const promise = deleteCoupon.mutateAsync();
|
||||
toast.promise(promise, {
|
||||
success: "Coupon Removed Successfully",
|
||||
error: "Failed To Remove Coupon",
|
||||
});
|
||||
}}
|
||||
confirmationDialog={{
|
||||
title: `Delete Coupon "${activeCoupon.coupon.name}" ?`,
|
||||
description: "Offers added by this coupon will no longer be available.",
|
||||
}}
|
||||
isPending={deleteCoupon.isPending}
|
||||
>
|
||||
<div className="mt-3 flex max-w-[600px] items-center gap-3 rounded-lg border border-border bg-background p-4">
|
||||
<TagIcon className="hidden size-6 text-muted-foreground lg:block" />
|
||||
<div>
|
||||
<h4 className="font-medium text-foreground">
|
||||
{activeCoupon.coupon.name || `Coupon #${activeCoupon.coupon.id}`}
|
||||
</h4>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Valid from {format(fromUnixTime(activeCoupon.start), "MMM d, yyyy")}{" "}
|
||||
{activeCoupon.end && (
|
||||
<>to {format(fromUnixTime(activeCoupon.end), "MMMM d, yyyy")}</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DangerSettingCard>
|
||||
);
|
||||
}
|
||||
|
||||
export function CouponSection(props: {
|
||||
teamId: string | undefined;
|
||||
isPaymentSetup: boolean;
|
||||
}) {
|
||||
const address = useActiveAccount()?.address;
|
||||
const [optimisticCouponData, setOptimisticCouponData] = useState<
|
||||
| {
|
||||
type: "added";
|
||||
data: ActiveCouponResponse;
|
||||
}
|
||||
| {
|
||||
type: "deleted";
|
||||
data: null;
|
||||
}
|
||||
>();
|
||||
|
||||
const activeCoupon = useQuery({
|
||||
queryKey: ["active-coupon", address, props.teamId],
|
||||
queryFn: async () => {
|
||||
const res = await apiServerProxy<{ data: ActiveCouponResponse }>({
|
||||
method: "GET",
|
||||
pathname: "/v1/active-coupon",
|
||||
searchParams: props.teamId ? { teamId: props.teamId } : undefined,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
const json = res.data;
|
||||
return json.data;
|
||||
},
|
||||
});
|
||||
|
||||
const deleteActiveCoupon = useMutation({
|
||||
mutationFn: async () => {
|
||||
const res = await apiServerProxy({
|
||||
method: "DELETE",
|
||||
pathname: "/v1/active-coupon",
|
||||
searchParams: props.teamId ? { teamId: props.teamId } : undefined,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to delete coupon");
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
setOptimisticCouponData({
|
||||
type: "deleted",
|
||||
data: null,
|
||||
});
|
||||
activeCoupon.refetch().then(() => {
|
||||
setOptimisticCouponData(undefined);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (activeCoupon.isPending) {
|
||||
return <LoadingCouponSection />;
|
||||
}
|
||||
|
||||
const couponData = optimisticCouponData
|
||||
? optimisticCouponData.data
|
||||
: activeCoupon.data;
|
||||
|
||||
return (
|
||||
<>
|
||||
{couponData ? (
|
||||
<CouponDetailsCardUI
|
||||
activeCoupon={couponData}
|
||||
deleteCoupon={{
|
||||
mutateAsync: deleteActiveCoupon.mutateAsync,
|
||||
isPending: deleteActiveCoupon.isPending,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Suspense fallback={<LoadingCouponSection />}>
|
||||
<ApplyCouponCard
|
||||
teamId={props.teamId}
|
||||
onCouponApplied={(coupon) => {
|
||||
setOptimisticCouponData({
|
||||
type: "added",
|
||||
data: coupon,
|
||||
});
|
||||
|
||||
activeCoupon.refetch().then(() => {
|
||||
setOptimisticCouponData(undefined);
|
||||
});
|
||||
}}
|
||||
isPaymentSetup={props.isPaymentSetup}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingCouponSection() {
|
||||
return (
|
||||
<div className="flex h-[300px] items-center justify-center rounded-lg border border-border bg-card">
|
||||
<Spinner className="size-6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { mobileViewport } from "../../../../stories/utils";
|
||||
import { CouponDetailsCardUI } from "./CouponCard";
|
||||
|
||||
const meta = {
|
||||
title: "Billing/Coupons/CouponDetails",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
return (
|
||||
<div className="container flex max-w-[1100px] flex-col gap-10 py-10">
|
||||
<CouponDetailsCardUI
|
||||
activeCoupon={{
|
||||
id: "xyz",
|
||||
start: 1727992716,
|
||||
end: 1759528716,
|
||||
coupon: {
|
||||
id: "XYZFOOBAR",
|
||||
name: "TEST COUPON",
|
||||
duration: "repeating",
|
||||
duration_in_months: 12,
|
||||
},
|
||||
}}
|
||||
deleteCoupon={{
|
||||
mutateAsync: async () => {
|
||||
setIsPending(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
setIsPending(false);
|
||||
},
|
||||
isPending: isPending,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { type CouponData, CouponsUI } from "./CouponsUI";
|
||||
|
||||
const meta = {
|
||||
title: "Billing/Coupons",
|
||||
component: Story,
|
||||
} satisfies Meta<typeof CouponsUI>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CouponsUI>;
|
||||
|
||||
const couponStubs: CouponData[] = [
|
||||
// Forever
|
||||
{
|
||||
id: "di_1QAE7jCQUO4TBFqFQgnS4Qj4",
|
||||
start: 1729011691,
|
||||
end: null,
|
||||
coupon: {
|
||||
id: "forever_active",
|
||||
name: "Example 1",
|
||||
duration: "forever",
|
||||
duration_in_months: null,
|
||||
},
|
||||
},
|
||||
|
||||
// Once
|
||||
{
|
||||
id: "once",
|
||||
start: 1733391052,
|
||||
end: null,
|
||||
coupon: {
|
||||
id: "once_active",
|
||||
name: "Example 2",
|
||||
duration: "once",
|
||||
duration_in_months: null,
|
||||
},
|
||||
},
|
||||
|
||||
// Repeating - 3 months
|
||||
{
|
||||
id: "repeating-3",
|
||||
start: 1733391629,
|
||||
end: 1743391629, // Will end in the future
|
||||
coupon: {
|
||||
id: "repeating_active",
|
||||
name: "Example 3",
|
||||
duration: "repeating",
|
||||
duration_in_months: 3,
|
||||
},
|
||||
},
|
||||
|
||||
// Repeating - 1 month
|
||||
{
|
||||
id: "repeating-1",
|
||||
start: 1733391713,
|
||||
end: 1736391713,
|
||||
coupon: {
|
||||
id: "repeating_single",
|
||||
name: "Example 4",
|
||||
duration: "repeating",
|
||||
duration_in_months: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
const [status, setStatus] = useState<"pending" | "error" | "success">(
|
||||
"success",
|
||||
);
|
||||
|
||||
const [activeCouponsType, setActiveCouponsType] = useState<
|
||||
"multiple" | "zero" | "one"
|
||||
>("multiple");
|
||||
|
||||
const activeCoupons = useMemo(() => {
|
||||
if (activeCouponsType === "multiple") {
|
||||
return couponStubs;
|
||||
}
|
||||
|
||||
if (activeCouponsType === "zero") {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (activeCouponsType === "one") {
|
||||
return couponStubs.slice(0, 1);
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [activeCouponsType]);
|
||||
|
||||
const [hasAccountCoupon, setHasAccountCoupon] = useState(true);
|
||||
const [isPaymentSetup, setIsPaymentSetup] = useState(true);
|
||||
const [applyCouponStatus, setApplyCouponStatus] = useState<
|
||||
200 | 400 | 401 | 409 | 429 | 500
|
||||
>(200);
|
||||
|
||||
return (
|
||||
<div className="container max-w-[1100px] py-10">
|
||||
<div className="mb-8 space-y-6">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold text-lg">Status</h3>
|
||||
<RadioGroup
|
||||
value={status}
|
||||
onValueChange={(value) =>
|
||||
setStatus(value as "pending" | "error" | "success")
|
||||
}
|
||||
className="flex gap-4"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="pending" id="status-pending" />
|
||||
<Label htmlFor="status-pending">Pending</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="error" id="status-error" />
|
||||
<Label htmlFor="status-error">Error</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="success" id="status-success" />
|
||||
<Label htmlFor="status-success">Success</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold text-lg">Active Coupons</h3>
|
||||
<RadioGroup
|
||||
value={activeCouponsType}
|
||||
onValueChange={(value) =>
|
||||
setActiveCouponsType(value as "multiple" | "zero" | "one")
|
||||
}
|
||||
className="flex gap-4"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="multiple" id="coupons-multiple" />
|
||||
<Label htmlFor="coupons-multiple">Multiple</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="zero" id="coupons-zero" />
|
||||
<Label htmlFor="coupons-zero">Zero</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="one" id="coupons-one" />
|
||||
<Label htmlFor="coupons-one">One</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold text-lg">Apply Coupon Status</h3>
|
||||
<RadioGroup
|
||||
value={applyCouponStatus.toString()}
|
||||
className="flex gap-4"
|
||||
onValueChange={(value) =>
|
||||
setApplyCouponStatus(
|
||||
Number.parseInt(value) as 200 | 400 | 401 | 409 | 429 | 500,
|
||||
)
|
||||
}
|
||||
>
|
||||
{[200, 400, 401, 409, 429, 500].map((code) => (
|
||||
<div key={code} className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value={code.toString()}
|
||||
id={`status-${code}`}
|
||||
/>
|
||||
<Label htmlFor={`status-${code}`}>{code}</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold text-lg">Other Settings</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Switch
|
||||
id="has-account-coupon"
|
||||
checked={hasAccountCoupon}
|
||||
onCheckedChange={setHasAccountCoupon}
|
||||
/>
|
||||
<Label htmlFor="has-account-coupon">Has Account Coupon</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Switch
|
||||
id="payment-setup"
|
||||
checked={isPaymentSetup}
|
||||
onCheckedChange={setIsPaymentSetup}
|
||||
/>
|
||||
<Label htmlFor="payment-setup">Payment Setup</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CouponsUI
|
||||
status={status}
|
||||
activeCoupons={activeCoupons}
|
||||
accountCouponId={hasAccountCoupon ? activeCoupons[0]?.id : undefined}
|
||||
isPaymentSetup={isPaymentSetup}
|
||||
applyCoupon={async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return {
|
||||
status: applyCouponStatus,
|
||||
data:
|
||||
applyCouponStatus === 200
|
||||
? {
|
||||
id: "xyz",
|
||||
start: 1727992716,
|
||||
end: 1759528716,
|
||||
coupon: {
|
||||
id: "test",
|
||||
name: "TEST COUPON",
|
||||
duration: "repeating",
|
||||
duration_in_months: 12,
|
||||
},
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}}
|
||||
deleteCoupon={async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { Suspense } from "react";
|
||||
import { getAuthToken } from "../../../../../app/api/lib/getAuthToken";
|
||||
import { CouponsClient } from "./CouponsClient";
|
||||
import type { CouponData } from "./CouponsUI";
|
||||
|
||||
async function AsyncCoupons(props: {
|
||||
teamId: string;
|
||||
isPaymentSetup: boolean;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
const allCouponsPromise = fetch(
|
||||
`${API_SERVER_URL}/v1/active-coupons?teamId=${props.teamId}`,
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const accountCouponPromise = fetch(
|
||||
`${API_SERVER_URL}/v1/active-coupon?teamId=${props.teamId}`,
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const [allCouponsRes, accountCouponRes] = await Promise.allSettled([
|
||||
allCouponsPromise,
|
||||
accountCouponPromise,
|
||||
]);
|
||||
|
||||
if (
|
||||
allCouponsRes.status === "rejected" ||
|
||||
accountCouponRes.status === "rejected"
|
||||
) {
|
||||
return (
|
||||
<CouponsClient
|
||||
status="error"
|
||||
activeCoupons={[]}
|
||||
accountCouponId={undefined}
|
||||
isPaymentSetup={props.isPaymentSetup}
|
||||
teamId={props.teamId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const allCouponsData = (await allCouponsRes.value.json()) as {
|
||||
data: CouponData[];
|
||||
};
|
||||
|
||||
const accountCouponData = (await accountCouponRes.value.json()) as {
|
||||
data: CouponData | null;
|
||||
};
|
||||
|
||||
return (
|
||||
<CouponsClient
|
||||
status="success"
|
||||
activeCoupons={allCouponsData.data}
|
||||
accountCouponId={accountCouponData.data?.id}
|
||||
isPaymentSetup={props.isPaymentSetup}
|
||||
teamId={props.teamId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Coupons(props: {
|
||||
teamId: string;
|
||||
isPaymentSetup: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<CouponsClient
|
||||
status="pending"
|
||||
activeCoupons={[]}
|
||||
accountCouponId={undefined}
|
||||
isPaymentSetup={true}
|
||||
teamId={props.teamId}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<AsyncCoupons
|
||||
teamId={props.teamId}
|
||||
isPaymentSetup={props.isPaymentSetup}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { type CouponData, CouponsUI, type CouponsUIProps } from "./CouponsUI";
|
||||
|
||||
export function CouponsClient(
|
||||
props: Omit<
|
||||
CouponsUIProps,
|
||||
"applyCoupon" | "onCouponApplied" | "deleteCoupon"
|
||||
> & {
|
||||
teamId: string;
|
||||
},
|
||||
) {
|
||||
const router = useDashboardRouter();
|
||||
return (
|
||||
<CouponsUI
|
||||
{...props}
|
||||
deleteCoupon={async () => {
|
||||
const res = await apiServerProxy({
|
||||
method: "DELETE",
|
||||
pathname: "/v1/active-coupon",
|
||||
searchParams: props.teamId ? { teamId: props.teamId } : undefined,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to delete coupon");
|
||||
}
|
||||
|
||||
router.refresh();
|
||||
}}
|
||||
applyCoupon={async (promoCode: string) => {
|
||||
const res = await apiServerProxy<{ data: CouponData }>({
|
||||
method: "POST",
|
||||
pathname: "/v1/coupons/redeem",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
promoCode,
|
||||
teamId: props.teamId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return {
|
||||
status: res.status,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
router.refresh();
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
data: json.data,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+391
@@ -0,0 +1,391 @@
|
||||
"use client";
|
||||
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { format, fromUnixTime } from "date-fns";
|
||||
import {
|
||||
AlertCircleIcon,
|
||||
CalendarIcon,
|
||||
CalendarX2Icon,
|
||||
ClockIcon,
|
||||
InfinityIcon,
|
||||
PlusIcon,
|
||||
TicketCheckIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
type Coupon = {
|
||||
id: string;
|
||||
name: string;
|
||||
duration: "forever" | "once" | "repeating";
|
||||
duration_in_months: number | null;
|
||||
};
|
||||
|
||||
export type CouponData = {
|
||||
id: string;
|
||||
start: number;
|
||||
end: number | null;
|
||||
coupon: Coupon;
|
||||
};
|
||||
|
||||
type ApplyCouponFn = (promoCode: string) => Promise<{
|
||||
status: number;
|
||||
data: null | CouponData;
|
||||
}>;
|
||||
|
||||
export type CouponsUIProps = {
|
||||
activeCoupons: CouponData[];
|
||||
status: "pending" | "error" | "success";
|
||||
accountCouponId: string | undefined;
|
||||
isPaymentSetup: boolean;
|
||||
applyCoupon: ApplyCouponFn;
|
||||
deleteCoupon: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function CouponsUI(props: CouponsUIProps) {
|
||||
const formatUnixTime = (timestamp: number) => {
|
||||
const date = fromUnixTime(timestamp);
|
||||
return format(date, "MMM d, yyyy");
|
||||
};
|
||||
|
||||
const renderDuration = (coupon: Coupon) => {
|
||||
if (coupon.duration === "forever") {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<InfinityIcon className="size-4 text-muted-foreground" />
|
||||
<span>Forever</span>
|
||||
</div>
|
||||
);
|
||||
} else if (coupon.duration === "once") {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<ClockIcon className="size-4 text-muted-foreground" />
|
||||
<span>One-time discount</span>
|
||||
</div>
|
||||
);
|
||||
} else if (coupon.duration === "repeating") {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<ClockIcon className="size-4 text-muted-foreground" />
|
||||
<span>
|
||||
Repeats every {coupon.duration_in_months} month
|
||||
{coupon.duration_in_months !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const accountCoupon = props.activeCoupons.find(
|
||||
(coupon) => coupon.id === props.accountCouponId,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border">
|
||||
<div className="flex items-center justify-between border-b bg-card px-6 py-4">
|
||||
<h3 className="font-semibold text-xl tracking-tight">Coupons</h3>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<PlusIcon className="size-4" />
|
||||
Apply Coupon
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<ApplyCouponModalContent
|
||||
isPaymentSetup={props.isPaymentSetup}
|
||||
applyCoupon={props.applyCoupon}
|
||||
accountCoupon={accountCoupon}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{props.status === "success" && props.activeCoupons.length > 0 ? (
|
||||
<TableContainer className="rounded-t-none border-none">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Starts On</TableHead>
|
||||
<TableHead>Ends On</TableHead>
|
||||
<TableHead>Duration </TableHead>
|
||||
{accountCoupon && (
|
||||
<TableHead className="w-24">Actions</TableHead>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{props.activeCoupons.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
{/* Name */}
|
||||
<TableCell className="font-medium">
|
||||
{item.coupon.name}
|
||||
</TableCell>
|
||||
|
||||
{/* Start Date */}
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarIcon className="size-4 text-muted-foreground" />
|
||||
{formatUnixTime(item.start)}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* End Date */}
|
||||
<TableCell>
|
||||
{item.end !== null ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarIcon className="size-4 text-muted-foreground" />
|
||||
{formatUnixTime(item.end)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarX2Icon className="size-4 text-muted-foreground" />
|
||||
<span className="text-foreground">Never </span>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
{/* Duration */}
|
||||
<TableCell>{renderDuration(item.coupon)}</TableCell>
|
||||
|
||||
{accountCoupon && (
|
||||
<TableCell>
|
||||
<ToolTipLabel
|
||||
label={
|
||||
item.id === props.accountCouponId
|
||||
? "Remove coupon"
|
||||
: "This coupon cannot be removed"
|
||||
}
|
||||
>
|
||||
<DeleteCouponButton
|
||||
deleteCoupon={props.deleteCoupon}
|
||||
disabled={item.id !== props.accountCouponId}
|
||||
/>
|
||||
</ToolTipLabel>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
) : (
|
||||
<div className="px-6 py-4">
|
||||
<p className="flex min-h-[150px] items-center justify-center text-muted-foreground">
|
||||
{props.status === "pending" ? (
|
||||
<Spinner className="size-8" />
|
||||
) : props.status === "error" ? (
|
||||
<span className="text-destructive-text">
|
||||
Failed to load coupons
|
||||
</span>
|
||||
) : (
|
||||
<span>No coupons</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteCouponButton(props: {
|
||||
deleteCoupon: () => Promise<void>;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
const deleteCoupon = useMutation({
|
||||
mutationFn: props.deleteCoupon,
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={props.disabled}
|
||||
onClick={() => {
|
||||
const promise = deleteCoupon.mutateAsync();
|
||||
toast.promise(promise, {
|
||||
success: "Coupon removed successfully",
|
||||
error: "Failed to remove coupon",
|
||||
});
|
||||
}}
|
||||
aria-label="Remove coupon"
|
||||
>
|
||||
{deleteCoupon.isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<Trash2Icon className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const applyCouponFormSchema = z.object({
|
||||
promoCode: z.string().min(1, "Coupon code is required"),
|
||||
});
|
||||
|
||||
function ApplyCouponModalContent(props: {
|
||||
isPaymentSetup: boolean;
|
||||
applyCoupon: ApplyCouponFn;
|
||||
accountCoupon: CouponData | undefined;
|
||||
}) {
|
||||
const applyCoupon = useMutation({
|
||||
mutationFn: props.applyCoupon,
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof applyCouponFormSchema>>({
|
||||
resolver: zodResolver(applyCouponFormSchema),
|
||||
defaultValues: {
|
||||
promoCode: "",
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(values: z.infer<typeof applyCouponFormSchema>) {
|
||||
try {
|
||||
const res = await applyCoupon.mutateAsync(values.promoCode);
|
||||
switch (res.status) {
|
||||
case 200: {
|
||||
toast.success("Coupon applied successfully");
|
||||
|
||||
break;
|
||||
}
|
||||
case 400: {
|
||||
toast.error("Coupon code is invalid");
|
||||
break;
|
||||
}
|
||||
case 401: {
|
||||
toast.error("You are not authorized to apply coupons", {
|
||||
description: "Login to dashboard and try again",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 409: {
|
||||
toast.error("Coupon already applied");
|
||||
break;
|
||||
}
|
||||
case 429: {
|
||||
toast.error("Too many coupons applied in a short period", {
|
||||
description: "Please try again after some time",
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
toast.error("Failed to apply coupon");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
toast.error("Failed to apply coupon");
|
||||
}
|
||||
}
|
||||
|
||||
const couponEnabled = props.isPaymentSetup && !props.accountCoupon;
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Apply Coupon</DialogTitle>
|
||||
{couponEnabled && (
|
||||
<DialogDescription>
|
||||
Enter coupon code to apply discounts or free trials
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
||||
<div className="h-4" />
|
||||
|
||||
{couponEnabled && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="promoCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Coupon Code</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className=""
|
||||
disabled={!props.isPaymentSetup}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!props.isPaymentSetup && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircleIcon className="size-5" />
|
||||
<AlertTitle>Payment method required</AlertTitle>
|
||||
<AlertDescription>
|
||||
A valid payment method must be added to apply a coupon.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{props.isPaymentSetup && props.accountCoupon && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircleIcon className="size-5" />
|
||||
<AlertTitle>Coupon already applied</AlertTitle>
|
||||
<AlertDescription>
|
||||
Remove coupon {`"${props.accountCoupon.coupon.name}"`} to apply
|
||||
a new coupon
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-end border-t bg-card p-6">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={applyCoupon.isPending || !couponEnabled}
|
||||
className="gap-2"
|
||||
>
|
||||
{applyCoupon.isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<TicketCheckIcon className="size-4" />
|
||||
)}
|
||||
Redeem
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { AlertCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { PlanInfoCard } from "../../../../app/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard";
|
||||
import { CouponSection } from "./CouponCard";
|
||||
import { CreditsInfoCard } from "./PlanCard";
|
||||
import { BillingPricing } from "./Pricing";
|
||||
import { Coupons } from "./SubscriptionCoupons/Coupons";
|
||||
|
||||
// TODO - move this in app router folder in other pr
|
||||
|
||||
@@ -68,7 +68,7 @@ export const Billing: React.FC<BillingProps> = ({
|
||||
/>
|
||||
|
||||
<CreditsInfoCard twAccount={twAccount} />
|
||||
<CouponSection teamId={team.id} isPaymentSetup={validPayment} />
|
||||
<Coupons teamId={team.id} isPaymentSetup={validPayment} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -41,6 +41,16 @@
|
||||
|
||||
/* Others */
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* Sidebar */
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 0 0% 4%;
|
||||
--sidebar-primary: 221 83% 54%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 0 0% 93%;
|
||||
--sidebar-accent-foreground: 0 0% 9%;
|
||||
--sidebar-border: 0 0% 85%;
|
||||
--sidebar-ring: 0 0% 80%;
|
||||
}
|
||||
|
||||
.dark,
|
||||
@@ -78,6 +88,16 @@
|
||||
--active-border: 0 0% 22%;
|
||||
--ring: 0 0% 30%;
|
||||
--input: 0 0% 15%;
|
||||
|
||||
/* sidebar */
|
||||
--sidebar-background: 0 0% 0%;
|
||||
--sidebar-foreground: 0 0% 98%;
|
||||
--sidebar-primary: 221 83% 54%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 0 0% 11%;
|
||||
--sidebar-accent-foreground: 0 0% 98%;
|
||||
--sidebar-border: 0 0% 15%;
|
||||
--sidebar-ring: 0 0% 30%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,16 @@ module.exports = {
|
||||
link: {
|
||||
foreground: "hsl(var(--link-foreground))",
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: "hsl(var(--sidebar-background))",
|
||||
foreground: "hsl(var(--sidebar-foreground))",
|
||||
primary: "hsl(var(--sidebar-primary))",
|
||||
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
||||
accent: "hsl(var(--sidebar-accent))",
|
||||
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
||||
border: "hsl(var(--sidebar-border))",
|
||||
ring: "hsl(var(--sidebar-ring))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
|
||||
@@ -16,11 +16,7 @@ thirdweb does not charge any fees for buy with fiat. Any fees on onramping are b
|
||||
</Details>
|
||||
|
||||
<Details summary="What are the fees for Buy With Crypto?">
|
||||
For buying with crypto, thirdweb charges 0.3% and the application developer may charge up to 0.7% on each transaction
|
||||
</Details>
|
||||
|
||||
<Details summary="How does fee sharing work for Buy With Crypto?">
|
||||
thirdweb shares 70% of all fees collected through Buy With Crypto with our developers. Fee sharing is not currently available for Buy With Fiat transactions.
|
||||
For buying with crypto or swap fees, thirdweb charges a 0.3% protocol fee. Developers may monetize their application by charging additional fees set through the dashboard.
|
||||
</Details>
|
||||
|
||||
<Details summary="What does the term “network fees” refer to?">
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { createMetadata, DocImage } from "@doc";
|
||||
import paySettingsImage from "../assets/pay-settings.png";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "thirdweb Universal Bridge - Fee Sharing",
|
||||
icon: "thirdweb",
|
||||
},
|
||||
title: "thirdweb Universal Bridge - Fee Sharing | thirdweb",
|
||||
description: "How to participate in fee sharing with thirdweb Universal Bridge",
|
||||
});
|
||||
|
||||
# Fee Sharing
|
||||
|
||||
thirdweb collects a 1% fee per end user transaction. We share 70% of this fee with you.
|
||||
|
||||
For example, if a user purchases $100 worth of Polygon through your application, thirdweb charges a total of $1.00. thirdweb collects $0.30 and sends $0.70 to you.
|
||||
|
||||
Plase note that fee sharing only applies to fees collected through swaps and bridges. Fiat purchases do not currently qualify for fee sharing.
|
||||
|
||||
To participate in fee sharing, all you need to do is set a recipient address in your dashboard. Your recipient address can be configured in your dashboard by navigating to your Team > Project > Connect > Universal Bridge in [thirdweb dashboard](https://www.thirdweb.com/team).
|
||||
|
||||
<DocImage src={paySettingsImage} />
|
||||
@@ -36,7 +36,7 @@ In this guide, we'll show you how to purchase 0.01 Base ETH from USD in Typescri
|
||||
<Step title='Install the Connect SDK'>
|
||||
<InstallTabs
|
||||
npm="npm i thirdweb"
|
||||
yarn="yarn install thirdweb"
|
||||
yarn="yarn add thirdweb"
|
||||
pnpm="pnpm i thirdweb"
|
||||
/>
|
||||
<Step title='Get Your Client ID'>
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import {
|
||||
createMetadata,
|
||||
Callout,
|
||||
DocImage,
|
||||
InstallTabs,
|
||||
Steps,
|
||||
Step,
|
||||
} from "@doc";
|
||||
import OnrampStepOne from "../../assets/avax-to-usd.png";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "thirdweb Universal Bridge - Cross-Chain Swapping",
|
||||
icon: "thirdweb",
|
||||
},
|
||||
title: "thirdweb Universal Bridge - Cross-Chain Swapping | thirdweb",
|
||||
description:
|
||||
"Learn how to build a custom cross-chain swapping experience with thirdweb Universal Bridge.",
|
||||
});
|
||||
|
||||
# Leverage Cross-Chain Swaps with Universal Bridge
|
||||
|
||||
Learn how to enable your users to swap from any asset to any other with thirdweb's Universal Bridge.
|
||||
|
||||
In this guide, we'll show you how to purchase 10 USDC on Optimism in Typescript.
|
||||
|
||||
---
|
||||
|
||||
<Steps>
|
||||
<Step title='Install the Connect SDK'>
|
||||
<InstallTabs
|
||||
npm="npm i thirdweb"
|
||||
yarn="yarn add thirdweb"
|
||||
pnpm="pnpm i thirdweb"
|
||||
/>
|
||||
<Step title='Get Your Client ID'>
|
||||
|
||||
Log in to the [thirdweb dashboard](https://thirdweb.com/team). Click on Create New > Project to get your **Client ID**. You'll need your Client ID to interact with the Connect SDK.
|
||||
|
||||
</Step>
|
||||
</Step>
|
||||
<Step title='Find available routes'>
|
||||
Before your user can select which tokens they'd like to swap, they'll need to find available routes.
|
||||
|
||||
You can do this using the `routes` function in our `Bridge` namespace. You or your users can filter by origin and destination chains and/or tokens. Pagination is also built-in to the function.
|
||||
|
||||
```tsx
|
||||
import { Bridge, NATIVE_TOKEN_ADDRESS } from "thirdweb";
|
||||
|
||||
// Get all available routes
|
||||
const allRoutes = await Bridge.routes({
|
||||
client: thirdwebClient,
|
||||
});
|
||||
|
||||
// Filter routes for a specific token or chain
|
||||
const filteredRoutes = await Bridge.routes({
|
||||
originChainId: 1, // From Ethereum
|
||||
originTokenAddress: NATIVE_TOKEN_ADDRESS,
|
||||
destinationChainId: 10, // To Optimism
|
||||
client: thirdwebClient,
|
||||
});
|
||||
|
||||
// Paginate through routes
|
||||
const paginatedRoutes = await Bridge.routes({
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
client: thirdwebClient,
|
||||
});
|
||||
```
|
||||
|
||||
This will return an array of `Route` objects, which will include information such as `symbol`, `address`, and `chainId` for both the origin and destination tokens.
|
||||
|
||||
</Step>
|
||||
<Step title='Get a quote'>
|
||||
Once you know which routes are available, you can retrieve a quote to show the user how much they can expect to pay for a given swap.
|
||||
|
||||
In this example, we'll use the `Buy.quote` function to get a quote for buying 10 USDC on Optimism for Base ETH.
|
||||
|
||||
<Callout variant="info">
|
||||
The `Buy` namespace is purpose-built for when you want to obtain a specific amount of the output token.
|
||||
If you have a specific input amount and are flexible on the output amount, you can use the `Sell` namespace.
|
||||
Learn more about sells [here](https://portal.thirdweb.com/references/typescript/v5/sell/prepare).
|
||||
</Callout>
|
||||
|
||||
Quote allows us to get an expected amount before the user has connected their wallet. This quote won't come with executable transactions, and won't be a guaranteed price.
|
||||
|
||||
|
||||
```tsx
|
||||
import { Bridge, NATIVE_TOKEN_ADDRESS } from "thirdweb";
|
||||
|
||||
const buyQuote = await Bridge.Buy.quote({
|
||||
originChainId: 8453, // Base
|
||||
originTokenAddress: NATIVE_TOKEN_ADDRESS,
|
||||
destinationChainId: 10, // Optimism
|
||||
destinationTokenAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // Optimism USDC
|
||||
buyAmountWei: 10000000n, // 10 USDC
|
||||
client: thirdwebClient,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`To get ${buyQuote.destinationAmount} wei on destination chain, you need to pay ${buyQuote.originAmount} wei`,
|
||||
);
|
||||
```
|
||||
|
||||
This will return a `Quote` object, which will include the `originAmount` and `destinationAmount` in wei, along with some more useful information about the predicted quote.
|
||||
</Step>
|
||||
<Step title='Get the prepared transaction'>
|
||||
Now that we know how much the user can expect to pay, we can have them connect their wallet and execute the swap.
|
||||
|
||||
To get a prepared quote, we'll use the `Buy.prepare` function. The key difference with this function is it requires a `sender` and `receiver` to be specified.
|
||||
|
||||
```tsx
|
||||
import { Bridge, NATIVE_TOKEN_ADDRESS } from "thirdweb";
|
||||
|
||||
const preparedBuy = await Bridge.Buy.prepare({
|
||||
originChainId: 8453, // Base
|
||||
originTokenAddress: NATIVE_TOKEN_ADDRESS,
|
||||
destinationChainId: 10, // Optimism
|
||||
destinationTokenAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // Optimism USDC
|
||||
buyAmountWei: 10000000n, // 10 USDC
|
||||
sender: "0x...", // Your user's wallet address
|
||||
receiver: "0x...", // Recipient address (can be the same as sender)
|
||||
client: thirdwebClient,
|
||||
});
|
||||
|
||||
// The prepared quote contains the transactions you need to execute
|
||||
console.log(`Transactions to execute: ${preparedBuy.transactions.length}`);
|
||||
```
|
||||
|
||||
This will return a `PreparedQuote` object. It will look very similar to the `Quote` you received in the previous step, but it will include a `transactions` array. This array will contain the transactions you need to execute to complete the swap.
|
||||
</Step>
|
||||
<Step title='Execute the swap'>
|
||||
To execute the swap, we'll need to send all transactions in the `transactions` array one after the other.
|
||||
|
||||
<Callout variant="warning">
|
||||
Currently, the `transactions` array does not include approvals. You'll need to execute any necessary approvals prior to executing the swap transactions.
|
||||
</Callout>
|
||||
|
||||
```tsx
|
||||
import { sendTransaction, waitForReceipt } from "thirdweb";
|
||||
|
||||
const preparedBuy = await Bridge.Buy.prepare({
|
||||
originChainId: 8453, // Base
|
||||
originTokenAddress: NATIVE_TOKEN_ADDRESS,
|
||||
destinationChainId: 10, // Optimism
|
||||
destinationTokenAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // Optimism USDC
|
||||
buyAmountWei: 10000000n, // 10 USDC
|
||||
sender: "0x...", // Your user's wallet address
|
||||
receiver: "0x...", // Recipient address (can be the same as sender)
|
||||
client: thirdwebClient,
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
for (const transaction of preparedBuy.transactions) {
|
||||
const tx = prepareTransaction({
|
||||
to: transaction.to as string,
|
||||
value: BigInt(transaction.value ?? 0n),
|
||||
data: transaction.data,
|
||||
chain: defineChain(transaction.chainId),
|
||||
client
|
||||
});
|
||||
|
||||
const result = await sendAndConfirmTransaction({ transaction: tx, account });
|
||||
let swapStatus;
|
||||
do {
|
||||
swapStatus = await Bridge.status({
|
||||
transactionHash: result.transactionHash,
|
||||
client,
|
||||
});
|
||||
} while (swapStatus.status !== "COMPLETED");
|
||||
};
|
||||
```
|
||||
|
||||
<Callout variant="info">
|
||||
The returned transactions follow the [Ox](https://oxlib.sh/) standard [TransactionEnvelopeEip1559](https://oxlib.sh/guides/transaction-envelopes)
|
||||
format. This is a simple transaction with `data`, `to`, `value`, and `chainId` properties. **Gas is not included in the transaction, and should
|
||||
be estimated separately if necessary.** Normally, the thirdweb SDK will handle this for you.
|
||||
</Callout>
|
||||
|
||||
</Step>
|
||||
<Step title='Getting the swap status'>
|
||||
You'll notice in the previous step we call `Bridge.status` to get the status of the swap. We can get the status of any past swap using just the transaction hash and chain ID of its origin transaction.
|
||||
|
||||
When sending the transactions in a prepared quote, you **must** use `Bridge.status` to get the `COMPLETED` status before moving on to the next transaction. This is because `status` waits for both the origin
|
||||
and destination transactions to complete. Waiting for the origin transaction receipt is not sufficient since the funds might not have arrived on the destination chain yet.
|
||||
|
||||
The `status` will also return all transactions (origin and destination) involved in the swap.
|
||||
```tsx
|
||||
import { Bridge } from "thirdweb";
|
||||
|
||||
// Check the status of a bridge transaction
|
||||
const bridgeStatus = await Bridge.status({
|
||||
transactionHash:
|
||||
"0xe199ef82a0b6215221536e18ec512813c1aa10b4f5ed0d4dfdfcd703578da56d",
|
||||
chainId: 8453, // The chain ID where the transaction was initiated
|
||||
client: thirdwebClient,
|
||||
});
|
||||
|
||||
// The status will be one of: "COMPLETED", "PENDING", "FAILED", or "NOT_FOUND"
|
||||
if (bridgeStatus.status === "completed") {
|
||||
console.log(`
|
||||
Bridge completed!
|
||||
Sent: ${bridgeStatus.originAmount} wei on chain ${bridgeStatus.originChainId}
|
||||
Received: ${bridgeStatus.destinationAmount} wei on chain ${bridgeStatus.destinationChainId}
|
||||
`);
|
||||
} else if (bridgeStatus.status === "pending") {
|
||||
console.log("Bridge transaction is still pending...");
|
||||
} else {
|
||||
console.log("Bridge transaction failed");
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createMetadata, DocImage, Grid, SDKCard, FeatureCard } from "@doc";
|
||||
import PayOverviewImage from "../assets/pay-overview.png";
|
||||
import SupportedChains from "../../../_images/supported-chains.png";
|
||||
import {Rocket, ArrowLeftRight, Wallet, Pencil, ShieldCheck, PiggyBank, Globe, Component} from "lucide-react";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
@@ -14,7 +15,7 @@ export const metadata = createMetadata({
|
||||
|
||||
# Overview
|
||||
|
||||
Universal Bridge allows your users to bridge, swap, and purchase cryptocurrencies and execute transactions with any fiat options or tokens via cross-chain routing.
|
||||
Universal Bridge allows your users to bridge, swap, and purchase cryptocurrencies and execute transactions with any fiat options or tokens via cross-chain routing. It enables users to purchase or complete in-app transactions with any token they hold.
|
||||
|
||||
<DocImage src={PayOverviewImage} />
|
||||
|
||||
@@ -24,46 +25,46 @@ Universal Bridge allows your users to bridge, swap, and purchase cryptocurrencie
|
||||
className="my-4 grid gap-2 md:grid-cols-2 lg:grid-cols-2 "
|
||||
>
|
||||
<FeatureCard
|
||||
title="Earn money"
|
||||
description="Receive 70% of fees on each crypto-to-crypto transaction"
|
||||
iconUrl="/icons/feature-cards/earn-money.svg"
|
||||
title="Avoid chargeback & liability"
|
||||
description="Automatic screening for sanctioned wallets and regions to ensure application safety."
|
||||
iconUrl={<Rocket />}
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
title="Onboard users"
|
||||
description="Fiat on-ramps to enable purchases with traditional payment methods"
|
||||
iconUrl="/icons/feature-cards/onboard-users.svg"
|
||||
title="Global Coverage"
|
||||
description="Bridging support in over 160+ countries to reach a truly global audience."
|
||||
iconUrl={<Globe/>}
|
||||
/>
|
||||
<FeatureCard
|
||||
title="Enable in-app purchases"
|
||||
description="Purchase crypto without leaving the application"
|
||||
iconUrl="/icons/feature-cards/in-app-purchase.svg"
|
||||
title="Earn revenue"
|
||||
description="Monetize your application and earn swap fees on each transaction."
|
||||
iconUrl={<PiggyBank />}
|
||||
/>
|
||||
<FeatureCard
|
||||
title="Chain Coverage"
|
||||
description="Support purchases on over 20+ widely used EVM chains"
|
||||
iconUrl="/icons/feature-cards/chain-coverage.svg"
|
||||
title="Transact Securely"
|
||||
description="Strict one-time approvals ensuring funds are not at risk of exploits."
|
||||
iconUrl={<ShieldCheck />}
|
||||
/>
|
||||
<FeatureCard
|
||||
title="Liquidity Aggregation"
|
||||
description="Ensure every transaction is possible"
|
||||
iconUrl="/icons/feature-cards/liquidity-aggregation.svg"
|
||||
title="Easy-to-integrate Components"
|
||||
description="Easily integrate components for buying funds, e-commerce checkouts, and more."
|
||||
iconUrl={<Component />}
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
title="Global Coverage"
|
||||
description="Support for over 130+ countries"
|
||||
iconUrl="/icons/feature-cards/global-coverage.svg"
|
||||
title="Customizable Flows"
|
||||
description="Create your own customized headless flows using the Universal Bridge SDK or API."
|
||||
iconUrl={<Pencil/>}
|
||||
/>
|
||||
<FeatureCard
|
||||
title="Integration Options"
|
||||
description="Use pre-built modals or customize the transaction experience"
|
||||
iconUrl="/icons/feature-cards/integration-options.svg"
|
||||
title="Flexible Wallet Payments"
|
||||
description="Enable payments with any wallet that has funds not just the one connected."
|
||||
iconUrl= {<Wallet />}
|
||||
/>
|
||||
<FeatureCard
|
||||
title="Secure"
|
||||
description="Strict one-time approvals mean user funds are not at risk of smart contract allowance exploits"
|
||||
iconUrl="/icons/feature-cards/security.svg"
|
||||
title="Multi-hop Routing"
|
||||
description="Handle multiple swaps at once within one session for a seamless swap experience."
|
||||
iconUrl={<ArrowLeftRight />}
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -74,30 +75,20 @@ Universal Bridge is supported on select EVM compatible chains. To view the full
|
||||
|
||||
<DocImage src={SupportedChains} />
|
||||
|
||||
## Fee Sharing
|
||||
|
||||
Thirdweb collects a 1% fee per end-user transaction and shares 70% of that fee with you. For example, on a $100 Polygon purchase, a $1.00 fee is charged—of which $0.30 is retained by thirdweb and $0.70 is remitted to you.
|
||||
|
||||
Note: Fee sharing applies only to fees from swaps and bridges; fiat purchases are excluded.
|
||||
|
||||
To enable fee sharing, simply set a recipient address in your dashboard under Team > Project > Connect > Universal Bridge in the thirdweb dashboard.
|
||||
|
||||
## Onramp providers
|
||||
## Onramp Providers
|
||||
|
||||
| Provider | Supported Countries |
|
||||
| -------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| Transak | https://transak.notion.site/On-Ramp-Payment-Methods-Fees-Other-Details-b0761634feed4b338a69f4f186d906a5 |
|
||||
| Kado | https://www.kado.money/supported-countries |
|
||||
| Stripe | https://docs.stripe.com/crypto/onramp |
|
||||
|
||||
Specify a preferred onramp provider by setting the preferredProvider parameter in ConnectButton, PayEmbed, or Headless Integration. For details, refer to
|
||||
[customization](../pay/customization/connectbutton#preferredProvider) or the [API Reference for preferredProvider](/references/typescript/v5/GetBuyWithFiatQuoteParams).
|
||||
| Coinbase | https://docs.cdp.coinbase.com/onramp/docs/payment-methods/ |
|
||||
|
||||
## Integration Options
|
||||
|
||||
| OPTION | BEST FOR |
|
||||
| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [ConnectButton](/connect/pay/get-started#option-1-connectbutton) | Developers who want out-of-the-box support complete with wallet and social login. |
|
||||
| [PayEmbed](/connect/pay/get-started#option-2-embed-pay) | Developers who want onramp and crypto purchase experiences directly in their application. |
|
||||
| [Transaction Flow (sendTransaction)](/connect/pay/get-started#option-3-send-a-transaction-with-pay) | Developers who want users to onramp or purchase crypto directly into a transaction. Great for minting and NFT purchase flows. |
|
||||
| [Custom Universal Bridge Experience](/connect/pay/build-a-custom-experience) | Developers who want full customization over every step of the onramp process, from displaying quotes to polling for transaction statuses. |
|
||||
| [Buy Crypto](https://playground.thirdweb.com/connect/pay/) | Developers who want onramp and crypto purchase experiences directly in their application. |
|
||||
| [Transactions](https://playground.thirdweb.com/connect/pay/transactions) | Developers who want users to onramp or purchase crypto directly into a transaction. Great for minting and NFT purchase flows. |
|
||||
| [In-App Purchases](https://playground.thirdweb.com/connect/pay/commerce) | Developers who want to take payments from Fiat or Crypto directly to a seller wallet |
|
||||
| [Headless](https://playground.thirdweb.com/connect/pay/backend) | Developers who prefer a headless customized flow using APIs. |
|
||||
|
||||
@@ -9,7 +9,7 @@ export const metadata = createMetadata({
|
||||
|
||||
# Webhooks
|
||||
|
||||
Pay can be configured to send webhook events to notify your application any time an event happens on your transaction. Pay sends a response, via a HTTP request, to any endpoint URLs that you have provided us in your Team > Project > Connect > Pay > Webhooks page in [thirdweb dashboard](https://thirdweb.com/team).
|
||||
Universal Bridge can be configured to send webhook events to notify your application any time an event happens on your transaction. Universal Bridge sends a response, via a HTTP request, to any endpoint URLs that you have provided us in your Team > Project > Connect > Universal Bridge > Webhooks page in [thirdweb dashboard](https://thirdweb.com/team).
|
||||
|
||||
## Events
|
||||
|
||||
|
||||
@@ -314,9 +314,13 @@ export const sidebar: SideBar = {
|
||||
href: `${paySlug}/guides/accept-direct-payments`,
|
||||
},
|
||||
{
|
||||
name: "Build a Custom Experience",
|
||||
name: "Build a Custom Onramp Experience",
|
||||
href: `${paySlug}/guides/build-a-custom-experience`,
|
||||
},
|
||||
{
|
||||
name: "Cross-Chain Swapping",
|
||||
href: `${paySlug}/guides/cross-chain-swapping`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Network,
|
||||
Rocket,
|
||||
StickyNote,
|
||||
Webhook,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
|
||||
@@ -58,6 +59,33 @@ export const sidebar: SideBar = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Webhooks",
|
||||
href: `${insightSlug}/webhooks`,
|
||||
icon: <Webhook />,
|
||||
links: [
|
||||
{
|
||||
name: "Getting Started",
|
||||
href: `${insightSlug}/webhooks`,
|
||||
},
|
||||
{
|
||||
name: "Managing Webhooks",
|
||||
href: `${insightSlug}/webhooks/managing-webhooks`,
|
||||
},
|
||||
{
|
||||
name: "Filtering",
|
||||
href: `${insightSlug}/webhooks/filtering`,
|
||||
},
|
||||
{
|
||||
name: "Payload",
|
||||
href: `${insightSlug}/webhooks/payload`,
|
||||
},
|
||||
{
|
||||
name: "API Reference",
|
||||
href: "https://insight-api.thirdweb.com/reference#tag/webhooks",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "API Reference",
|
||||
href: "https://insight-api.thirdweb.com/reference",
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Insight Webhooks | thirdweb Infrastructure",
|
||||
description: "Filtering",
|
||||
image: {
|
||||
title: "Insight",
|
||||
icon: "insight",
|
||||
},
|
||||
});
|
||||
|
||||
# Filtering
|
||||
|
||||
## Webhook Topics
|
||||
|
||||
### `v1.events`
|
||||
|
||||
Subscribes to blockchain log events.
|
||||
|
||||
### `v1.transactions`
|
||||
|
||||
Subscribes to blockchain transactions.
|
||||
|
||||
## Webhook Filters
|
||||
|
||||
You can filter webhook notifications based on specific criteria.
|
||||
Each webhook must have either an events filter or a transactions filter (or both).
|
||||
|
||||
### Event Filters
|
||||
```typescript
|
||||
{
|
||||
"v1.events": {
|
||||
chain_ids: string[], // Filter by specific chains
|
||||
addresses: string[], // Filter by contract addresses
|
||||
signatures: { // Filter by event signatures
|
||||
sig_hash: string, // Event signature hash
|
||||
abi?: string, // Optional ABI for data decoding
|
||||
params?: Record<string, any> // Filter on decoded parameters
|
||||
}[]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Transaction Filters
|
||||
```typescript
|
||||
{
|
||||
"v1.transactions": {
|
||||
chain_ids: string[], // Filter by specific chains
|
||||
from_addresses: string[], // Filter by sender addresses
|
||||
to_addresses: string[], // Filter by recipient addresses
|
||||
signatures: { // Filter by function signatures
|
||||
sig_hash: string, // Function signature hash
|
||||
abi?: string, // Optional ABI for data decoding
|
||||
params?: string[] // Filter on decoded parameters
|
||||
}[]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ABIs
|
||||
|
||||
You can specify partial ABIs to have decoded data sent in the webhook payload. For this you also need to give the `sig_hash` of the event or function call.
|
||||
|
||||
The following example will filter for `Transfer` events on Ethereum for the contract `0x1f9840a85d5af5bf1d1762f925bdaddc4201f984`
|
||||
```typescript
|
||||
{
|
||||
...
|
||||
filters: {
|
||||
"v1.events": {
|
||||
chain_ids: ["1"],
|
||||
addresses: ["0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"],
|
||||
signatures: [
|
||||
{
|
||||
sig_hash:
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
abi: '{"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Transfer","type":"event"}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
And this example will filter for `Approve` function calls on Ethereum for the contract `0x1f9840a85d5af5bf1d1762f925bdaddc4201f984`
|
||||
```typescript
|
||||
{
|
||||
...
|
||||
filters: {
|
||||
"v1.transactions": {
|
||||
chain_ids: ["1"],
|
||||
addresses: ["0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"],
|
||||
signatures: [
|
||||
{
|
||||
sig_hash: "0x095ea7b3",
|
||||
abi: '{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"rawAmount","type":"uint256"}],"name":"approve","type":"function"}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Params
|
||||
|
||||
`params` on the `signatures` object will allow you to filter based on the decoded data. Only strict equality is supported at the moment.
|
||||
|
||||
### Notes
|
||||
- You can specify ABIs to receive decoded event/transaction data
|
||||
- Parameter filtering supports equality matching only
|
||||
- At least one filter criteria must be specified
|
||||
@@ -0,0 +1,68 @@
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Insight Webhooks | thirdweb Infrastructure",
|
||||
description: "Managing Insight webhooks",
|
||||
image: {
|
||||
title: "Insight",
|
||||
icon: "insight",
|
||||
},
|
||||
});
|
||||
|
||||
# Managing Webhooks
|
||||
|
||||
For most up to date technical spec refer to https://insight-api.thirdweb.com/reference#tag/webhooks.
|
||||
|
||||
## List Webhooks
|
||||
`GET /v1/webhooks`
|
||||
|
||||
Retrieve all webhooks for your project or get details for a specific webhook by ID.
|
||||
|
||||
## Create Webhook
|
||||
`POST /v1/webhooks`
|
||||
|
||||
Creates a new webhook subscription.
|
||||
It may take up to a minute to start working.
|
||||
|
||||
### Verifying the created webhook
|
||||
- The webhook starts in a suspended state
|
||||
- An OTP (One-Time Password) is sent to your webhook URL for verification
|
||||
- You must verify the webhook within 15 minutes using the OTP
|
||||
- Once verified, it may take up to a minute for the webhook to become fully active
|
||||
- You can request a new OTP if you can't retrieve the initial OTP
|
||||
|
||||
## Update Webhook
|
||||
`PATCH /v1/webhooks/:webhook_id`
|
||||
|
||||
You can modify the URL or filters of a webhook. Additionally you can enable and disable the webhook.
|
||||
Changes may take up to a minute to take effect.
|
||||
|
||||
### Updating webhook URL
|
||||
- If you update the webhook URL, you'll need to verify it again with a new OTP
|
||||
- Other fields can be updated without requiring re-verification
|
||||
|
||||
## Delete Webhook
|
||||
`DELETE /v1/webhooks/:webhook_id`
|
||||
|
||||
Permanently removes a webhook subscription. This action cannot be undone.
|
||||
|
||||
## Verify Webhook
|
||||
`POST /v1/webhooks/:webhook_id/verify`
|
||||
|
||||
Activates a webhook using the OTP code that was sent to your webhook URL. Required for:
|
||||
- New webhook creation
|
||||
- After updating a webhook URL
|
||||
|
||||
## Resend OTP
|
||||
`POST /v1/webhooks/:webhook_id/resend-otp`
|
||||
|
||||
Request a new OTP code if the original expires or is lost:
|
||||
- Invalidates the previous OTP
|
||||
- New OTP expires after 15 minutes
|
||||
- Only works for webhooks pending verification
|
||||
|
||||
## Test Webhook
|
||||
`POST /v1/webhooks/test`
|
||||
|
||||
Sends a sample payload signed with a test secret ('test123').
|
||||
Useful for testing your receiver endpoint before creating actual webhooks.
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Insight Webhooks | thirdweb Infrastructure",
|
||||
description: "Getting started with Insight webhooks",
|
||||
image: {
|
||||
title: "Insight",
|
||||
icon: "insight",
|
||||
},
|
||||
});
|
||||
|
||||
# Webhooks
|
||||
|
||||
Webhooks allow you to receive notifications when specific blockchain events or transactions occur. This enables you to automate workflows and keep your applications in sync with on-chain activity.
|
||||
|
||||
## About Webhooks
|
||||
|
||||
### Data Delivery
|
||||
Webhook events are collected and delivered in batches for optimal performance and reliability. This makes webhooks ideal for:
|
||||
- Tracking when specific blockchain events occur
|
||||
- Aggregating on-chain data
|
||||
- Building analytics and monitoring systems
|
||||
- Triggering downstream processes
|
||||
|
||||
### Delivery Guarantees
|
||||
Events are guaranteed to be delivered *at least once*.
|
||||
Your application should implement idempotency and deduplication logic using the unique event ID in the payload.
|
||||
Events may occasionally be delivered out of order.
|
||||
|
||||
### Performance Requirements
|
||||
- Your receiving endpoint must respond within 3 seconds
|
||||
- If your endpoint consistently fails to respond in time, the webhook will be automatically suspended
|
||||
- We recommend implementing a queue system if you need to perform time-consuming operations
|
||||
@@ -0,0 +1,202 @@
|
||||
import { createMetadata } from "@doc";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Insight Webhooks | thirdweb Infrastructure",
|
||||
description: "Payload",
|
||||
image: {
|
||||
title: "Insight",
|
||||
icon: "insight",
|
||||
},
|
||||
});
|
||||
|
||||
# Webhook Payload
|
||||
|
||||
## Payload Format
|
||||
```typescript
|
||||
{
|
||||
topic: string, // topic of the data
|
||||
timestamp: string, // timestamp of when the payload was sent in seconds
|
||||
data: [
|
||||
{
|
||||
data: object, // data of the event or transaction
|
||||
status: "new" | "reverted",
|
||||
type: "event" | "transaction",
|
||||
id: string // unique id of the data
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Example Response:
|
||||
|
||||
<Tabs defaultValue="events">
|
||||
|
||||
<TabsList>
|
||||
<TabsTrigger value="events">Events</TabsTrigger>
|
||||
<TabsTrigger value="transactions">Transactions</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value='events'>
|
||||
```json
|
||||
{
|
||||
"timestamp": 1743164112,
|
||||
"topic": "v1.events",
|
||||
"data": [
|
||||
{
|
||||
"data": {
|
||||
"chain_id": "1",
|
||||
"block_number": 22140383,
|
||||
"block_hash": "0x85bd4045d947c34b6b568fc84c7550c0efa741f71c834bbb3d3950e9da27842e",
|
||||
"block_timestamp": 1743104207,
|
||||
"transaction_hash": "0x460ced3718d0e09145eae8b63fd985dc366adfc4523c791116f24dc051e8363a",
|
||||
"transaction_index": 93,
|
||||
"log_index": 230,
|
||||
"address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
|
||||
"data": "0x000000000000000000000000000000000000000000000000009440c61e928cca",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x000000000000000000000000d360131b77ead72f1f23fb185b4896fe01dc8cb5",
|
||||
"0x00000000000000000000000085cd07ea01423b1e937929b44e4ad8c40bbb5e71"
|
||||
],
|
||||
"decoded": {
|
||||
"name": "Transfer",
|
||||
"indexed_params": {
|
||||
"from": "0xd360131b77ead72f1f23fb185b4896fe01dc8cb5",
|
||||
"to": "0x85cd07ea01423b1e937929b44e4ad8c40bbb5e71"
|
||||
},
|
||||
"non_indexed_params": {
|
||||
"amount": "41729516213800138"
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": "new",
|
||||
"type": "event",
|
||||
"id": "76b81da4ec46486f0a6e325596f506cad13ebd629520aded104efcaa37a419d5"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='transactions'>
|
||||
```json
|
||||
{
|
||||
"timestamp": 1743165030,
|
||||
"topic": "v1.transactions",
|
||||
"data": [
|
||||
{
|
||||
"data": {
|
||||
"chain_id": "1",
|
||||
"hash": "0xceba210be88785cac0884127cb42d370bcde8248e11e490af0deeb6ded2bb421",
|
||||
"nonce": 5,
|
||||
"block_hash": "0x86cfe1e32fb3e5091bc2533b78d1503a4539936f50ff5a33b09df639f1572fdd",
|
||||
"block_number": 22145183,
|
||||
"block_timestamp": 1743162179,
|
||||
"transaction_index": 62,
|
||||
"from_address": "0xcd1785a6a49748948059d3c27d8f55ad0ef94c8c",
|
||||
"to_address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
|
||||
"value": "0",
|
||||
"gas": 50000,
|
||||
"gas_price": "2000000000",
|
||||
"data": "0xa9059cbb00000000000000000000000028c6c06298d514db089934071355e5743bf21d60000000000000000000000000000000000000000000000024ac7a2466fe6d4000",
|
||||
"function_selector": "0xa9059cbb",
|
||||
"max_fee_per_gas": "2000000000",
|
||||
"max_priority_fee_per_gas": "2000000000",
|
||||
"transaction_type": 2,
|
||||
"r": "93121701149456501424579980283392650303695688563543540003926043202730906996706",
|
||||
"s": "5712073776861917154895012394334633911928305143620822626852008432451821415527",
|
||||
"v": "1",
|
||||
"access_list_json": "[]",
|
||||
"contract_address": null,
|
||||
"gas_used": 35318,
|
||||
"cumulative_gas_used": 9124496,
|
||||
"effective_gas_price": "2000000000",
|
||||
"blob_gas_used": 0,
|
||||
"blob_gas_price": "0",
|
||||
"logs_bloom": "0x40000000000000000000000000000000000000000020000000000000000000000000000000000000000001000000001000008000000000000000000000000000000000000000000000000008100000000000000000000000000000000000000008000000000000000000000000000000000000000000000002000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
"status": 1,
|
||||
"decoded": {
|
||||
"name": "transfer",
|
||||
"inputs": {
|
||||
"dst": "0x28c6c06298d514db089934071355e5743bf21d60",
|
||||
"rawAmount": "676511072800000000000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": "new",
|
||||
"type": "transaction",
|
||||
"id": "e922999aff6625e1e26e6eca478b2c6ece33a5b8cf3a3c8bde96c00da8d2acc0"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
## Headers
|
||||
Each webhook request includes:
|
||||
- `x-webhook-id` - ID of the webhook configuration to know which one triggered the send
|
||||
- `x-webhook-signature` - hash of the payload signed with the webhook's secret. Used to validate the payload
|
||||
|
||||
## Signature Verification (highly recommended)
|
||||
|
||||
To verify that the request is from thirdweb webhooks it is highly recommended to verify the payload.
|
||||
Each webhook has a `webhook_secret` which is used to sign the raw payload and is then attached to the headers.
|
||||
|
||||
To verify the webhook:
|
||||
```typescript
|
||||
const generateSignature = (
|
||||
rawBody: string,
|
||||
secret: string,
|
||||
): string => {
|
||||
return crypto
|
||||
.createHmac("sha256", secret)
|
||||
.update(rawBody)
|
||||
.digest("hex");
|
||||
};
|
||||
|
||||
const isValidSignature = (
|
||||
rawBody: string,
|
||||
signature: string,
|
||||
secret: string,
|
||||
): boolean => {
|
||||
const expectedSignature = generateSignature(
|
||||
body,
|
||||
secret,
|
||||
);
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(expectedSignature),
|
||||
Buffer.from(signature),
|
||||
);
|
||||
};
|
||||
|
||||
// extract the signature from request headers
|
||||
const signature = req.headers['x-signature'];
|
||||
// get the raw body of the request
|
||||
const rawBody = req.rawBody;
|
||||
|
||||
const isValid = isValidSignature(rawBody, signature, secret);
|
||||
```
|
||||
|
||||
**You need to use the raw request body when verifying webhooks, as the cryptographic signature is sensitive to even the slightest changes. You should watch out for frameworks that parse the request as JSON and then stringify it because this too will break the signature verification.**
|
||||
|
||||
## Payload Age
|
||||
|
||||
You can optionally also verify the age of the payload.
|
||||
After you have verified the signature, you can be sure the timestamp in the payload is correct and apply any time limit you wish.
|
||||
|
||||
```typescript
|
||||
export const isExpired = (
|
||||
timestamp: string,
|
||||
expirationInSeconds: number,
|
||||
): boolean => {
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
return currentTime - parseInt(timestamp) > expirationInSeconds;
|
||||
};
|
||||
```
|
||||
|
||||
## Reorged Data
|
||||
If a blockchain reorganization occurs:
|
||||
- You'll receive an event with `status: "reverted"` instead of `status: "new"`
|
||||
- You should handle this by reverting any actions taken for the original event
|
||||
@@ -62,9 +62,9 @@ export default function RootLayout({
|
||||
<StickyTopContainer>
|
||||
{/* Note: Please change id as well when changing text or href so that new banner is shown to user even if user dismissed the older one */}
|
||||
<Banner
|
||||
id="nebula-alpha"
|
||||
text="Introducing Nebula - the most powerful AI to interact with the blockchain. Join the waitlist."
|
||||
href="https://thirdweb.com/nebula"
|
||||
id="ub-launch"
|
||||
text="Let users pay with whatever they have without leaving your app"
|
||||
href="https://thirdweb.com/connect/universal-bridge"
|
||||
/>
|
||||
<Header />
|
||||
</StickyTopContainer>
|
||||
|
||||
@@ -20,6 +20,24 @@ Nebula supports reading and writing capabilities on any EVM-compatible chain. [V
|
||||
Yes, you may ask Nebula questions in any spoken language.
|
||||
</Details>
|
||||
|
||||
<Details summary="How can I make Nebula play a certain character better?">
|
||||
1. Define the Role Clearly: Specify the character’s role and behavior in your prompt.
|
||||
|
||||
Example, “You are an assistant embedded in a blockchain application, designed to provide insights and execute transactions without introducing yourself.”
|
||||
|
||||
2. Avoid phrases that suggest personality traits or self-awareness: Use Contextual Instructions and add specific instructions to avoid unnecessary introductions.
|
||||
|
||||
Example, “Respond directly to queries without stating your identity or greeting users.”
|
||||
|
||||
3. Set Output Expectations: Guide the format of responses.
|
||||
|
||||
Example, “Provide concise answers or actions based on the user’s input without additional commentary.”
|
||||
|
||||
4. Preload Context: Use Nebula’s context filters to scope responses within a specific domain (e.g., blockchain data or transactions) and avoid generic replies.
|
||||
|
||||
5. Session Handling: Utilize Nebula’s session handling feature to maintain relevant context throughout interactions, ensuring responses align with the established character behavior.
|
||||
</Details>
|
||||
|
||||
<Details summary="How can I improve upon Nebula's responses?">
|
||||
We recommend using [context filters](/nebula/key-concepts/context-filters) to improve relevant responses. You may also modify hyperparameters such as temperature, top-p,
|
||||
and more using the OpenAI API. Learn more about our [OpenAI API integration](/nebula/plugins/openai).
|
||||
|
||||
@@ -167,6 +167,11 @@ export function getSidebarLinkGroups(doc: TransformedDoc, path: string) {
|
||||
return tag === "@modules";
|
||||
});
|
||||
|
||||
const bridge = docs.filter((d) => {
|
||||
const [tag] = getCustomTag(d) || [];
|
||||
return tag === "@bridge";
|
||||
});
|
||||
|
||||
// sort extensions into their own groups
|
||||
if (extensions.length) {
|
||||
const extensionGroups = extensions.reduce(
|
||||
@@ -247,9 +252,68 @@ export function getSidebarLinkGroups(doc: TransformedDoc, path: string) {
|
||||
}
|
||||
}
|
||||
|
||||
if (bridge.length) {
|
||||
const bridgeGroups = bridge.reduce(
|
||||
(acc, d) => {
|
||||
const [, moduleName] = getCustomTag(d) || [];
|
||||
if (moduleName) {
|
||||
if (!acc[moduleName]) {
|
||||
acc[moduleName] = [];
|
||||
}
|
||||
acc[moduleName]?.push(d);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, SomeDoc[]>,
|
||||
);
|
||||
const bridgeLinkGroups: {
|
||||
name: string;
|
||||
href: string;
|
||||
links?: { name: string; href?: string }[];
|
||||
}[] = Object.entries(bridgeGroups)
|
||||
.filter(([namespaceName]) => namespaceName.toLowerCase() !== "common")
|
||||
.map(([namespaceName, docs]) => {
|
||||
const links = docs.map((d) => ({
|
||||
name: d.name,
|
||||
href: getLink(`${path}/${namespaceName.toLowerCase()}/${d.name}`),
|
||||
}));
|
||||
return {
|
||||
name: namespaceName,
|
||||
href: "",
|
||||
links,
|
||||
};
|
||||
});
|
||||
|
||||
// Add the top-level functions
|
||||
for (const group of Object.entries(bridgeGroups).filter(
|
||||
([namespaceName]) => namespaceName.toLowerCase() === "common",
|
||||
)) {
|
||||
const docs = group[1];
|
||||
for (const doc of docs) {
|
||||
bridgeLinkGroups.push({
|
||||
name: doc.name,
|
||||
href: getLink(`${path}/${doc.name}`),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!linkGroups.find((group) => group.name === name)) {
|
||||
linkGroups.push({
|
||||
name: name,
|
||||
href: getLink(`${path}/${key}`),
|
||||
links: [{ name: "Universal Bridge", links: bridgeLinkGroups }],
|
||||
isCollapsible: false,
|
||||
});
|
||||
} else {
|
||||
linkGroups
|
||||
.find((group) => group.name === name)
|
||||
?.links.push({ name: "Universal Bridge", links: bridgeLinkGroups });
|
||||
}
|
||||
}
|
||||
|
||||
const nonExtensions = docs.filter((d) => {
|
||||
const [tag] = getCustomTag(d) || [];
|
||||
return tag !== "@extension" && tag !== "@modules";
|
||||
return tag !== "@extension" && tag !== "@modules" && tag !== "@bridge";
|
||||
});
|
||||
|
||||
// sort into groups
|
||||
|
||||
@@ -36,11 +36,17 @@ export function getSlugToDocMap(doc: TransformedDoc) {
|
||||
const extensionBlockTag = v.signatures
|
||||
?.find((s) =>
|
||||
s.blockTags?.some(
|
||||
(tag) => tag.tag === "@extension" || tag.tag === "@modules",
|
||||
(tag) =>
|
||||
tag.tag === "@extension" ||
|
||||
tag.tag === "@modules" ||
|
||||
tag.tag === "@bridge",
|
||||
),
|
||||
)
|
||||
?.blockTags?.find(
|
||||
(tag) => tag.tag === "@extension" || tag.tag === "@modules",
|
||||
(tag) =>
|
||||
tag.tag === "@extension" ||
|
||||
tag.tag === "@modules" ||
|
||||
tag.tag === "@bridge",
|
||||
);
|
||||
|
||||
if (extensionBlockTag) {
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.93.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6584](https://github.com/thirdweb-dev/js/pull/6584) [`3d7c009`](https://github.com/thirdweb-dev/js/commit/3d7c009d72af4a0f84405d916301e0660600c191) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix CLI secret key handling
|
||||
|
||||
## 5.93.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6578](https://github.com/thirdweb-dev/js/pull/6578) [`2a1f4be`](https://github.com/thirdweb-dev/js/commit/2a1f4be48b41d406e86203a4fc00d8bc687cb795) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Fix bug with PayEmbed incorrectly showing failed state
|
||||
|
||||
- [#6580](https://github.com/thirdweb-dev/js/pull/6580) [`c3d0ea0`](https://github.com/thirdweb-dev/js/commit/c3d0ea005959d5b65c5af3f2043a93f62207e770) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - respect supportedTokens prop in PayEmbed
|
||||
|
||||
- [#6495](https://github.com/thirdweb-dev/js/pull/6495) [`b49c191`](https://github.com/thirdweb-dev/js/commit/b49c1913553b0d0cc73ab74359db21896a8bcb1f) Thanks [@kumaryash90](https://github.com/kumaryash90)! - Support Stylus contracts in CLI and SDK
|
||||
|
||||
## 5.93.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6569](https://github.com/thirdweb-dev/js/pull/6569) [`2d4c5ff`](https://github.com/thirdweb-dev/js/commit/2d4c5ff07c5c84d3705a35a01990d598939e2e44) Thanks [@MananTank](https://github.com/MananTank)! - Fix "Cannot read properties of undefined (reading 'forwardRef')" error in certain bundler setups
|
||||
|
||||
## 5.93.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "thirdweb",
|
||||
"version": "5.93.5",
|
||||
"version": "5.93.8",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
@@ -233,7 +233,11 @@
|
||||
"fuse.js": "7.1.0",
|
||||
"input-otp": "^1.4.1",
|
||||
"mipd": "0.0.7",
|
||||
"open": "10.1.0",
|
||||
"ora": "8.2.0",
|
||||
"ox": "0.6.10",
|
||||
"prompts": "2.4.2",
|
||||
"toml": "3.0.0",
|
||||
"uqr": "0.1.2",
|
||||
"viem": "2.23.10"
|
||||
},
|
||||
@@ -358,6 +362,7 @@
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/cross-spawn": "^6.0.6",
|
||||
"@types/prompts": "2.4.9",
|
||||
"@types/react": "19.0.10",
|
||||
"@viem/anvil": "0.0.10",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"function activateProgram(address program) returns (uint16,uint256)"
|
||||
]
|
||||
@@ -55,7 +55,7 @@ import type { PreparedQuote, Quote } from "./types/Quote.js";
|
||||
* @returns A promise that resolves to a non-finalized quote for the requested buy.
|
||||
*
|
||||
* @throws Will throw an error if there is an issue fetching the quote.
|
||||
* @bridge
|
||||
* @bridge Buy
|
||||
* @beta
|
||||
*/
|
||||
export async function quote(options: quote.Options): Promise<quote.Result> {
|
||||
@@ -188,7 +188,7 @@ export declare namespace quote {
|
||||
* @returns A promise that resolves to a non-finalized quote for the requested buy.
|
||||
*
|
||||
* @throws Will throw an error if there is an issue fetching the quote.
|
||||
* @bridge
|
||||
* @bridge Buy
|
||||
* @beta
|
||||
*/
|
||||
export async function prepare(
|
||||
|
||||
@@ -55,7 +55,7 @@ import type { PreparedQuote, Quote } from "./types/Quote.js";
|
||||
* @returns A promise that resolves to a non-finalized quote for the requested sell.
|
||||
*
|
||||
* @throws Will throw an error if there is an issue fetching the quote.
|
||||
* @bridge
|
||||
* @bridge Sell
|
||||
* @beta
|
||||
*/
|
||||
export async function quote(options: quote.Options): Promise<quote.Result> {
|
||||
@@ -188,7 +188,7 @@ export declare namespace quote {
|
||||
* @returns A promise that resolves to a non-finalized quote for the requested buy.
|
||||
*
|
||||
* @throws Will throw an error if there is an issue fetching the quote.
|
||||
* @bridge
|
||||
* @bridge Sell
|
||||
* @beta
|
||||
*/
|
||||
export async function prepare(
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { Status } from "./types/Status.js";
|
||||
/**
|
||||
* Retrieves a Universal Bridge quote for the provided sell intent. The quote will specify the expected `destinationAmount` that will be received in exchange for the specified `originAmount`, which is specified with the `sellAmountWei` option.
|
||||
*
|
||||
+ * The returned status will include both the origin and destination transactions and any finalized amounts for the route.
|
||||
* The returned status will include both the origin and destination transactions and any finalized amounts for the route.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
|
||||
Regular → Executable
+24
@@ -4,9 +4,18 @@ import {
|
||||
generate,
|
||||
isValidChainIdAndContractAddress,
|
||||
} from "./commands/generate/generate.js";
|
||||
import { deployStylus, publishStylus } from "./commands/stylus/builder.js";
|
||||
import { createStylusProject } from "./commands/stylus/create.js";
|
||||
|
||||
// skip the first two args?
|
||||
const [, , command = "", ...rest] = process.argv;
|
||||
|
||||
let secretKey: string | undefined;
|
||||
const keyIndex = rest.indexOf("-k");
|
||||
if (keyIndex !== -1 && rest.length > keyIndex + 1) {
|
||||
secretKey = rest[keyIndex + 1];
|
||||
}
|
||||
|
||||
async function main() {
|
||||
switch (command) {
|
||||
case "generate": {
|
||||
@@ -20,6 +29,21 @@ async function main() {
|
||||
break;
|
||||
}
|
||||
|
||||
case "publish-stylus": {
|
||||
await publishStylus(secretKey);
|
||||
break;
|
||||
}
|
||||
|
||||
case "deploy-stylus": {
|
||||
await deployStylus(secretKey);
|
||||
break;
|
||||
}
|
||||
|
||||
case "create-stylus": {
|
||||
await createStylusProject();
|
||||
break;
|
||||
}
|
||||
|
||||
case "login": {
|
||||
// Not implemented yet
|
||||
console.info(
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import open from "open";
|
||||
import ora, { type Ora } from "ora";
|
||||
import { parse } from "toml";
|
||||
import { createThirdwebClient } from "../../../client/client.js";
|
||||
import { upload } from "../../../storage/upload.js";
|
||||
|
||||
const THIRDWEB_URL = "https://thirdweb.com";
|
||||
|
||||
export async function publishStylus(secretKey?: string) {
|
||||
const spinner = ora("Checking if this is a Stylus project...").start();
|
||||
const uri = await buildStylus(spinner, secretKey);
|
||||
|
||||
const url = getUrl(uri, "publish").toString();
|
||||
spinner.succeed(`Upload complete, navigate to ${url}`);
|
||||
await open(url);
|
||||
}
|
||||
|
||||
export async function deployStylus(secretKey?: string) {
|
||||
const spinner = ora("Checking if this is a Stylus project...").start();
|
||||
const uri = await buildStylus(spinner, secretKey);
|
||||
|
||||
const url = getUrl(uri, "deploy").toString();
|
||||
spinner.succeed(`Upload complete, navigate to ${url}`);
|
||||
await open(url);
|
||||
}
|
||||
|
||||
async function buildStylus(spinner: Ora, secretKey?: string) {
|
||||
if (!secretKey) {
|
||||
spinner.fail(
|
||||
"Error: Secret key is required. Please pass it via the -k parameter.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Validate stylus project
|
||||
const root = process.cwd();
|
||||
if (!root) {
|
||||
spinner.fail("Error: No package directory found.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cargoTomlPath = join(root, "Cargo.toml");
|
||||
if (!existsSync(cargoTomlPath)) {
|
||||
spinner.fail("Error: No Cargo.toml found. Not a Stylus/Rust project.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cargoToml = readFileSync(cargoTomlPath, "utf8");
|
||||
const parsedCargoToml = parse(cargoToml);
|
||||
if (!parsedCargoToml.dependencies?.["stylus-sdk"]) {
|
||||
spinner.fail(
|
||||
"Error: Not a Stylus project. Missing stylus-sdk dependency.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
spinner.succeed("Stylus project detected.");
|
||||
|
||||
// Step 2: Run stylus command to generate initcode
|
||||
spinner.start("Generating initcode...");
|
||||
const initcodeResult = spawnSync("cargo", ["stylus", "get-initcode"], {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
if (initcodeResult.status !== 0) {
|
||||
spinner.fail("Failed to generate initcode.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const initcode = extractBytecode(initcodeResult.stdout);
|
||||
if (!initcode) {
|
||||
spinner.fail("Failed to generate initcode.");
|
||||
process.exit(1);
|
||||
}
|
||||
spinner.succeed("Initcode generated.");
|
||||
|
||||
// Step 3: Run stylus command to generate abi
|
||||
spinner.start("Generating ABI...");
|
||||
const abiResult = spawnSync("cargo", ["stylus", "export-abi", "--json"], {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
if (abiResult.status !== 0) {
|
||||
spinner.fail("Failed to generate ABI.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const abiContent = abiResult.stdout.trim();
|
||||
if (!abiContent) {
|
||||
spinner.fail("Failed to generate ABI.");
|
||||
process.exit(1);
|
||||
}
|
||||
spinner.succeed("ABI generated.");
|
||||
|
||||
// Step 4: Process the output
|
||||
const contractName = extractContractNameFromExportAbi(abiContent);
|
||||
if (!contractName) {
|
||||
spinner.fail("Error: Could not determine contract name from ABI output.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let cleanedAbi = "";
|
||||
try {
|
||||
const jsonMatch = abiContent.match(/\[.*\]/s);
|
||||
if (jsonMatch) {
|
||||
cleanedAbi = jsonMatch[0];
|
||||
} else {
|
||||
throw new Error("No valid JSON ABI found in the file.");
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.fail("Error: ABI file contains invalid format.");
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
compiler: {},
|
||||
language: "rust",
|
||||
output: {
|
||||
abi: JSON.parse(cleanedAbi),
|
||||
devdoc: {},
|
||||
userdoc: {},
|
||||
},
|
||||
settings: {
|
||||
compilationTarget: {
|
||||
"src/main.rs": contractName,
|
||||
},
|
||||
},
|
||||
sources: {},
|
||||
};
|
||||
spinner.succeed("Stylus contract exported successfully.");
|
||||
|
||||
// Step 5: Upload to IPFS
|
||||
spinner.start("Uploading to IPFS...");
|
||||
const client = createThirdwebClient({
|
||||
secretKey,
|
||||
});
|
||||
|
||||
const metadataUri = await upload({
|
||||
client,
|
||||
files: [metadata],
|
||||
});
|
||||
|
||||
const bytecodeUri = await upload({
|
||||
client,
|
||||
files: [initcode],
|
||||
});
|
||||
|
||||
const uri = await upload({
|
||||
client,
|
||||
files: [
|
||||
{
|
||||
name: contractName,
|
||||
metadataUri,
|
||||
bytecodeUri,
|
||||
analytics: {
|
||||
command: "publish-stylus",
|
||||
contract_name: contractName,
|
||||
cli_version: "",
|
||||
project_type: "stylus",
|
||||
},
|
||||
compilers: {
|
||||
stylus: [
|
||||
{ compilerVersion: "", evmVersion: "", metadataUri, bytecodeUri },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
spinner.succeed("Upload complete");
|
||||
|
||||
return uri;
|
||||
} catch (error) {
|
||||
spinner.fail(`Error: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function extractContractNameFromExportAbi(abiRawOutput: string): string | null {
|
||||
const match = abiRawOutput.match(/<stdin>:(I[A-Za-z0-9_]+)/);
|
||||
if (match?.[1]) {
|
||||
return match[1].replace(/^I/, "");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getUrl(hash: string, command: string) {
|
||||
const url = new URL(
|
||||
`${THIRDWEB_URL}/contracts/${command}/${encodeURIComponent(hash.replace("ipfs://", ""))}`,
|
||||
);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
function extractBytecode(rawOutput: string): string {
|
||||
const hexStart = rawOutput.indexOf("7f000000");
|
||||
if (hexStart === -1) {
|
||||
throw new Error("Could not find start of bytecode");
|
||||
}
|
||||
return rawOutput.slice(hexStart).trim();
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import ora from "ora";
|
||||
import prompts from "prompts";
|
||||
|
||||
export async function createStylusProject() {
|
||||
const spinner = ora();
|
||||
|
||||
// Step 1: Ensure cargo is installed
|
||||
const cargoCheck = spawnSync("cargo", ["--version"]);
|
||||
if (cargoCheck.status !== 0) {
|
||||
console.error("Error: `cargo` is not installed");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Step 2: Install stylus etc.
|
||||
spinner.start("Installing Stylus...");
|
||||
const install = spawnSync("cargo", ["install", "--force", "cargo-stylus"], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
if (install.status !== 0) {
|
||||
spinner.fail("Failed to install Stylus.");
|
||||
process.exit(1);
|
||||
}
|
||||
spinner.succeed("Stylus installed.");
|
||||
|
||||
spawnSync("rustup", ["default", "1.83"], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
spawnSync("rustup", ["target", "add", "wasm32-unknown-unknown"], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
// Step 3: Create the project
|
||||
const { projectName } = await prompts({
|
||||
type: "text",
|
||||
name: "projectName",
|
||||
message: "Project name:",
|
||||
initial: "my-stylus-project",
|
||||
});
|
||||
spinner.start(`Creating new Stylus project: ${projectName}...`);
|
||||
const newProject = spawnSync("cargo", ["stylus", "new", projectName], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
if (newProject.status !== 0) {
|
||||
spinner.fail("Failed to create Stylus project.");
|
||||
process.exit(1);
|
||||
}
|
||||
spinner.succeed("Project created successfully.");
|
||||
|
||||
console.log(`\n✅ Done! cd into your project: ${projectName}`);
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export function prepareDeterministicDeployTransaction(
|
||||
data: async () => {
|
||||
const infraContractInfo =
|
||||
await computeDeploymentInfoFromContractId(options);
|
||||
return infraContractInfo.initBytecodeWithsalt;
|
||||
return infraContractInfo.initCalldata;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Abi, AbiConstructor } from "abitype";
|
||||
import { activateStylusContract } from "../../extensions/stylus/write/activateStylusContract.js";
|
||||
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
|
||||
import { sendTransaction } from "../../transaction/actions/send-transaction.js";
|
||||
import { prepareTransaction } from "../../transaction/prepare-transaction.js";
|
||||
import { encodeAbiParameters } from "../../utils/abi/encodeAbiParameters.js";
|
||||
import { normalizeFunctionParams } from "../../utils/abi/normalizeFunctionParams.js";
|
||||
@@ -24,6 +26,7 @@ export type PrepareDirectDeployTransactionOptions = Prettify<
|
||||
abi: Abi;
|
||||
bytecode: Hex;
|
||||
constructorParams?: Record<string, unknown>;
|
||||
extraDataWithUri?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -70,6 +73,7 @@ export function prepareDirectDeployTransaction(
|
||||
constructorAbi?.inputs || [], // Leave an empty array if there's no constructor
|
||||
normalizeFunctionParams(constructorAbi, options.constructorParams),
|
||||
),
|
||||
(options.extraDataWithUri as `0x${string}`) || "0x",
|
||||
]),
|
||||
});
|
||||
}
|
||||
@@ -121,6 +125,8 @@ export async function deployContract(
|
||||
options: PrepareDirectDeployTransactionOptions & {
|
||||
account: Account;
|
||||
salt?: string;
|
||||
extraDataWithUri?: Hex;
|
||||
isStylus?: boolean;
|
||||
},
|
||||
) {
|
||||
if (await isZkSyncChain(options.chain)) {
|
||||
@@ -135,14 +141,16 @@ export async function deployContract(
|
||||
});
|
||||
}
|
||||
|
||||
let address: string | null | undefined;
|
||||
if (options.salt !== undefined) {
|
||||
// Deploy with CREATE2 if salt is provided
|
||||
const info = await computeDeploymentInfoFromBytecode(options);
|
||||
const address = computeDeploymentAddress({
|
||||
address = computeDeploymentAddress({
|
||||
bytecode: options.bytecode,
|
||||
encodedArgs: info.encodedArgs,
|
||||
create2FactoryAddress: info.create2FactoryAddress,
|
||||
salt: options.salt,
|
||||
extraDataWithUri: options.extraDataWithUri,
|
||||
});
|
||||
const isDeployed = await isContractDeployed(
|
||||
getContract({
|
||||
@@ -160,21 +168,39 @@ export async function deployContract(
|
||||
chain: options.chain,
|
||||
client: options.client,
|
||||
to: info.create2FactoryAddress,
|
||||
data: info.initBytecodeWithsalt,
|
||||
data: info.initCalldata,
|
||||
}),
|
||||
});
|
||||
return address;
|
||||
} else {
|
||||
const deployTx = prepareDirectDeployTransaction(options);
|
||||
const receipt = await sendAndConfirmTransaction({
|
||||
account: options.account,
|
||||
transaction: deployTx,
|
||||
});
|
||||
address = receipt.contractAddress;
|
||||
if (!address) {
|
||||
throw new Error(
|
||||
`Could not find deployed contract address in transaction: ${receipt.transactionHash}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const deployTx = prepareDirectDeployTransaction(options);
|
||||
const receipt = await sendAndConfirmTransaction({
|
||||
account: options.account,
|
||||
transaction: deployTx,
|
||||
});
|
||||
if (!receipt.contractAddress) {
|
||||
throw new Error(
|
||||
`Could not find deployed contract address in transaction: ${receipt.transactionHash}`,
|
||||
);
|
||||
if (options.isStylus) {
|
||||
try {
|
||||
const activationTransaction = await activateStylusContract({
|
||||
chain: options.chain,
|
||||
client: options.client,
|
||||
contractAddress: address,
|
||||
});
|
||||
|
||||
await sendTransaction({
|
||||
transaction: activationTransaction,
|
||||
account: options.account,
|
||||
});
|
||||
} catch {
|
||||
console.error("Error: Contract could not be activated.");
|
||||
}
|
||||
}
|
||||
return receipt.contractAddress;
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { activateStylusContract } from "../../../extensions/stylus/write/activateStylusContract.js";
|
||||
import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and-confirm-transaction.js";
|
||||
import { sendTransaction } from "../../../transaction/actions/send-transaction.js";
|
||||
import {
|
||||
type FetchDeployMetadataResult,
|
||||
fetchBytecodeFromCompilerMetadata,
|
||||
@@ -255,5 +257,24 @@ export async function getOrDeployInfraContractFromMetadata(
|
||||
if (!deployedInfraContract) {
|
||||
throw new Error(`Failed to deploy ${options.contractMetadata.name}`);
|
||||
}
|
||||
|
||||
const isStylus = options.contractMetadata.metadata.language === "rust";
|
||||
if (isStylus) {
|
||||
try {
|
||||
const activationTransaction = await activateStylusContract({
|
||||
chain: options.chain,
|
||||
client: options.client,
|
||||
contractAddress: deployedInfraContract.address,
|
||||
});
|
||||
|
||||
await sendTransaction({
|
||||
transaction: activationTransaction,
|
||||
account: options.account,
|
||||
});
|
||||
} catch {
|
||||
console.error("Error: Contract could not be activated.");
|
||||
}
|
||||
}
|
||||
|
||||
return deployedInfraContract;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export function prepareInfraContractDeployTransactionFromMetadata(options: {
|
||||
data: async () => {
|
||||
const infraContractInfo =
|
||||
await computeDeploymentInfoFromMetadata(options);
|
||||
return infraContractInfo.initBytecodeWithsalt;
|
||||
return infraContractInfo.initCalldata;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Write
|
||||
*/
|
||||
export {
|
||||
activateStylusContract,
|
||||
type ActivateStylusContractOptions,
|
||||
} from "../../extensions/stylus/write/activateStylusContract.js";
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
fetchBytecodeFromCompilerMetadata,
|
||||
} from "../../utils/any-evm/deploy-metadata.js";
|
||||
import type { FetchDeployMetadataResult } from "../../utils/any-evm/deploy-metadata.js";
|
||||
import { encodeExtraDataWithUri } from "../../utils/any-evm/encode-extra-data-with-uri.js";
|
||||
import type { Hex } from "../../utils/encoding/hex.js";
|
||||
import type { Account } from "../../wallets/interfaces/wallet.js";
|
||||
import { getAllDefaultConstructorParamsForImplementation } from "./get-required-transactions.js";
|
||||
@@ -109,9 +110,14 @@ export async function deployPublishedContract(
|
||||
chain,
|
||||
deployMetadata,
|
||||
client,
|
||||
initializeParams: contractParams || deployMetadata.constructorParams,
|
||||
implementationConstructorParams:
|
||||
implementationConstructorParams || deployMetadata.implConstructorParams,
|
||||
initializeParams: {
|
||||
...deployMetadata.constructorParams,
|
||||
...contractParams,
|
||||
},
|
||||
implementationConstructorParams: {
|
||||
...deployMetadata.implConstructorParams,
|
||||
...implementationConstructorParams,
|
||||
},
|
||||
salt,
|
||||
});
|
||||
}
|
||||
@@ -194,6 +200,7 @@ export async function deployContractfromDeployMetadata(
|
||||
compilerMetadata: deployMetadata,
|
||||
contractParams: processedInitializeParams,
|
||||
salt,
|
||||
metadataUri: deployMetadata.metadataUri,
|
||||
});
|
||||
}
|
||||
case "autoFactory": {
|
||||
@@ -233,14 +240,15 @@ export async function deployContractfromDeployMetadata(
|
||||
client,
|
||||
account,
|
||||
contractId: deployMetadata.name,
|
||||
constructorParams:
|
||||
processedImplParams ||
|
||||
(await getAllDefaultConstructorParamsForImplementation({
|
||||
constructorParams: {
|
||||
...(await getAllDefaultConstructorParamsForImplementation({
|
||||
chain,
|
||||
client,
|
||||
contractId: deployMetadata.name,
|
||||
defaultExtensions: deployMetadata.defaultExtensions,
|
||||
})),
|
||||
...processedImplParams,
|
||||
},
|
||||
publisher: deployMetadata.publisher,
|
||||
version: deployMetadata.version,
|
||||
});
|
||||
@@ -320,6 +328,7 @@ export async function deployContractfromDeployMetadata(
|
||||
compilerMetadata: deployMetadata,
|
||||
contractParams: processedInitializeParams,
|
||||
salt,
|
||||
metadataUri: deployMetadata.metadataUri,
|
||||
});
|
||||
}
|
||||
default:
|
||||
@@ -335,6 +344,7 @@ async function directDeploy(options: {
|
||||
compilerMetadata: CompilerMetadata;
|
||||
contractParams?: Record<string, unknown>;
|
||||
salt?: string;
|
||||
metadataUri?: string;
|
||||
}): Promise<string> {
|
||||
const { account, client, chain, compilerMetadata, contractParams, salt } =
|
||||
options;
|
||||
@@ -342,6 +352,7 @@ async function directDeploy(options: {
|
||||
const { deployContract } = await import(
|
||||
"../../contract/deployment/deploy-with-abi.js"
|
||||
);
|
||||
const isStylus = options.compilerMetadata.metadata.language === "rust";
|
||||
return deployContract({
|
||||
account,
|
||||
client,
|
||||
@@ -354,6 +365,13 @@ async function directDeploy(options: {
|
||||
abi: compilerMetadata.abi,
|
||||
constructorParams: contractParams,
|
||||
salt,
|
||||
extraDataWithUri:
|
||||
isStylus && options.metadataUri
|
||||
? encodeExtraDataWithUri({
|
||||
metadataUri: options.metadataUri,
|
||||
})
|
||||
: undefined,
|
||||
isStylus,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
import type { AbiParameterToPrimitiveType } from "abitype";
|
||||
import type {
|
||||
BaseTransactionOptions,
|
||||
WithOverrides,
|
||||
} from "../../../../../transaction/types.js";
|
||||
import { prepareContractCall } from "../../../../../transaction/prepare-contract-call.js";
|
||||
import { encodeAbiParameters } from "../../../../../utils/abi/encodeAbiParameters.js";
|
||||
import { once } from "../../../../../utils/promise/once.js";
|
||||
import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js";
|
||||
|
||||
/**
|
||||
* Represents the parameters for the "activateProgram" function.
|
||||
*/
|
||||
export type ActivateProgramParams = WithOverrides<{
|
||||
program: AbiParameterToPrimitiveType<{ type: "address"; name: "program" }>;
|
||||
}>;
|
||||
|
||||
export const FN_SELECTOR = "0x58c780c2" as const;
|
||||
const FN_INPUTS = [
|
||||
{
|
||||
type: "address",
|
||||
name: "program",
|
||||
},
|
||||
] as const;
|
||||
const FN_OUTPUTS = [
|
||||
{
|
||||
type: "uint16",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
},
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Checks if the `activateProgram` method is supported by the given contract.
|
||||
* @param availableSelectors An array of 4byte function selectors of the contract. You can get this in various ways, such as using "whatsabi" or if you have the ABI of the contract available you can use it to generate the selectors.
|
||||
* @returns A boolean indicating if the `activateProgram` method is supported.
|
||||
* @extension STYLUS
|
||||
* @example
|
||||
* ```ts
|
||||
* import { isActivateProgramSupported } from "thirdweb/extensions/stylus";
|
||||
*
|
||||
* const supported = isActivateProgramSupported(["0x..."]);
|
||||
* ```
|
||||
*/
|
||||
export function isActivateProgramSupported(availableSelectors: string[]) {
|
||||
return detectMethod({
|
||||
availableSelectors,
|
||||
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the parameters for the "activateProgram" function.
|
||||
* @param options - The options for the activateProgram function.
|
||||
* @returns The encoded ABI parameters.
|
||||
* @extension STYLUS
|
||||
* @example
|
||||
* ```ts
|
||||
* import { encodeActivateProgramParams } from "thirdweb/extensions/stylus";
|
||||
* const result = encodeActivateProgramParams({
|
||||
* program: ...,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function encodeActivateProgramParams(options: ActivateProgramParams) {
|
||||
return encodeAbiParameters(FN_INPUTS, [options.program]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the "activateProgram" function into a Hex string with its parameters.
|
||||
* @param options - The options for the activateProgram function.
|
||||
* @returns The encoded hexadecimal string.
|
||||
* @extension STYLUS
|
||||
* @example
|
||||
* ```ts
|
||||
* import { encodeActivateProgram } from "thirdweb/extensions/stylus";
|
||||
* const result = encodeActivateProgram({
|
||||
* program: ...,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function encodeActivateProgram(options: ActivateProgramParams) {
|
||||
// we do a "manual" concat here to avoid the overhead of the "concatHex" function
|
||||
// we can do this because we know the specific formats of the values
|
||||
return (FN_SELECTOR +
|
||||
encodeActivateProgramParams(options).slice(
|
||||
2,
|
||||
)) as `${typeof FN_SELECTOR}${string}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a transaction to call the "activateProgram" function on the contract.
|
||||
* @param options - The options for the "activateProgram" function.
|
||||
* @returns A prepared transaction object.
|
||||
* @extension STYLUS
|
||||
* @example
|
||||
* ```ts
|
||||
* import { sendTransaction } from "thirdweb";
|
||||
* import { activateProgram } from "thirdweb/extensions/stylus";
|
||||
*
|
||||
* const transaction = activateProgram({
|
||||
* contract,
|
||||
* program: ...,
|
||||
* overrides: {
|
||||
* ...
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // Send the transaction
|
||||
* await sendTransaction({ transaction, account });
|
||||
* ```
|
||||
*/
|
||||
export function activateProgram(
|
||||
options: BaseTransactionOptions<
|
||||
| ActivateProgramParams
|
||||
| {
|
||||
asyncParams: () => Promise<ActivateProgramParams>;
|
||||
}
|
||||
>,
|
||||
) {
|
||||
const asyncOptions = once(async () => {
|
||||
return "asyncParams" in options ? await options.asyncParams() : options;
|
||||
});
|
||||
|
||||
return prepareContractCall({
|
||||
contract: options.contract,
|
||||
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
|
||||
params: async () => {
|
||||
const resolvedOptions = await asyncOptions();
|
||||
return [resolvedOptions.program] as const;
|
||||
},
|
||||
value: async () => (await asyncOptions()).overrides?.value,
|
||||
accessList: async () => (await asyncOptions()).overrides?.accessList,
|
||||
gas: async () => (await asyncOptions()).overrides?.gas,
|
||||
gasPrice: async () => (await asyncOptions()).overrides?.gasPrice,
|
||||
maxFeePerGas: async () => (await asyncOptions()).overrides?.maxFeePerGas,
|
||||
maxPriorityFeePerGas: async () =>
|
||||
(await asyncOptions()).overrides?.maxPriorityFeePerGas,
|
||||
nonce: async () => (await asyncOptions()).overrides?.nonce,
|
||||
extraGas: async () => (await asyncOptions()).overrides?.extraGas,
|
||||
erc20Value: async () => (await asyncOptions()).overrides?.erc20Value,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
decodeAbiParameters,
|
||||
formatTransactionRequest,
|
||||
parseEther,
|
||||
} from "viem";
|
||||
import type { Chain } from "../../../chains/types.js";
|
||||
import type { ThirdwebClient } from "../../../client/client.js";
|
||||
import { getContract } from "../../../contract/contract.js";
|
||||
import { eth_call } from "../../../rpc/actions/eth_call.js";
|
||||
import { getRpcClient } from "../../../rpc/rpc.js";
|
||||
import { encode } from "../../../transaction/actions/encode.js";
|
||||
import type { PreparedTransaction } from "../../../transaction/prepare-transaction.js";
|
||||
import { activateProgram } from "../__generated__/IArbWasm/write/activateProgram.js";
|
||||
|
||||
const ARB_WASM_ADDRESS = "0x0000000000000000000000000000000000000071";
|
||||
|
||||
export type ActivateStylusContractOptions = {
|
||||
chain: Chain;
|
||||
client: ThirdwebClient;
|
||||
contractAddress: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Activate a stylus contract by calling ArbWasm Precompile
|
||||
* @param options - The options for activating the contract
|
||||
* @returns A prepared transaction to send
|
||||
* @example
|
||||
* ```ts
|
||||
* import { activateStylusContract } from "thirdweb/stylus";
|
||||
* const transaction = activateStylusContract({
|
||||
* client,
|
||||
* chain,
|
||||
* contractAddress,
|
||||
* });
|
||||
* await sendTransaction({ transaction, account });
|
||||
* ```
|
||||
*/
|
||||
export async function activateStylusContract(
|
||||
options: ActivateStylusContractOptions,
|
||||
) {
|
||||
const { chain, client, contractAddress } = options;
|
||||
const arbWasmPrecompile = getContract({
|
||||
client,
|
||||
chain,
|
||||
address: ARB_WASM_ADDRESS,
|
||||
});
|
||||
|
||||
const dataFee = await estimateDataFee({
|
||||
transaction: activateProgram({
|
||||
program: contractAddress,
|
||||
contract: arbWasmPrecompile,
|
||||
}),
|
||||
});
|
||||
|
||||
return activateProgram({
|
||||
program: contractAddress,
|
||||
contract: arbWasmPrecompile,
|
||||
overrides: {
|
||||
value: dataFee,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function estimateDataFee(options: {
|
||||
// biome-ignore lint/suspicious/noExplicitAny:
|
||||
transaction: PreparedTransaction<any>;
|
||||
}) {
|
||||
const data = await encode(options.transaction);
|
||||
|
||||
const serializedTx = formatTransactionRequest({
|
||||
data,
|
||||
to: ARB_WASM_ADDRESS,
|
||||
value: parseEther("1"), // only for simulation. it will be replaced with estimated data fee.
|
||||
});
|
||||
|
||||
const rpcRequest = getRpcClient(options.transaction);
|
||||
try {
|
||||
const result = await eth_call(rpcRequest, serializedTx);
|
||||
const [, dataFee] = decodeAbiParameters(
|
||||
[
|
||||
{
|
||||
type: "uint16",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
},
|
||||
],
|
||||
result,
|
||||
);
|
||||
|
||||
return (dataFee * BigInt(100 + 10)) / BigInt(100); // bump 10%
|
||||
} catch {
|
||||
return 0n;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import type { Chain } from "../../../../../../chains/types.js";
|
||||
import { getCachedChain } from "../../../../../../chains/utils.js";
|
||||
import type { ThirdwebClient } from "../../../../../../client/client.js";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "../../../../../../constants/addresses.js";
|
||||
import type { BuyWithCryptoStatus } from "../../../../../../pay/buyWithCrypto/getStatus.js";
|
||||
@@ -112,10 +113,21 @@ export default function BuyScreen(props: BuyScreenProps) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
const supportedDestinations = props.supportedTokens
|
||||
? Object.entries(props.supportedTokens).map(([chainId, tokens]) => ({
|
||||
chain: getCachedChain(Number.parseInt(chainId)),
|
||||
tokens: tokens.map((t) => ({
|
||||
...t,
|
||||
buyWithCryptoEnabled: true,
|
||||
buyWithFiatEnabled: true,
|
||||
})),
|
||||
}))
|
||||
: supportedDestinationsQuery.data;
|
||||
|
||||
return (
|
||||
<BuyScreenContent
|
||||
{...props}
|
||||
supportedDestinations={supportedDestinationsQuery.data}
|
||||
supportedDestinations={supportedDestinations}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -583,10 +583,10 @@ function useSwapStatus(props: {
|
||||
uiStatus = "completed";
|
||||
break;
|
||||
case "FAILED":
|
||||
case "NOT_FOUND":
|
||||
uiStatus = "failed";
|
||||
break;
|
||||
case "PENDING":
|
||||
case "NOT_FOUND":
|
||||
uiStatus = "pending";
|
||||
break;
|
||||
case "NONE":
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import type React from "react";
|
||||
import { forwardRef, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
CarbonDocumentAudio,
|
||||
CarbonDocumentUnknown,
|
||||
@@ -49,134 +50,132 @@ import { useResolvedMediaType } from "./useResolvedMediaType.js";
|
||||
* @param props - Refer to [`MediaRendererProps`](https://portal.thirdweb.com/references/typescript/v5/MediaRendererProps) to see the available props.
|
||||
*/
|
||||
export const MediaRenderer = /* @__PURE__ */ (() =>
|
||||
React.forwardRef<HTMLMediaElement, MediaRendererProps>(
|
||||
function Media_Renderer(
|
||||
{
|
||||
src,
|
||||
poster,
|
||||
alt,
|
||||
gatewayUrl,
|
||||
requireInteraction = false,
|
||||
width = "300px",
|
||||
height = "300px",
|
||||
style,
|
||||
mimeType,
|
||||
client,
|
||||
controls,
|
||||
className,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const mergedStyle: React.CSSProperties = {
|
||||
objectFit: "contain",
|
||||
...style,
|
||||
};
|
||||
|
||||
const { mediaInfo, isFetched: mediaInfoIsFetched } = useResolvedMediaType(
|
||||
client,
|
||||
src ?? undefined,
|
||||
mimeType,
|
||||
gatewayUrl,
|
||||
);
|
||||
|
||||
const { mediaInfo: possiblePosterSrc } = useResolvedMediaType(
|
||||
client,
|
||||
poster ?? undefined,
|
||||
undefined,
|
||||
gatewayUrl,
|
||||
);
|
||||
|
||||
if (!mediaInfoIsFetched || !src) {
|
||||
return <div style={style} />;
|
||||
}
|
||||
|
||||
if (mediaInfo.mimeType) {
|
||||
// html content
|
||||
if (mediaInfo.mimeType.startsWith("text/html")) {
|
||||
return (
|
||||
<IframePlayer
|
||||
style={mergedStyle}
|
||||
src={mediaInfo.url}
|
||||
poster={possiblePosterSrc.url}
|
||||
ref={ref as unknown as React.ForwardedRef<HTMLIFrameElement>}
|
||||
requireInteraction={requireInteraction}
|
||||
className={className}
|
||||
alt={alt}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 3d model
|
||||
if (mediaInfo.mimeType.startsWith("model")) {
|
||||
console.error(
|
||||
"Encountered an unsupported media type. 3D model support was removed in v5.92.0. To add a 3D model to your app, use @google/model-viewer and use the ModelViewer component.",
|
||||
);
|
||||
}
|
||||
|
||||
// video
|
||||
if (mediaInfo.mimeType.startsWith("video")) {
|
||||
return (
|
||||
<VideoPlayer
|
||||
style={mergedStyle}
|
||||
src={mediaInfo.url}
|
||||
ref={ref as unknown as React.ForwardedRef<HTMLVideoElement>}
|
||||
poster={
|
||||
possiblePosterSrc.mimeType?.startsWith("image/")
|
||||
? possiblePosterSrc.url
|
||||
: undefined
|
||||
}
|
||||
requireInteraction={requireInteraction}
|
||||
className={className}
|
||||
controls={controls}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// audio
|
||||
if (mediaInfo.mimeType.startsWith("audio")) {
|
||||
return (
|
||||
<AudioPlayer
|
||||
style={mergedStyle}
|
||||
src={mediaInfo.url}
|
||||
poster={possiblePosterSrc.url}
|
||||
alt={alt}
|
||||
ref={ref as unknown as React.ForwardedRef<HTMLAudioElement>}
|
||||
className={className}
|
||||
height={height}
|
||||
width={width}
|
||||
controls={controls}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// image
|
||||
if (mediaInfo.mimeType.startsWith("image/")) {
|
||||
return (
|
||||
<ImageRenderer
|
||||
style={mergedStyle}
|
||||
src={mediaInfo.url}
|
||||
alt={alt}
|
||||
ref={ref as unknown as React.ForwardedRef<HTMLImageElement>}
|
||||
className={className}
|
||||
height={height}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// unknown mime types or no mime type
|
||||
return (
|
||||
<LinkPlayer
|
||||
style={mergedStyle}
|
||||
src={mediaInfo.url}
|
||||
alt={alt}
|
||||
ref={ref as unknown as React.Ref<HTMLAnchorElement>}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
forwardRef<HTMLMediaElement, MediaRendererProps>(function Media_Renderer(
|
||||
{
|
||||
src,
|
||||
poster,
|
||||
alt,
|
||||
gatewayUrl,
|
||||
requireInteraction = false,
|
||||
width = "300px",
|
||||
height = "300px",
|
||||
style,
|
||||
mimeType,
|
||||
client,
|
||||
controls,
|
||||
className,
|
||||
},
|
||||
))();
|
||||
ref,
|
||||
) {
|
||||
const mergedStyle: React.CSSProperties = {
|
||||
objectFit: "contain",
|
||||
...style,
|
||||
};
|
||||
|
||||
const { mediaInfo, isFetched: mediaInfoIsFetched } = useResolvedMediaType(
|
||||
client,
|
||||
src ?? undefined,
|
||||
mimeType,
|
||||
gatewayUrl,
|
||||
);
|
||||
|
||||
const { mediaInfo: possiblePosterSrc } = useResolvedMediaType(
|
||||
client,
|
||||
poster ?? undefined,
|
||||
undefined,
|
||||
gatewayUrl,
|
||||
);
|
||||
|
||||
if (!mediaInfoIsFetched || !src) {
|
||||
return <div style={style} />;
|
||||
}
|
||||
|
||||
if (mediaInfo.mimeType) {
|
||||
// html content
|
||||
if (mediaInfo.mimeType.startsWith("text/html")) {
|
||||
return (
|
||||
<IframePlayer
|
||||
style={mergedStyle}
|
||||
src={mediaInfo.url}
|
||||
poster={possiblePosterSrc.url}
|
||||
ref={ref as unknown as React.ForwardedRef<HTMLIFrameElement>}
|
||||
requireInteraction={requireInteraction}
|
||||
className={className}
|
||||
alt={alt}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 3d model
|
||||
if (mediaInfo.mimeType.startsWith("model")) {
|
||||
console.error(
|
||||
"Encountered an unsupported media type. 3D model support was removed in v5.92.0. To add a 3D model to your app, use @google/model-viewer and use the ModelViewer component.",
|
||||
);
|
||||
}
|
||||
|
||||
// video
|
||||
if (mediaInfo.mimeType.startsWith("video")) {
|
||||
return (
|
||||
<VideoPlayer
|
||||
style={mergedStyle}
|
||||
src={mediaInfo.url}
|
||||
ref={ref as unknown as React.ForwardedRef<HTMLVideoElement>}
|
||||
poster={
|
||||
possiblePosterSrc.mimeType?.startsWith("image/")
|
||||
? possiblePosterSrc.url
|
||||
: undefined
|
||||
}
|
||||
requireInteraction={requireInteraction}
|
||||
className={className}
|
||||
controls={controls}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// audio
|
||||
if (mediaInfo.mimeType.startsWith("audio")) {
|
||||
return (
|
||||
<AudioPlayer
|
||||
style={mergedStyle}
|
||||
src={mediaInfo.url}
|
||||
poster={possiblePosterSrc.url}
|
||||
alt={alt}
|
||||
ref={ref as unknown as React.ForwardedRef<HTMLAudioElement>}
|
||||
className={className}
|
||||
height={height}
|
||||
width={width}
|
||||
controls={controls}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// image
|
||||
if (mediaInfo.mimeType.startsWith("image/")) {
|
||||
return (
|
||||
<ImageRenderer
|
||||
style={mergedStyle}
|
||||
src={mediaInfo.url}
|
||||
alt={alt}
|
||||
ref={ref as unknown as React.ForwardedRef<HTMLImageElement>}
|
||||
className={className}
|
||||
height={height}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// unknown mime types or no mime type
|
||||
return (
|
||||
<LinkPlayer
|
||||
style={mergedStyle}
|
||||
src={mediaInfo.url}
|
||||
alt={alt}
|
||||
ref={ref as unknown as React.Ref<HTMLAnchorElement>}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}))();
|
||||
|
||||
interface PlayButtonProps {
|
||||
onClick: () => void;
|
||||
@@ -227,7 +226,7 @@ const PlayButton: React.FC<PlayButtonProps> = ({ onClick, isPlaying }) => {
|
||||
};
|
||||
|
||||
const ImageRenderer = /* @__PURE__ */ (() =>
|
||||
React.forwardRef<
|
||||
forwardRef<
|
||||
HTMLImageElement,
|
||||
Pick<
|
||||
MediaRendererProps,
|
||||
@@ -266,7 +265,7 @@ const ImageRenderer = /* @__PURE__ */ (() =>
|
||||
}))();
|
||||
|
||||
const VideoPlayer = /* @__PURE__ */ (() =>
|
||||
React.forwardRef<
|
||||
forwardRef<
|
||||
HTMLVideoElement,
|
||||
Pick<
|
||||
MediaRendererProps,
|
||||
@@ -393,7 +392,7 @@ const VideoPlayer = /* @__PURE__ */ (() =>
|
||||
}))();
|
||||
|
||||
const AudioPlayer = /* @__PURE__ */ (() =>
|
||||
React.forwardRef<
|
||||
forwardRef<
|
||||
HTMLAudioElement,
|
||||
Pick<
|
||||
MediaRendererProps,
|
||||
@@ -504,7 +503,7 @@ const AudioPlayer = /* @__PURE__ */ (() =>
|
||||
* @internal Exported for tests
|
||||
*/
|
||||
export const IframePlayer = /* @__PURE__ */ (() =>
|
||||
React.forwardRef<
|
||||
forwardRef<
|
||||
HTMLIFrameElement,
|
||||
Omit<
|
||||
MediaRendererProps,
|
||||
@@ -574,7 +573,7 @@ export const IframePlayer = /* @__PURE__ */ (() =>
|
||||
* @internal Exported for tests
|
||||
*/
|
||||
export const LinkPlayer = /* @__PURE__ */ (() =>
|
||||
React.forwardRef<
|
||||
forwardRef<
|
||||
HTMLAnchorElement,
|
||||
Pick<MediaRendererProps, "src" | "alt" | "style" | "className">
|
||||
>(function Link_Player({ src, alt, style, className }, ref) {
|
||||
|
||||
@@ -11,6 +11,7 @@ type ComputeDeploymentAddressOptions = {
|
||||
encodedArgs: Hex;
|
||||
create2FactoryAddress: string;
|
||||
salt?: string;
|
||||
extraDataWithUri?: Hex;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -40,10 +41,12 @@ export function computeDeploymentAddress(
|
||||
: getSaltHash(bytecode);
|
||||
|
||||
// 1. create init bytecode hash with contract's bytecode and encoded args
|
||||
const initBytecode = encodePacked(
|
||||
["bytes", "bytes"],
|
||||
[bytecode, options.encodedArgs],
|
||||
);
|
||||
const initBytecode = options.extraDataWithUri
|
||||
? encodePacked(
|
||||
["bytes", "bytes", "bytes"],
|
||||
[bytecode, options.encodedArgs, options.extraDataWithUri],
|
||||
)
|
||||
: encodePacked(["bytes", "bytes"], [bytecode, options.encodedArgs]);
|
||||
|
||||
// 2. abi-encode pack the deployer address, salt, and bytecode hash
|
||||
const deployInfoPacked = encodePacked(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Abi, AbiConstructor } from "abitype";
|
||||
import { encodePacked } from "viem";
|
||||
import type { Chain } from "../../chains/types.js";
|
||||
import type { ThirdwebClient } from "../../client/client.js";
|
||||
import { fetchPublishedContractMetadata } from "../../contract/deployment/publisher.js";
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
type FetchDeployMetadataResult,
|
||||
fetchBytecodeFromCompilerMetadata,
|
||||
} from "./deploy-metadata.js";
|
||||
import { encodeExtraDataWithUri } from "./encode-extra-data-with-uri.js";
|
||||
import { getInitBytecodeWithSalt } from "./get-init-bytecode-with-salt.js";
|
||||
|
||||
/**
|
||||
@@ -70,6 +72,7 @@ export async function computeDeploymentInfoFromMetadata(args: {
|
||||
}
|
||||
}
|
||||
|
||||
const isStylus = contractMetadata.metadata.language === "rust";
|
||||
return computeDeploymentInfoFromBytecode({
|
||||
client: args.client,
|
||||
chain: args.chain,
|
||||
@@ -81,6 +84,11 @@ export async function computeDeploymentInfoFromMetadata(args: {
|
||||
}),
|
||||
constructorParams: processedConstructorParams,
|
||||
salt: args.salt,
|
||||
extraDataWithUri: isStylus
|
||||
? encodeExtraDataWithUri({
|
||||
metadataUri: contractMetadata.metadataUri,
|
||||
})
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -91,8 +99,9 @@ export async function computeDeploymentInfoFromBytecode(args: {
|
||||
bytecode: Hex;
|
||||
constructorParams?: Record<string, unknown>;
|
||||
salt?: string;
|
||||
extraDataWithUri?: Hex;
|
||||
}) {
|
||||
const { client, chain, constructorParams, salt } = args;
|
||||
const { client, chain, constructorParams, salt, extraDataWithUri } = args;
|
||||
const create2FactoryAddress = await computeCreate2FactoryAddress({
|
||||
client,
|
||||
chain,
|
||||
@@ -110,11 +119,16 @@ export async function computeDeploymentInfoFromBytecode(args: {
|
||||
encodedArgs,
|
||||
salt,
|
||||
});
|
||||
|
||||
const initCalldata = extraDataWithUri
|
||||
? encodePacked(["bytes", "bytes"], [initBytecodeWithsalt, extraDataWithUri])
|
||||
: initBytecodeWithsalt;
|
||||
return {
|
||||
bytecode,
|
||||
initBytecodeWithsalt,
|
||||
initCalldata,
|
||||
encodedArgs,
|
||||
create2FactoryAddress,
|
||||
salt,
|
||||
extraDataWithUri,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ export type ExtendedMetadata = {
|
||||
>;
|
||||
compositeAbi?: Abi;
|
||||
compilers?: Record<
|
||||
"solc" | "zksolc",
|
||||
"solc" | "zksolc" | "stylus",
|
||||
{
|
||||
evmVersion: string;
|
||||
compilerVersion: string;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { encodePacked } from "ox/AbiParameters";
|
||||
import { numberToHex, stringToHex } from "../encoding/hex.js";
|
||||
|
||||
export function encodeExtraDataWithUri(options: {
|
||||
metadataUri: string;
|
||||
}) {
|
||||
const uriHex = stringToHex(options.metadataUri);
|
||||
const lengthHex = numberToHex(uriHex.replace("0x", "").length / 2, {
|
||||
size: 1,
|
||||
});
|
||||
|
||||
return encodePacked(["bytes", "bytes"], [uriHex, lengthHex]);
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
# @thirdweb-dev/wagmi-adapter
|
||||
|
||||
## 0.2.43
|
||||
|
||||
## 0.2.42
|
||||
|
||||
## 0.2.41
|
||||
|
||||
## 0.2.40
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/wagmi-adapter",
|
||||
"version": "0.2.40",
|
||||
"version": "0.2.43",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
|
||||
Generated
+209
-28
@@ -1091,9 +1091,21 @@ importers:
|
||||
mipd:
|
||||
specifier: 0.0.7
|
||||
version: 0.0.7([email protected])
|
||||
open:
|
||||
specifier: 10.1.0
|
||||
version: 10.1.0
|
||||
ora:
|
||||
specifier: 8.2.0
|
||||
version: 8.2.0
|
||||
ox:
|
||||
specifier: 0.6.10
|
||||
version: 0.6.10([email protected])([email protected])
|
||||
prompts:
|
||||
specifier: 2.4.2
|
||||
version: 2.4.2
|
||||
toml:
|
||||
specifier: 3.0.0
|
||||
version: 3.0.0
|
||||
uqr:
|
||||
specifier: 0.1.2
|
||||
version: 0.1.2
|
||||
@@ -1164,6 +1176,9 @@ importers:
|
||||
'@types/cross-spawn':
|
||||
specifier: ^6.0.6
|
||||
version: 6.0.6
|
||||
'@types/prompts':
|
||||
specifier: 2.4.9
|
||||
version: 2.4.9
|
||||
'@types/react':
|
||||
specifier: 19.0.10
|
||||
version: 19.0.10
|
||||
@@ -6342,6 +6357,9 @@ packages:
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==}
|
||||
|
||||
@@ -7493,6 +7511,10 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-EB2wFjXF106LQLe/CYnKCMCdLeTW47AtcEtUfiqAOgr2a08k0+YgRklur2aLfEYHlhz6baMskZ8L2U92Hh0vyA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
||||
engines: {node: '>=10.16.0'}
|
||||
@@ -7741,6 +7763,10 @@ packages:
|
||||
resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -8236,6 +8262,14 @@ packages:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -8255,6 +8289,10 @@ packages:
|
||||
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -9408,6 +9446,10 @@ packages:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -9982,6 +10024,11 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-F2FnH/otLNJv0J6wc73A5Xo7oHLNnqplYqZhUu01tD54DIPvxIRSTSLkrUB/M0nHO4vo1O9PDfN4KoTxCzLh/w==}
|
||||
|
||||
@@ -10015,6 +10062,11 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
|
||||
engines: {node: '>=14.16'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -10135,6 +10187,10 @@ packages:
|
||||
resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -10155,6 +10211,10 @@ packages:
|
||||
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -10708,6 +10768,10 @@ packages:
|
||||
resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Q5ALD6JiS6xAUWCwX3taQmgwxyveCtIIuL08+ml0nHwT3k0S/GIFJN+Hd38b1qYIMaE5X++iqsqWVksz7SYW+Q==}
|
||||
|
||||
@@ -11278,6 +11342,10 @@ packages:
|
||||
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -11868,9 +11936,17 @@ packages:
|
||||
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-kihvp0O4lFwf5tZMkfanwQLIZ9ORe9OeOFgZonH0BQeThgwfJiaZFeOfvvJVnJIM9TiVmx0RDD35hUJDR0++rQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -11910,6 +11986,10 @@ packages:
|
||||
resolution: {integrity: sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==}
|
||||
|
||||
@@ -13175,6 +13255,10 @@ packages:
|
||||
resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -13221,6 +13305,10 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-1IXGM/TfPT6nfYMIXkJdzn+L4JEsmb0FL1O2OBjaH03V3yuUDdKFulGLMFG6ErV+8pZ5HVC0limve01RyO+saA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
|
||||
@@ -13612,6 +13700,10 @@ packages:
|
||||
resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-XXh1Acvf1r3BQX0BDLQw6yhZ7yUGvYxIcKOBuMdetnX7iXtczipJTfw0uyFwk0ltkKEE9PpJvivYmARF3u64VQ==}
|
||||
hasBin: true
|
||||
@@ -13660,6 +13752,10 @@ packages:
|
||||
resolution: {integrity: sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==}
|
||||
|
||||
@@ -14038,6 +14134,9 @@ packages:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -15263,7 +15362,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 3.0.0
|
||||
'@aws-crypto/sha256-js': 3.0.0
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0
|
||||
'@aws-sdk/client-sts': 3.592.0
|
||||
'@aws-sdk/core': 3.592.0
|
||||
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
|
||||
@@ -15309,7 +15408,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 3.0.0
|
||||
'@aws-crypto/sha256-js': 3.0.0
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0
|
||||
'@aws-sdk/client-sts': 3.592.0
|
||||
'@aws-sdk/core': 3.592.0
|
||||
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
|
||||
@@ -15355,7 +15454,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 3.0.0
|
||||
'@aws-crypto/sha256-js': 3.0.0
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0
|
||||
'@aws-sdk/client-sts': 3.592.0
|
||||
'@aws-sdk/core': 3.592.0
|
||||
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
|
||||
@@ -15402,7 +15501,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/[email protected](@aws-sdk/[email protected])':
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 3.0.0
|
||||
'@aws-crypto/sha256-js': 3.0.0
|
||||
@@ -15445,7 +15544,6 @@ snapshots:
|
||||
'@smithy/util-utf8': 3.0.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/client-sts'
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
@@ -15582,7 +15680,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 3.0.0
|
||||
'@aws-crypto/sha256-js': 3.0.0
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0
|
||||
'@aws-sdk/core': 3.592.0
|
||||
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
|
||||
'@aws-sdk/middleware-host-header': 3.577.0
|
||||
@@ -16024,7 +16122,7 @@ snapshots:
|
||||
|
||||
'@aws-sdk/[email protected](@aws-sdk/[email protected])':
|
||||
dependencies:
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0
|
||||
'@aws-sdk/types': 3.577.0
|
||||
'@smithy/property-provider': 3.1.11
|
||||
'@smithy/shared-ini-file-loader': 3.1.12
|
||||
@@ -22926,6 +23024,11 @@ snapshots:
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]':
|
||||
dependencies:
|
||||
'@types/node': 22.13.10
|
||||
kleur: 3.0.3
|
||||
|
||||
'@types/[email protected]':
|
||||
dependencies:
|
||||
'@types/node': 22.13.10
|
||||
@@ -24857,6 +24960,10 @@ snapshots:
|
||||
esbuild: 0.20.2
|
||||
node-eval: 2.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
run-applescript: 7.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
streamsearch: 1.1.0
|
||||
@@ -25132,6 +25239,10 @@ snapshots:
|
||||
dependencies:
|
||||
restore-cursor: 4.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
restore-cursor: 5.1.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -25625,6 +25736,13 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
bundle-name: 4.1.0
|
||||
default-browser-id: 5.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
execa: 1.0.0
|
||||
@@ -25644,6 +25762,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
define-data-property: 1.1.4
|
||||
@@ -26174,8 +26294,8 @@ snapshots:
|
||||
'@typescript-eslint/parser': 7.14.1([email protected]([email protected]))([email protected])
|
||||
eslint: 9.22.0([email protected])
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.8.3([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected])))([email protected]([email protected]))
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected])))([email protected]([email protected])))([email protected]([email protected]))
|
||||
eslint-import-resolver-typescript: 3.8.3([email protected])([email protected]([email protected]))
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected]([email protected]))
|
||||
eslint-plugin-jsx-a11y: 6.10.2([email protected]([email protected]))
|
||||
eslint-plugin-react: 7.37.4([email protected]([email protected]))
|
||||
eslint-plugin-react-hooks: 5.1.0([email protected]([email protected]))
|
||||
@@ -26194,21 +26314,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
[email protected]([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected])))([email protected]([email protected])):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.0([email protected])
|
||||
enhanced-resolve: 5.18.1
|
||||
eslint: 9.22.0([email protected])
|
||||
get-tsconfig: 4.10.0
|
||||
is-bun-module: 1.3.0
|
||||
stable-hash: 0.0.4
|
||||
tinyglobby: 0.2.12
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected])))([email protected]([email protected])))([email protected]([email protected]))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
[email protected]([email protected])([email protected]):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
@@ -26224,6 +26329,21 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
[email protected]([email protected])([email protected]([email protected])):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.0([email protected])
|
||||
enhanced-resolve: 5.18.1
|
||||
eslint: 9.22.0([email protected])
|
||||
get-tsconfig: 4.10.0
|
||||
is-bun-module: 1.3.0
|
||||
stable-hash: 0.0.4
|
||||
tinyglobby: 0.2.12
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected]([email protected]))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
acorn: 8.14.1
|
||||
@@ -26256,14 +26376,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
[email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected]([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected])))([email protected]([email protected])))([email protected]([email protected])):
|
||||
[email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected]([email protected])([email protected]([email protected])))([email protected]([email protected])):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 7.14.1([email protected]([email protected]))([email protected])
|
||||
eslint: 9.22.0([email protected])
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.8.3([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected])))([email protected]([email protected]))
|
||||
eslint-import-resolver-typescript: 3.8.3([email protected])([email protected]([email protected]))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -26296,7 +26416,7 @@ snapshots:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
[email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected])))([email protected]([email protected])))([email protected]([email protected])):
|
||||
[email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected]([email protected])):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.8
|
||||
@@ -26307,7 +26427,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 9.22.0([email protected])
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected]([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected])))([email protected]([email protected])))([email protected]([email protected]))
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected]([email protected])([email protected]([email protected])))([email protected]([email protected]))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@@ -27389,6 +27509,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -28057,6 +28179,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -28084,6 +28208,10 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
is-docker: 3.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
global-dirs: 3.0.1
|
||||
@@ -28179,6 +28307,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -28196,6 +28326,10 @@ snapshots:
|
||||
dependencies:
|
||||
is-docker: 2.2.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
is-inside-container: 1.0.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -28789,6 +28923,11 @@ snapshots:
|
||||
chalk: 5.4.1
|
||||
is-unicode-supported: 1.3.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
chalk: 5.4.1
|
||||
is-unicode-supported: 1.3.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -29986,6 +30125,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -30510,12 +30651,23 @@ snapshots:
|
||||
dependencies:
|
||||
mimic-fn: 4.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
mimic-function: 5.0.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
emoji-regex-xs: 1.0.0
|
||||
regex: 5.1.1
|
||||
regex-recursion: 5.1.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
default-browser: 5.2.1
|
||||
define-lazy-prop: 3.0.0
|
||||
is-inside-container: 1.0.0
|
||||
is-wsl: 3.1.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
is-docker: 2.2.1
|
||||
@@ -30575,6 +30727,18 @@ snapshots:
|
||||
string-width: 6.1.0
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
chalk: 5.4.1
|
||||
cli-cursor: 5.0.0
|
||||
cli-spinners: 2.9.2
|
||||
is-interactive: 2.0.0
|
||||
is-unicode-supported: 2.1.0
|
||||
log-symbols: 6.0.0
|
||||
stdin-discarder: 0.2.2
|
||||
string-width: 7.2.0
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -32101,6 +32265,11 @@ snapshots:
|
||||
onetime: 5.1.2
|
||||
signal-exit: 3.0.7
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
onetime: 7.0.0
|
||||
signal-exit: 4.1.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -32190,6 +32359,8 @@ snapshots:
|
||||
bufferutil: 4.0.9
|
||||
utf-8-validate: 5.0.10
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
@@ -32651,6 +32822,8 @@ snapshots:
|
||||
dependencies:
|
||||
bl: 5.1.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]([email protected])([email protected])([email protected]):
|
||||
dependencies:
|
||||
'@storybook/core': 8.6.4([email protected])([email protected])([email protected]([email protected])([email protected])([email protected]))([email protected])
|
||||
@@ -32708,6 +32881,12 @@ snapshots:
|
||||
emoji-regex: 10.4.0
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
emoji-regex: 10.4.0
|
||||
get-east-asian-width: 1.3.0
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -33185,6 +33364,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
|
||||
Reference in New Issue
Block a user