Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fecd3a6c16 | ||
|
|
8aeb4d6308 | ||
|
|
f1cd2539d1 | ||
|
|
a322e16853 | ||
|
|
e50d77d28f | ||
|
|
7f3b91da37 | ||
|
|
41d50443d9 | ||
|
|
a8ac98bd49 | ||
|
|
0781da9dca | ||
|
|
c5dfad8dbb | ||
|
|
9663079062 | ||
|
|
1f6bb7c329 | ||
|
|
30e13e6b91 | ||
|
|
2dfc245d44 | ||
|
|
64d7bf358f | ||
|
|
a6e9c97c6f | ||
|
|
ee57ded902 | ||
|
|
6d3947ab0c | ||
|
|
632f1d138f | ||
|
|
18f0e575c4 |
@@ -337,6 +337,12 @@ async function redirects() {
|
||||
destination: "/connect/account-abstraction",
|
||||
permanent: false,
|
||||
},
|
||||
// redirect /connect/pay to /connect/universal-bridge
|
||||
{
|
||||
source: "/connect/pay",
|
||||
destination: "/connect/universal-bridge",
|
||||
permanent: false,
|
||||
},
|
||||
// PREVIOUS CAMPAIGNS
|
||||
{
|
||||
source: "/unlimited-wallets",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type AccountValidationSchema,
|
||||
accountValidationSchema,
|
||||
@@ -81,15 +82,21 @@ export const AccountForm: React.FC<AccountFormProps> = ({
|
||||
data,
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
const error = err as Error;
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
|
||||
if (
|
||||
onDuplicateError &&
|
||||
error?.message.match(/email address already exists/)
|
||||
) {
|
||||
onDuplicateError(values.email);
|
||||
return;
|
||||
} else if (error.message.includes("INVALID_EMAIL_ADDRESS")) {
|
||||
toast.error("Invalid Email Address");
|
||||
} else {
|
||||
toast.error(error.message || "Failed to update account");
|
||||
}
|
||||
|
||||
trackEvent({
|
||||
category: "account",
|
||||
action: "update",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# thirdweb-login
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`f1cd253`](https://github.com/thirdweb-dev/js/commit/f1cd2539d1be15eb18807b3f5f2b90509e3d58cf)]:
|
||||
- thirdweb@5.88.1
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1f6bb7c`](https://github.com/thirdweb-dev/js/commit/1f6bb7c3294d70648b120a6a6a6cba13302a84fc), [`30e13e6`](https://github.com/thirdweb-dev/js/commit/30e13e6b9176265a2f4eddfa53578889abbcb750), [`2dfc245`](https://github.com/thirdweb-dev/js/commit/2dfc245d44dde86e42f6c799305db707316432aa), [`ee57ded`](https://github.com/thirdweb-dev/js/commit/ee57ded902cb69da6fc171599a4a90776e650149), [`9663079`](https://github.com/thirdweb-dev/js/commit/966307906212ac99dc0a2a9be88e514c920d39c4), [`64d7bf3`](https://github.com/thirdweb-dev/js/commit/64d7bf358fe2014b684688d41d525a75e47f1b82)]:
|
||||
- thirdweb@5.88.0
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "thirdweb-login",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
"prelint": "biome check ./src",
|
||||
"lint": "eslint ./src",
|
||||
"prefix": "biome check ./src --fix",
|
||||
"fix": "eslint ./src --fix"
|
||||
"fix": "eslint ./src --fix",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@abstract-foundation/agw-client": "^1.4.0",
|
||||
"@abstract-foundation/agw-react": "^1.5.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "1.1.5",
|
||||
@@ -36,16 +38,20 @@
|
||||
"lucide-react": "0.474.0",
|
||||
"next": "15.1.6",
|
||||
"next-themes": "^0.4.4",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"openapi-types": "^12.1.3",
|
||||
"prettier": "3.3.3",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-hook-form": "7.54.2",
|
||||
"react-pick-color": "^2.0.0",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "1.27.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"thirdweb": "workspace:*",
|
||||
"timeago.js": "^4.0.2",
|
||||
"use-debounce": "^10.0.4"
|
||||
"use-debounce": "^10.0.4",
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.13.0",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@@ -1,34 +1,34 @@
|
||||
import thirdwebIconSrc from "@/../public/thirdweb.svg";
|
||||
import { Sidebar } from "@/components/ui/sidebar";
|
||||
import { Sidebar, type SidebarLink } from "@/components/ui/sidebar";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { navLinks } from "./navLinks";
|
||||
import { ScrollShadow } from "../components/ui/ScrollShadow/ScrollShadow";
|
||||
import { otherLinks } from "./otherLinks";
|
||||
|
||||
export function AppSidebar() {
|
||||
export function AppSidebar(props: {
|
||||
links: SidebarLink[];
|
||||
}) {
|
||||
return (
|
||||
<div className="sticky top-0 z-10 hidden h-dvh w-[300px] flex-col border-border/50 border-r-2 xl:flex">
|
||||
<div className="px-6 pt-6">
|
||||
<Link
|
||||
className="flex items-center gap-2"
|
||||
href="/"
|
||||
aria-label="thirdweb Docs"
|
||||
title="thirdweb Docs"
|
||||
>
|
||||
<Image src={thirdwebIconSrc} className="size-6" alt="" />
|
||||
<div className="z-10 hidden h-dvh w-[300px] flex-col border-border/50 border-r-2 xl:flex">
|
||||
<div className="border-b px-6 py-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Image src={thirdwebIconSrc} className="size-6" alt="thirdweb" />
|
||||
<span className="font-bold text-lg leading-none tracking-tight">
|
||||
Playground
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-5" />
|
||||
|
||||
<div className="px-6">
|
||||
<Sidebar links={navLinks} />
|
||||
<div className="relative flex max-h-full flex-1 flex-col overflow-hidden">
|
||||
<ScrollShadow
|
||||
className="grow pr-4 pl-6"
|
||||
scrollableClassName="max-h-full pt-6"
|
||||
>
|
||||
<Sidebar links={props.links} />
|
||||
</ScrollShadow>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex flex-col gap-4 p-6">
|
||||
<div className="mt-auto flex flex-col gap-4 border-t px-6 py-6">
|
||||
{otherLinks.map((link) => {
|
||||
return (
|
||||
<Link
|
||||
|
||||
@@ -5,12 +5,14 @@ import { MenuIcon, XIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ScrollShadow } from "../components/ui/ScrollShadow/ScrollShadow";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Sidebar } from "../components/ui/sidebar";
|
||||
import { navLinks } from "./navLinks";
|
||||
import { Sidebar, type SidebarLink } from "../components/ui/sidebar";
|
||||
import { otherLinks } from "./otherLinks";
|
||||
|
||||
export function MobileHeader() {
|
||||
export function MobileHeader(props: {
|
||||
links: SidebarLink[];
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -26,43 +28,53 @@ export function MobileHeader() {
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
|
||||
<header
|
||||
className="sticky top-0 z-10 flex justify-between gap-4 border-b bg-background px-4 py-4 xl:hidden "
|
||||
onClick={(e) => {
|
||||
if (e.target instanceof HTMLAnchorElement) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
className="flex items-center gap-3"
|
||||
href="/"
|
||||
aria-label="thirdweb Docs"
|
||||
title="thirdweb Docs"
|
||||
>
|
||||
<Image src={thirdwebIconSrc} className="size-7" alt="" />
|
||||
<span className="font-bold text-xl leading-none tracking-tight">
|
||||
Playground
|
||||
</span>
|
||||
</Link>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="!h-auto p-2"
|
||||
onClick={() => {
|
||||
setIsOpen((v) => !v);
|
||||
}}
|
||||
>
|
||||
{!isOpen ? (
|
||||
<MenuIcon className="size-6" />
|
||||
) : (
|
||||
<XIcon className="size-6" />
|
||||
)}
|
||||
</Button>
|
||||
<>
|
||||
<header className="sticky top-0 z-10 flex justify-between gap-4 border-b bg-background px-4 py-4 xl:hidden">
|
||||
<Link
|
||||
className="flex items-center gap-3"
|
||||
href="/"
|
||||
aria-label="thirdweb Docs"
|
||||
title="thirdweb Docs"
|
||||
>
|
||||
<Image src={thirdwebIconSrc} className="size-7" alt="" />
|
||||
<span className="font-bold text-xl leading-none tracking-tight">
|
||||
Playground
|
||||
</span>
|
||||
</Link>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="!h-auto p-2"
|
||||
onClick={() => {
|
||||
setIsOpen((v) => !v);
|
||||
}}
|
||||
>
|
||||
{!isOpen ? (
|
||||
<MenuIcon className="size-6" />
|
||||
) : (
|
||||
<XIcon className="size-6" />
|
||||
)}
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
{isOpen && (
|
||||
<div className="fade-in-0 slide-in-from-top-5 fixed top-[75px] right-0 bottom-0 left-0 z-50 flex animate-in flex-col gap-6 overflow-auto bg-background p-6 duration-200">
|
||||
<Sidebar links={navLinks} />
|
||||
<div className="mt-auto flex flex-col gap-4">
|
||||
<div
|
||||
className="fade-in-0 slide-in-from-top-5 fixed top-[75px] right-0 bottom-0 left-0 z-50 flex animate-in flex-col bg-background duration-200"
|
||||
onClickCapture={(e) => {
|
||||
if (e.target instanceof HTMLElement && e.target.closest("a")) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="relative flex max-h-full flex-1 flex-col overflow-hidden">
|
||||
<ScrollShadow
|
||||
className="grow px-6"
|
||||
scrollableClassName="max-h-full pt-6"
|
||||
>
|
||||
<Sidebar links={props.links} />
|
||||
</ScrollShadow>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex flex-col gap-4 border-t px-6 py-6">
|
||||
{otherLinks.map((link) => {
|
||||
return (
|
||||
<Link
|
||||
@@ -78,6 +90,6 @@ export function MobileHeader() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { abstractWallet } from "@abstract-foundation/agw-react/thirdweb";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
arbitrumSepolia,
|
||||
baseSepolia,
|
||||
@@ -25,21 +25,26 @@ import { cn } from "../../../../lib/utils";
|
||||
import { CodeGen } from "../components/CodeGen";
|
||||
import type { ConnectPlaygroundOptions } from "../components/types";
|
||||
|
||||
type Tab = "modal" | "button" | "code";
|
||||
|
||||
export function RightSection(props: {
|
||||
connectOptions: ConnectPlaygroundOptions;
|
||||
tab?: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const previewTab = useMemo(
|
||||
() =>
|
||||
["modal", "button", "code"].includes(props.tab || "")
|
||||
? (props.tab as "modal" | "button" | "code")
|
||||
: "modal",
|
||||
[props.tab],
|
||||
);
|
||||
const setPreviewTab = (tab: "modal" | "button" | "code") => {
|
||||
router.push(`/connect/sign-in?tab=${tab}`);
|
||||
};
|
||||
const pathname = usePathname();
|
||||
const [previewTab, _setPreviewTab] = useState<Tab>(() => {
|
||||
return props.tab === "code"
|
||||
? "code"
|
||||
: props.tab === "button"
|
||||
? "button"
|
||||
: "modal";
|
||||
});
|
||||
|
||||
function setPreviewTab(tab: "modal" | "button" | "code") {
|
||||
_setPreviewTab(tab);
|
||||
window.history.replaceState({}, "", `${pathname}?tab=${tab}`);
|
||||
}
|
||||
|
||||
const { connectOptions } = props;
|
||||
const wallet = useActiveWallet();
|
||||
const account = useActiveAccount();
|
||||
|
||||
@@ -134,7 +134,7 @@ function WalletButton(props: {
|
||||
>
|
||||
<span className="flex items-center gap-3">
|
||||
<Img
|
||||
src={walletImage.data || ""}
|
||||
src={walletImage.data}
|
||||
alt=""
|
||||
className="size-7 rounded-lg"
|
||||
loading="lazy"
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--destructive-text: 357.15deg 100% 68.72%;
|
||||
--success-text: 142.09 70.56% 35.29%;
|
||||
--warning-text: 38 92% 40%;
|
||||
--inverted-foreground: 0 0% 100%;
|
||||
--inverted: 0 0% 4%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 5.9% 10%;
|
||||
@@ -35,7 +40,7 @@
|
||||
.dark {
|
||||
--background: 240deg 2% 11%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240deg 2% 11%;
|
||||
--card: 240deg 2% 13%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240deg 2% 11%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
@@ -49,7 +54,13 @@
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--destructive-text: 360 72% 55%;
|
||||
--warning-text: 38 92% 50%;
|
||||
--success-text: 142 75% 50%;
|
||||
--border: 240deg 2% 20%;
|
||||
--active-border: 240deg 2% 25%;
|
||||
--inverted-foreground: 0 0% 0%;
|
||||
--inverted: 0 0% 100%;
|
||||
--input: 240deg 2% 20%;
|
||||
--ring: 240deg 2% 30%;
|
||||
--chart-1: 220 70% 50%;
|
||||
@@ -79,3 +90,17 @@ html.dark .shiki span {
|
||||
font-weight: var(--shiki-dark-font-weight) !important;
|
||||
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.no-scrollbar::-webkit-scrollbar,
|
||||
.no-scrollbar *::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.no-scrollbar,
|
||||
.no-scrollbar * {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
|
||||
async function fetchChainsFromApi() {
|
||||
const res = await fetch("https://api.thirdweb.com/v1/chains");
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.data as ChainMetadata[];
|
||||
}
|
||||
|
||||
export function useAllChainsData() {
|
||||
const query = useQuery({
|
||||
queryKey: ["all-chains"],
|
||||
queryFn: async () => {
|
||||
const idToChain = new Map<number, ChainMetadata>();
|
||||
const chains = await fetchChainsFromApi();
|
||||
|
||||
for (const c of chains) {
|
||||
idToChain.set(c.chainId, c);
|
||||
}
|
||||
|
||||
return {
|
||||
allChains: chains,
|
||||
idToChain,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
isPending: query.isLoading,
|
||||
data: query.data || { allChains: [], idToChain: new Map() },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
/**
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function useShowMore<T extends HTMLElement>(
|
||||
initialItemsToShow: number,
|
||||
itemsToAdd: number,
|
||||
) {
|
||||
// start with showing first `initialItemsToShow` items, when the last item is in view, show `itemsToAdd` more
|
||||
const [itemsToShow, setItemsToShow] = useState(initialItemsToShow);
|
||||
const lastItemRef = useCallback(
|
||||
(node: T) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0]?.isIntersecting) {
|
||||
setItemsToShow((prev) => prev + itemsToAdd); // show 10 more items
|
||||
}
|
||||
},
|
||||
{ threshold: 1 },
|
||||
);
|
||||
|
||||
observer.observe(node);
|
||||
// when the node is removed from the DOM, observer will be disconnected automatically by the browser
|
||||
},
|
||||
[itemsToAdd],
|
||||
);
|
||||
|
||||
return { itemsToShow, lastItemRef };
|
||||
}
|
||||
@@ -0,0 +1,886 @@
|
||||
"use client";
|
||||
|
||||
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import { CodeClient, CodeLoading } from "@/components/code/code.client";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowDownLeftIcon,
|
||||
ArrowUpRightIcon,
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
InfoIcon,
|
||||
PlayIcon,
|
||||
} from "lucide-react";
|
||||
import type { OpenAPIV3 } from "openapi-types";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
type ControllerRenderProps,
|
||||
type UseFormReturn,
|
||||
useForm,
|
||||
} from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { isProd } from "../../../lib/env";
|
||||
import type { BlueprintParameter, BlueprintPathMetadata } from "../utils";
|
||||
|
||||
export function BlueprintPlayground(props: {
|
||||
metadata: BlueprintPathMetadata;
|
||||
backLink: string;
|
||||
clientId: string;
|
||||
path: string;
|
||||
supportedChainIds: number[];
|
||||
}) {
|
||||
const [abortController, setAbortController] =
|
||||
useState<AbortController | null>(null);
|
||||
|
||||
const requestMutation = useMutation({
|
||||
mutationFn: async (url: string) => {
|
||||
const controller = new AbortController();
|
||||
setAbortController(controller);
|
||||
const start = performance.now();
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
return {
|
||||
status: res.status,
|
||||
data: await res.text(),
|
||||
time: performance.now() - start,
|
||||
};
|
||||
} catch (e) {
|
||||
const time = performance.now() - start;
|
||||
if (e instanceof Error) {
|
||||
return {
|
||||
data: e.message,
|
||||
time: time,
|
||||
};
|
||||
}
|
||||
return {
|
||||
data: "Failed to fetch",
|
||||
time: time,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const thirdwebDomain = !isProd ? "thirdweb-dev" : "thirdweb";
|
||||
|
||||
return (
|
||||
<BlueprintPlaygroundUI
|
||||
backLink={props.backLink}
|
||||
clientId={props.clientId}
|
||||
metadata={props.metadata}
|
||||
isPending={requestMutation.isPending}
|
||||
onRun={(url) => {
|
||||
requestMutation.mutate(url);
|
||||
}}
|
||||
response={
|
||||
abortController?.signal.aborted ? undefined : requestMutation.data
|
||||
}
|
||||
abortRequest={() => {
|
||||
if (abortController) {
|
||||
// just abort it - don't set a new controller
|
||||
abortController.abort();
|
||||
}
|
||||
}}
|
||||
domain={`https://insight.${thirdwebDomain}.com`}
|
||||
path={props.path}
|
||||
supportedChainIds={props.supportedChainIds}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function modifyParametersForPlayground(_parameters: BlueprintParameter[]) {
|
||||
const parameters = [..._parameters];
|
||||
|
||||
// make chain query param required - its not required in open api spec - because it either has to be set in subdomain or as a query param
|
||||
const chainIdParameter = parameters.find((p) => p.name === "chain");
|
||||
if (chainIdParameter) {
|
||||
chainIdParameter.required = true;
|
||||
}
|
||||
|
||||
// remove the client id parameter if it is present - we will always replace the parameter with project's client id
|
||||
const clientIdParameterIndex = parameters.findIndex(
|
||||
(p) => p.name === "clientId",
|
||||
);
|
||||
if (clientIdParameterIndex !== -1) {
|
||||
parameters.splice(clientIdParameterIndex, 1);
|
||||
}
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
export function BlueprintPlaygroundUI(props: {
|
||||
backLink: string;
|
||||
isPending: boolean;
|
||||
onRun: (url: string) => void;
|
||||
response:
|
||||
| {
|
||||
time: number;
|
||||
data: undefined | string;
|
||||
status?: number;
|
||||
}
|
||||
| undefined;
|
||||
clientId: string;
|
||||
abortRequest: () => void;
|
||||
domain: string;
|
||||
path: string;
|
||||
metadata: BlueprintPathMetadata;
|
||||
supportedChainIds: number[];
|
||||
}) {
|
||||
const parameters = useMemo(() => {
|
||||
const filteredParams = props.metadata.parameters?.filter(
|
||||
isOpenAPIV3ParameterObject,
|
||||
);
|
||||
return modifyParametersForPlayground(filteredParams || []);
|
||||
}, [props.metadata.parameters]);
|
||||
|
||||
const formSchema = useMemo(() => {
|
||||
return createParametersFormSchema(parameters);
|
||||
}, [parameters]);
|
||||
|
||||
const defaultValues = useMemo(() => {
|
||||
const values: Record<string, string | number> = {};
|
||||
for (const param of parameters) {
|
||||
if (param.name === "chain") {
|
||||
values.chain = "1";
|
||||
} else if (
|
||||
param.schema &&
|
||||
"type" in param.schema &&
|
||||
param.schema.default
|
||||
) {
|
||||
values[param.name] = param.schema.default;
|
||||
} else {
|
||||
values[param.name] = "";
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}, [parameters]);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: defaultValues,
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
const url = createBlueprintUrl({
|
||||
parameters: parameters,
|
||||
values: values,
|
||||
clientId: props.clientId,
|
||||
domain: props.domain,
|
||||
path: props.path,
|
||||
intent: "run",
|
||||
});
|
||||
|
||||
props.onRun(url);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="flex grow flex-col">
|
||||
<div className="flex grow flex-col pb-20">
|
||||
<div className="flex grow flex-col overflow-hidden rounded-xl border bg-card">
|
||||
<PlaygroundHeader
|
||||
domain={props.domain}
|
||||
parameters={parameters}
|
||||
path={props.path}
|
||||
isPending={props.isPending}
|
||||
getFormValues={() => form.getValues()}
|
||||
clientId={props.clientId}
|
||||
/>
|
||||
<div className="grid grow grid-cols-1 lg:grid-cols-2">
|
||||
<div className="flex max-h-[500px] grow flex-col max-sm:border-b lg:max-h-[740px] lg:border-r">
|
||||
<RequestConfigSection
|
||||
domain={props.domain}
|
||||
parameters={parameters}
|
||||
path={props.path}
|
||||
form={form}
|
||||
supportedChainIds={props.supportedChainIds}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex h-[500px] grow flex-col lg:h-[740px]">
|
||||
<ResponseSection
|
||||
isPending={props.isPending}
|
||||
response={props.response}
|
||||
abortRequest={props.abortRequest}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function PlaygroundHeader(props: {
|
||||
parameters: BlueprintParameter[];
|
||||
isPending: boolean;
|
||||
getFormValues: () => Record<string, string>;
|
||||
clientId: string;
|
||||
domain: string;
|
||||
path: string;
|
||||
}) {
|
||||
const [hasCopied, setHasCopied] = useState(false);
|
||||
return (
|
||||
<div className="border-b px-4 py-4 lg:flex lg:justify-center lg:py-3">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2 overflow-hidden rounded-xl border bg-muted/50 p-2 lg:justify-center">
|
||||
{/* copy url */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hover:accent h-auto w-auto p-1"
|
||||
onClick={() => {
|
||||
setHasCopied(true);
|
||||
const url = createBlueprintUrl({
|
||||
clientId: props.clientId,
|
||||
parameters: props.parameters,
|
||||
domain: props.domain,
|
||||
path: props.path,
|
||||
values: props.getFormValues(),
|
||||
intent: "copy",
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
setHasCopied(false);
|
||||
}, 500);
|
||||
navigator.clipboard.writeText(url);
|
||||
}}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="size-4 text-green-500" />
|
||||
) : (
|
||||
<CopyIcon className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* vertical line */}
|
||||
<div className="h-6 w-[1px] bg-border" />
|
||||
|
||||
{/* domain + path */}
|
||||
<div className="flex items-center gap-1.5 overflow-hidden">
|
||||
<div className="rounded-xl bg-muted-foreground/10 px-2.5 py-1 font-mono text-xs lg:text-sm">
|
||||
<span className="hidden lg:inline">{props.domain}</span>
|
||||
<span className="lg:hidden">...</span>
|
||||
</div>
|
||||
<div className="truncate font-mono text-xs lg:text-sm">
|
||||
{props.path}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Run */}
|
||||
<Button
|
||||
className="ml-2 hidden h-auto w-auto gap-1.5 p-1 px-2 lg:flex"
|
||||
disabled={props.isPending}
|
||||
type="submit"
|
||||
>
|
||||
{props.isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<PlayIcon className="size-4" />
|
||||
)}
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
className="gap-2 rounded-lg lg:hidden"
|
||||
type="submit"
|
||||
disabled={props.isPending}
|
||||
>
|
||||
{props.isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<PlayIcon className="size-4" />
|
||||
)}
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RequestConfigSection(props: {
|
||||
parameters: BlueprintParameter[];
|
||||
form: ParametersForm;
|
||||
domain: string;
|
||||
path: string;
|
||||
supportedChainIds: number[];
|
||||
}) {
|
||||
const { pathVariables, queryParams, filterQueryParams } = useMemo(() => {
|
||||
const pathVariables: OpenAPIV3.ParameterObject[] = [];
|
||||
const queryParams: OpenAPIV3.ParameterObject[] = [];
|
||||
const filterQueryParams: OpenAPIV3.ParameterObject[] = [];
|
||||
|
||||
for (const param of props.parameters) {
|
||||
if (param.in === "path") {
|
||||
pathVariables.push(param);
|
||||
}
|
||||
|
||||
if (param.in === "query") {
|
||||
if (param.name.startsWith("filter_")) {
|
||||
filterQueryParams.push(param);
|
||||
} else {
|
||||
queryParams.push(param);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pathVariables,
|
||||
queryParams,
|
||||
filterQueryParams,
|
||||
};
|
||||
}, [props.parameters]);
|
||||
|
||||
const showError =
|
||||
!props.form.formState.isValid &&
|
||||
props.form.formState.isDirty &&
|
||||
props.form.formState.isSubmitted;
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col overflow-hidden">
|
||||
<div className="flex min-h-[60px] items-center justify-between gap-2 border-b p-4 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowUpRightIcon className="size-5" />
|
||||
Request
|
||||
</div>
|
||||
{showError && <Badge variant="destructive">Invalid Request</Badge>}
|
||||
</div>
|
||||
|
||||
<ScrollShadow className="flex-1" scrollableClassName="max-h-full">
|
||||
{pathVariables.length > 0 && (
|
||||
<ParameterSection
|
||||
parameters={pathVariables}
|
||||
className="border-b"
|
||||
title="Path Variables"
|
||||
form={props.form}
|
||||
domain={props.domain}
|
||||
path={props.path}
|
||||
supportedChainIds={props.supportedChainIds}
|
||||
/>
|
||||
)}
|
||||
|
||||
{queryParams.length > 0 && (
|
||||
<ParameterSection
|
||||
className="border-b"
|
||||
parameters={queryParams}
|
||||
title="Query Parameters"
|
||||
form={props.form}
|
||||
domain={props.domain}
|
||||
path={props.path}
|
||||
supportedChainIds={props.supportedChainIds}
|
||||
/>
|
||||
)}
|
||||
|
||||
{filterQueryParams.length > 0 && (
|
||||
<ParameterSection
|
||||
parameters={filterQueryParams}
|
||||
title="Filter Query Parameters"
|
||||
form={props.form}
|
||||
domain={props.domain}
|
||||
path={props.path}
|
||||
supportedChainIds={props.supportedChainIds}
|
||||
/>
|
||||
)}
|
||||
</ScrollShadow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ParametersForm = UseFormReturn<{
|
||||
[x: string]: string | number;
|
||||
}>;
|
||||
|
||||
function ParameterSection(props: {
|
||||
parameters: BlueprintParameter[];
|
||||
title: string;
|
||||
form: ParametersForm;
|
||||
domain: string;
|
||||
path: string;
|
||||
supportedChainIds: number[];
|
||||
className?: string;
|
||||
}) {
|
||||
const url = `${props.domain}${props.path}`;
|
||||
return (
|
||||
<div className={cn("p-4 py-6", props.className)}>
|
||||
<h3 className="mb-3 font-medium text-sm"> {props.title} </h3>
|
||||
<div className="overflow-hidden rounded-lg border">
|
||||
{props.parameters.map((param, i) => {
|
||||
const description =
|
||||
param.schema && "type" in param.schema
|
||||
? param.schema.description
|
||||
: undefined;
|
||||
|
||||
const example =
|
||||
param.schema && "type" in param.schema
|
||||
? param.schema.example
|
||||
: undefined;
|
||||
const exampleToShow =
|
||||
typeof example === "string" || typeof example === "number"
|
||||
? example
|
||||
: undefined;
|
||||
|
||||
const showTip = description !== undefined || example !== undefined;
|
||||
|
||||
const hasError = !!props.form.formState.errors[param.name];
|
||||
|
||||
const placeholder = url.includes(`{${param.name}}`)
|
||||
? `{${param.name}}`
|
||||
: url.includes(`:${param.name}`)
|
||||
? `:${param.name}`
|
||||
: "Value";
|
||||
|
||||
return (
|
||||
<FormField
|
||||
key={param.name}
|
||||
control={props.form.control}
|
||||
name={param.name}
|
||||
render={({ field }) => (
|
||||
<FormItem
|
||||
className={cn(
|
||||
"space-y-0",
|
||||
i !== props.parameters.length - 1 && "border-b",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
key={param.name}
|
||||
className={cn(
|
||||
"grid items-center",
|
||||
param.name === "chain"
|
||||
? "grid-cols-1 lg:grid-cols-2"
|
||||
: "grid-cols-2",
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full flex-row flex-wrap items-center justify-between gap-1 border-r px-3 py-2">
|
||||
<div className="font-medium font-mono text-sm">
|
||||
{param.name}
|
||||
</div>
|
||||
{param.required && (
|
||||
<Badge
|
||||
className="px-2 text-muted-foreground"
|
||||
variant="secondary"
|
||||
>
|
||||
Required
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative">
|
||||
{param.name === "chain" ? (
|
||||
<SingleNetworkSelector
|
||||
chainId={
|
||||
field.value ? Number(field.value) : undefined
|
||||
}
|
||||
onChange={(chainId) => {
|
||||
field.onChange({
|
||||
target: { value: chainId.toString() },
|
||||
});
|
||||
}}
|
||||
className="rounded-none border-0 border-t lg:border-none"
|
||||
popoverContentClassName="min-w-[calc(100vw-20px)] lg:min-w-[500px]"
|
||||
chainIds={
|
||||
props.supportedChainIds.length > 0
|
||||
? props.supportedChainIds
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ParameterInput
|
||||
param={param}
|
||||
field={field}
|
||||
showTip={showTip}
|
||||
hasError={hasError}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
|
||||
{showTip && (
|
||||
<ToolTipLabel
|
||||
hoverable
|
||||
contentClassName="max-w-[100vw] break-all"
|
||||
label={
|
||||
<div className="flex flex-col gap-2">
|
||||
{description && (
|
||||
<p className="text-foreground">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{exampleToShow !== undefined && (
|
||||
<div>
|
||||
<p className="mb-1 text-muted-foreground">
|
||||
Example:{" "}
|
||||
<span className="font-mono">
|
||||
{exampleToShow}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
className="-translate-y-1/2 absolute top-1/2 right-2 hidden h-auto w-auto p-1.5 text-muted-foreground opacity-50 hover:opacity-100 lg:flex"
|
||||
>
|
||||
<div>
|
||||
<InfoIcon className="size-4" />
|
||||
</div>
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FormMessage className="mt-0 border-destructive-text border-t px-3 py-2" />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ParameterInput(props: {
|
||||
param: OpenAPIV3.ParameterObject;
|
||||
field: ControllerRenderProps<
|
||||
{
|
||||
[x: string]: string | number;
|
||||
},
|
||||
string
|
||||
>;
|
||||
showTip: boolean;
|
||||
hasError: boolean;
|
||||
placeholder: string;
|
||||
}) {
|
||||
const { param, field, showTip, hasError, placeholder } = props;
|
||||
|
||||
if (param.schema && "type" in param.schema && param.schema.enum) {
|
||||
const { value, onChange, ...restField } = field;
|
||||
return (
|
||||
<Select
|
||||
{...restField}
|
||||
value={value.toString()}
|
||||
onValueChange={(v) => {
|
||||
onChange({ target: { value: v } });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"border-none bg-transparent pr-10 font-mono focus:ring-0 focus:ring-offset-0",
|
||||
value === "" && "text-muted-foreground",
|
||||
)}
|
||||
chevronClassName="hidden"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent className="font-mono">
|
||||
{param.schema.enum.map((val) => {
|
||||
return (
|
||||
<SelectItem value={val} key={val}>
|
||||
{val}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...field}
|
||||
className={cn(
|
||||
"h-auto truncate rounded-none border-0 bg-transparent py-3 font-mono text-sm focus-visible:ring-0 focus-visible:ring-offset-0",
|
||||
showTip && "lg:pr-10",
|
||||
hasError && "text-destructive-text",
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function formatMilliseconds(ms: number) {
|
||||
if (ms < 1000) {
|
||||
return `${Math.round(ms)}ms`;
|
||||
}
|
||||
return `${(ms / 1000).toFixed(2)}s`;
|
||||
}
|
||||
|
||||
function ResponseSection(props: {
|
||||
isPending: boolean;
|
||||
response:
|
||||
| { data: undefined | string; status?: number; time: number }
|
||||
| undefined;
|
||||
abortRequest: () => void;
|
||||
}) {
|
||||
const formattedData = useMemo(() => {
|
||||
if (!props.response?.data) return undefined;
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(props.response.data), null, 2);
|
||||
} catch {
|
||||
return props.response.data;
|
||||
}
|
||||
}, [props.response]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full grow flex-col">
|
||||
<div className="flex min-h-[60px] items-center justify-between gap-2 border-b p-4 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowDownLeftIcon className="size-5" />
|
||||
Response
|
||||
{props.isPending && <ElapsedTimeCounter />}
|
||||
{props.response?.time && !props.isPending && (
|
||||
<span className="text-muted-foreground">
|
||||
{formatMilliseconds(props.response.time)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!props.isPending && props.response?.status && (
|
||||
<Badge
|
||||
variant={
|
||||
props.response.status >= 200 && props.response.status < 300
|
||||
? "success"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{props.response.status}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{props.isPending && (
|
||||
<div className="flex grow flex-col items-center justify-center gap-4">
|
||||
<Spinner className="size-14 text-muted-foreground" />
|
||||
<Button variant="ghost" onClick={props.abortRequest} size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!props.isPending && !props.response && (
|
||||
<div className="flex grow flex-col items-center justify-center p-4">
|
||||
<div>
|
||||
<div className="flex justify-center">
|
||||
<div className="rounded-xl border p-1">
|
||||
<div className="rounded-lg border bg-card p-1.5">
|
||||
<PlayIcon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 text-center">Click Run to start a request</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!props.isPending && props.response && (
|
||||
<CodeClient
|
||||
loader={<CodeLoading />}
|
||||
lang="json"
|
||||
code={formattedData || ""}
|
||||
className="rounded-none border-none bg-transparent"
|
||||
scrollableContainerClassName="h-full"
|
||||
scrollableClassName="h-full"
|
||||
// shadowColor="hsl(var(--muted)/50%)"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function openAPIV3ParamToZodFormSchema(
|
||||
schema: BlueprintParameter["schema"],
|
||||
isRequired: boolean,
|
||||
): z.ZodTypeAny | undefined {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("anyOf" in schema) {
|
||||
const anyOf = schema.anyOf;
|
||||
if (!anyOf) {
|
||||
return;
|
||||
}
|
||||
const anySchemas = anyOf
|
||||
.map((s) => openAPIV3ParamToZodFormSchema(s, isRequired))
|
||||
.filter((x) => !!x);
|
||||
// @ts-expect-error - Its ok, z.union is expecting tuple type but we have array
|
||||
return z.union(anySchemas);
|
||||
}
|
||||
|
||||
if (!("type" in schema)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if enum values
|
||||
const enumValues = schema.enum;
|
||||
if (enumValues) {
|
||||
const enumSchema = z.enum(
|
||||
// @ts-expect-error - Its correct
|
||||
enumValues,
|
||||
);
|
||||
|
||||
if (isRequired) {
|
||||
return enumSchema;
|
||||
}
|
||||
|
||||
return enumSchema.or(z.literal(""));
|
||||
}
|
||||
|
||||
switch (schema.type) {
|
||||
case "integer": {
|
||||
const intSchema = z.coerce
|
||||
.number({
|
||||
message: "Must be an integer",
|
||||
})
|
||||
.int({
|
||||
message: "Must be an integer",
|
||||
});
|
||||
return isRequired
|
||||
? intSchema.min(1, {
|
||||
message: "Required",
|
||||
})
|
||||
: intSchema.optional();
|
||||
}
|
||||
|
||||
case "number": {
|
||||
const numberSchema = z.coerce.number();
|
||||
return isRequired
|
||||
? numberSchema.min(1, {
|
||||
message: "Required",
|
||||
})
|
||||
: numberSchema.optional();
|
||||
}
|
||||
|
||||
case "boolean": {
|
||||
const booleanSchema = z.coerce.boolean();
|
||||
return isRequired ? booleanSchema : booleanSchema.optional();
|
||||
}
|
||||
|
||||
// everything else - just accept it as a string;
|
||||
default: {
|
||||
const stringSchema = z.string();
|
||||
return isRequired
|
||||
? stringSchema.min(1, {
|
||||
message: "Required",
|
||||
})
|
||||
: stringSchema.optional();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createParametersFormSchema(parameters: BlueprintParameter[]) {
|
||||
const shape: z.ZodRawShape = {};
|
||||
for (const param of parameters) {
|
||||
const paramSchema = openAPIV3ParamToZodFormSchema(
|
||||
param.schema,
|
||||
!!param.required,
|
||||
);
|
||||
if (paramSchema) {
|
||||
shape[param.name] = paramSchema;
|
||||
} else {
|
||||
shape[param.name] = param.required
|
||||
? z.string().min(1, { message: "Required" })
|
||||
: z.string();
|
||||
}
|
||||
}
|
||||
|
||||
return z.object(shape);
|
||||
}
|
||||
|
||||
function createBlueprintUrl(options: {
|
||||
parameters: BlueprintParameter[];
|
||||
values: Record<string, string>;
|
||||
clientId: string;
|
||||
domain: string;
|
||||
path: string;
|
||||
intent: "copy" | "run";
|
||||
}) {
|
||||
const { parameters, domain, path, values, clientId } = options;
|
||||
|
||||
let url = `${domain}${path}`;
|
||||
// loop over the values and replace {x} or :x with the actual values for paths
|
||||
// and add query parameters
|
||||
const pathVariables = parameters.filter((param) => param.in === "path");
|
||||
|
||||
const queryParams = parameters.filter((param) => param.in === "query");
|
||||
|
||||
for (const parameter of pathVariables) {
|
||||
const value = values[parameter.name];
|
||||
if (value) {
|
||||
url = url.replace(`{${parameter.name}}`, value);
|
||||
url = url.replace(`:${parameter.name}`, value);
|
||||
}
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const parameter of queryParams) {
|
||||
const value = values[parameter.name];
|
||||
if (value) {
|
||||
searchParams.append(parameter.name, value);
|
||||
}
|
||||
}
|
||||
|
||||
// add client Id search param
|
||||
if (options.intent === "run") {
|
||||
searchParams.append("clientId", clientId);
|
||||
} else {
|
||||
searchParams.append("clientId", "YOUR_THIRDWEB_CLIENT_ID");
|
||||
}
|
||||
|
||||
if (searchParams.toString()) {
|
||||
url = `${url}?${searchParams.toString()}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
function ElapsedTimeCounter() {
|
||||
const [ms, setMs] = useState(0);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
const internal = 100;
|
||||
const id = setInterval(() => {
|
||||
setMs((prev) => prev + internal);
|
||||
}, internal);
|
||||
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<span className="fade-in-0 animate-in text-muted-foreground text-sm duration-300">
|
||||
{formatMilliseconds(ms)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function isOpenAPIV3ParameterObject(
|
||||
x: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject,
|
||||
): x is OpenAPIV3.ParameterObject {
|
||||
return !("$ref" in x);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { redirect } from "next/navigation";
|
||||
import { THIRDWEB_CLIENT } from "../../../lib/client";
|
||||
import { fetchBlueprintSpec } from "../utils";
|
||||
import { BlueprintPlayground } from "./blueprint-playground.client";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
blueprint_slug: string;
|
||||
}>;
|
||||
searchParams: Promise<{ path: string }>;
|
||||
}) {
|
||||
const [params, searchParams] = await Promise.all([
|
||||
props.params,
|
||||
props.searchParams,
|
||||
]);
|
||||
|
||||
// invalid url
|
||||
if (!searchParams.path) {
|
||||
redirect("/insight");
|
||||
}
|
||||
|
||||
const [blueprintSpec] = await Promise.all([
|
||||
fetchBlueprintSpec({
|
||||
blueprintId: params.blueprint_slug,
|
||||
}),
|
||||
]);
|
||||
|
||||
const pathMetadata = blueprintSpec.openapiJson.paths[searchParams.path]?.get;
|
||||
|
||||
// invalid url
|
||||
if (!pathMetadata) {
|
||||
redirect("/insight");
|
||||
}
|
||||
|
||||
const supportedChainIds =
|
||||
blueprintSpec.openapiJson.servers?.[0]?.variables?.chainId?.enum?.map(
|
||||
Number,
|
||||
) || [];
|
||||
|
||||
const title = pathMetadata.summary || "";
|
||||
return (
|
||||
<div>
|
||||
<Breadcrumbs />
|
||||
<h1 className="mt-3 mb-6 font-semibold text-2xl tracking-tight lg:text-3xl">
|
||||
{title}
|
||||
</h1>
|
||||
<BlueprintPlayground
|
||||
key={searchParams.path}
|
||||
metadata={pathMetadata}
|
||||
backLink={"/insight"}
|
||||
clientId={THIRDWEB_CLIENT.clientId}
|
||||
path={searchParams.path}
|
||||
supportedChainIds={supportedChainIds}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Breadcrumbs() {
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/insight">Insight Blueprints</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type React from "react";
|
||||
import { APIHeader } from "../../components/blocks/APIHeader";
|
||||
|
||||
export default function Layout(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<APIHeader
|
||||
title="Insight"
|
||||
description={
|
||||
<>Simple & customizable endpoints for querying rich blockchain data</>
|
||||
}
|
||||
docsLink="https://portal.thirdweb.com/insight"
|
||||
heroLink="/insight-hero.avif"
|
||||
/>
|
||||
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
|
||||
import {} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { fetchAllBlueprints } from "./utils";
|
||||
|
||||
export default async function Page() {
|
||||
const blueprints = await fetchAllBlueprints();
|
||||
|
||||
return (
|
||||
<div className="pb-20">
|
||||
<h2 className="mb-2 font-semibold text-2xl tracking-tight">Blueprints</h2>
|
||||
<p className="mb-5 text-muted-foreground">
|
||||
A blueprint is an API that provides access to on-chain data in a
|
||||
user-friendly format. <br /> There is no need for ABIs, decoding, RPC,
|
||||
or web3 knowledge to fetch blockchain data.{" "}
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/insight/blueprints"
|
||||
target="_blank"
|
||||
className="underline decoration-muted-foreground/50 decoration-dotted underline-offset-[5px] hover:decoration-solid"
|
||||
>
|
||||
Learn more about Insight Blueprints{" "}
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-8">
|
||||
{blueprints.map((blueprint) => {
|
||||
const paths = Object.keys(blueprint.openapiJson.paths);
|
||||
|
||||
return (
|
||||
<BlueprintSection
|
||||
key={blueprint.id}
|
||||
blueprintId={blueprint.id}
|
||||
title={blueprint.name}
|
||||
blueprints={paths.map((pathName) => {
|
||||
const pathObj = blueprint.openapiJson.paths[pathName];
|
||||
if (!pathObj) {
|
||||
throw new Error(`Path not found: ${pathName}`);
|
||||
}
|
||||
return {
|
||||
name: pathObj.get?.summary || "Unknown",
|
||||
link: `/insight/${blueprint.id}?path=${pathName}`,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BlueprintSection(props: {
|
||||
title: string;
|
||||
blueprintId: string;
|
||||
blueprints: { name: string; link: string }[];
|
||||
}) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border bg-card">
|
||||
<div className="flex items-center gap-2 border-b bg-accent/20 px-6 py-4">
|
||||
<h2 className="font-semibold text-lg tracking-tight">{props.title}</h2>
|
||||
</div>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{props.blueprints.map((item) => (
|
||||
<TableRow
|
||||
key={item.link}
|
||||
className="group hover:bg-accent/50"
|
||||
linkBox
|
||||
>
|
||||
<TableCell>
|
||||
<span className="flex items-center gap-3">
|
||||
<Link
|
||||
href={item.link}
|
||||
className="before:absolute before:inset-0"
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import "server-only";
|
||||
|
||||
import type { OpenAPIV3 } from "openapi-types";
|
||||
import { isProd } from "../../lib/env";
|
||||
|
||||
type BlueprintListItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
const thirdwebDomain = !isProd ? "thirdweb-dev" : "thirdweb";
|
||||
|
||||
async function fetchBlueprintList() {
|
||||
const res = await fetch(
|
||||
`https://insight.${thirdwebDomain}.com/v1/blueprints`,
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`Failed to fetch blueprints: ${text}`);
|
||||
}
|
||||
|
||||
const json = (await res.json()) as { data: BlueprintListItem[] };
|
||||
|
||||
return json.data;
|
||||
}
|
||||
|
||||
export type BlueprintParameter = OpenAPIV3.ParameterObject;
|
||||
export type BlueprintPathMetadata = OpenAPIV3.PathItemObject;
|
||||
|
||||
type BlueprintSpec = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
openapiJson: OpenAPIV3.Document;
|
||||
};
|
||||
|
||||
export async function fetchBlueprintSpec(params: {
|
||||
blueprintId: string;
|
||||
}) {
|
||||
const res = await fetch(
|
||||
`https://insight.${thirdwebDomain}.com/v1/blueprints/${params.blueprintId}`,
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`Failed to fetch blueprint: ${text}`);
|
||||
}
|
||||
|
||||
const json = (await res.json()) as { data: BlueprintSpec };
|
||||
|
||||
return json.data;
|
||||
}
|
||||
|
||||
export async function fetchAllBlueprints() {
|
||||
// fetch list
|
||||
const blueprintSpecs = await fetchBlueprintList();
|
||||
|
||||
// fetch all blueprints
|
||||
const blueprints = await Promise.all(
|
||||
blueprintSpecs.map((spec) =>
|
||||
fetchBlueprintSpec({
|
||||
blueprintId: spec.id,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return blueprints;
|
||||
}
|
||||
@@ -6,7 +6,9 @@ import Script from "next/script";
|
||||
import { AppSidebar } from "./AppSidebar";
|
||||
import { Providers } from "./providers";
|
||||
import "./globals.css";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
import { MobileHeader } from "./MobileHeader";
|
||||
import { getSidebarLinks } from "./navLinks";
|
||||
|
||||
const sansFont = Inter({
|
||||
subsets: ["latin"],
|
||||
@@ -26,11 +28,12 @@ export const metadata: Metadata = {
|
||||
description: "thirdweb playground",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const sidebarLinks = await getSidebarLinks();
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
@@ -49,14 +52,18 @@ export default function RootLayout({
|
||||
monoFont.variable,
|
||||
)}
|
||||
>
|
||||
<MobileHeader />
|
||||
<div className="relative">
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
<AppSidebar />
|
||||
<div className="flex grow flex-col">
|
||||
<div className="container relative grow px-4 md:px-6">
|
||||
<Providers>{children}</Providers>
|
||||
</div>
|
||||
<NextTopLoader
|
||||
color="hsl(var(--foreground))"
|
||||
height={2}
|
||||
shadow={false}
|
||||
showSpinner={false}
|
||||
/>
|
||||
<MobileHeader links={sidebarLinks} />
|
||||
<div className="flex flex-col lg:h-dvh lg:flex-row">
|
||||
<AppSidebar links={sidebarLinks} />
|
||||
<div className="flex grow flex-col lg:overflow-auto">
|
||||
<div className="container relative grow px-4 md:px-6">
|
||||
<Providers>{children}</Providers>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,140 +1,176 @@
|
||||
import type { SidebarLink } from "../components/ui/sidebar";
|
||||
import { fetchAllBlueprints } from "./insight/utils";
|
||||
|
||||
export const navLinks: SidebarLink[] = [
|
||||
export const staticSidebarLinks: SidebarLink[] = [
|
||||
{
|
||||
name: "Sign in",
|
||||
expanded: true,
|
||||
name: "Connect",
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
name: "Button",
|
||||
href: "/connect/sign-in/button",
|
||||
name: "Connect Wallet",
|
||||
expanded: false,
|
||||
links: [
|
||||
{
|
||||
name: "Button",
|
||||
href: "/connect/sign-in/button",
|
||||
},
|
||||
{
|
||||
name: "Embed",
|
||||
href: "/connect/sign-in/embed",
|
||||
},
|
||||
{
|
||||
name: "Headless",
|
||||
href: "/connect/sign-in/headless",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Embed",
|
||||
href: "/connect/sign-in/embed",
|
||||
name: "Account Abstraction",
|
||||
expanded: false,
|
||||
links: [
|
||||
{
|
||||
name: "Connect",
|
||||
href: "/connect/account-abstraction/connect",
|
||||
},
|
||||
{
|
||||
name: "Sponsor Gas",
|
||||
href: "/connect/account-abstraction/sponsor",
|
||||
},
|
||||
{
|
||||
name: "Native AA (zkSync)",
|
||||
href: "/connect/account-abstraction/native-aa",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Headless",
|
||||
href: "/connect/sign-in/headless",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Account Abstraction",
|
||||
expanded: false,
|
||||
links: [
|
||||
{
|
||||
name: "Connect",
|
||||
href: "/connect/account-abstraction/connect",
|
||||
name: "In-App Wallet",
|
||||
expanded: false,
|
||||
links: [
|
||||
{
|
||||
name: "Any Auth",
|
||||
href: "/connect/in-app-wallet",
|
||||
},
|
||||
{
|
||||
name: "Ecosystems",
|
||||
href: "/connect/in-app-wallet/ecosystem",
|
||||
},
|
||||
{
|
||||
name: "Sponsor Gas",
|
||||
href: "/connect/in-app-wallet/sponsor",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Sponsor Gas",
|
||||
href: "/connect/account-abstraction/sponsor",
|
||||
name: "Pay",
|
||||
expanded: false,
|
||||
links: [
|
||||
{
|
||||
name: "Top up",
|
||||
href: "/connect/pay",
|
||||
},
|
||||
{
|
||||
name: "Commerce",
|
||||
href: "/connect/pay/commerce",
|
||||
},
|
||||
{
|
||||
name: "Transactions",
|
||||
href: "/connect/pay/transactions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Native AA (zkSync)",
|
||||
href: "/connect/account-abstraction/native-aa",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "In-App Wallet",
|
||||
expanded: false,
|
||||
links: [
|
||||
{
|
||||
name: "Any Auth",
|
||||
href: "/connect/in-app-wallet",
|
||||
name: "Auth",
|
||||
href: "/connect/auth",
|
||||
},
|
||||
{
|
||||
name: "Ecosystems",
|
||||
href: "/connect/in-app-wallet/ecosystem",
|
||||
name: "Social",
|
||||
href: "/connect/social",
|
||||
},
|
||||
{
|
||||
name: "Sponsor Gas",
|
||||
href: "/connect/in-app-wallet/sponsor",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Pay",
|
||||
expanded: false,
|
||||
links: [
|
||||
{
|
||||
name: "Top up",
|
||||
href: "/connect/pay",
|
||||
name: "Blockchain API",
|
||||
href: "/connect/blockchain-api",
|
||||
},
|
||||
{
|
||||
name: "Commerce",
|
||||
href: "/connect/pay/commerce",
|
||||
},
|
||||
{
|
||||
name: "Transactions",
|
||||
href: "/connect/pay/transactions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Engine",
|
||||
expanded: false,
|
||||
links: [
|
||||
{
|
||||
name: "Airdrop",
|
||||
href: "/engine/airdrop",
|
||||
},
|
||||
{
|
||||
name: "Minting",
|
||||
href: "/engine/minting",
|
||||
},
|
||||
{
|
||||
name: "Webhooks",
|
||||
href: "/engine/webhooks",
|
||||
},
|
||||
// {
|
||||
// name: "Session Keys",
|
||||
// href: "/engine/account-abstraction/session-keys",
|
||||
// },
|
||||
// {
|
||||
// name: "Smart Backend Wallets",
|
||||
// href: "/engine/account-abstraction/smart-backend-wallets",
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Auth",
|
||||
href: "/connect/auth",
|
||||
},
|
||||
{
|
||||
name: "Social",
|
||||
href: "/connect/social",
|
||||
},
|
||||
{
|
||||
name: "Blockchain API",
|
||||
href: "/connect/blockchain-api",
|
||||
},
|
||||
{
|
||||
name: "Headless UI",
|
||||
expanded: true,
|
||||
links: [
|
||||
{
|
||||
name: "Account",
|
||||
href: "/connect/ui",
|
||||
},
|
||||
{
|
||||
name: "NFT",
|
||||
href: "/connect/ui/nft",
|
||||
},
|
||||
{
|
||||
name: "Token",
|
||||
href: "/connect/ui/token",
|
||||
},
|
||||
{
|
||||
name: "Chain",
|
||||
href: "/connect/ui/chain",
|
||||
},
|
||||
{
|
||||
name: "Wallet",
|
||||
href: "/connect/ui/wallet",
|
||||
name: "Headless UI",
|
||||
expanded: false,
|
||||
links: [
|
||||
{
|
||||
name: "Account",
|
||||
href: "/connect/ui",
|
||||
},
|
||||
{
|
||||
name: "NFT",
|
||||
href: "/connect/ui/nft",
|
||||
},
|
||||
{
|
||||
name: "Token",
|
||||
href: "/connect/ui/token",
|
||||
},
|
||||
{
|
||||
name: "Chain",
|
||||
href: "/connect/ui/chain",
|
||||
},
|
||||
{
|
||||
name: "Wallet",
|
||||
href: "/connect/ui/wallet",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const engineSidebarLinks: SidebarLink = {
|
||||
name: "Engine",
|
||||
isCollapsible: false,
|
||||
expanded: false,
|
||||
links: [
|
||||
{
|
||||
name: "Airdrop",
|
||||
href: "/engine/airdrop",
|
||||
},
|
||||
{
|
||||
name: "Minting",
|
||||
href: "/engine/minting",
|
||||
},
|
||||
{
|
||||
name: "Webhooks",
|
||||
href: "/engine/webhooks",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export async function getSidebarLinks() {
|
||||
const insightBlueprints = await fetchAllBlueprints();
|
||||
|
||||
const insightLinks: SidebarLink[] = insightBlueprints.map((blueprint) => {
|
||||
const paths = Object.keys(blueprint.openapiJson.paths);
|
||||
return {
|
||||
name: blueprint.name,
|
||||
expanded: false,
|
||||
links: paths.map((pathName) => {
|
||||
const pathObj = blueprint.openapiJson.paths[pathName];
|
||||
if (!pathObj) {
|
||||
throw new Error(`Path not found: ${pathName}`);
|
||||
}
|
||||
return {
|
||||
name: pathObj.get?.summary || pathName,
|
||||
href: `/insight/${blueprint.id}?path=${pathName}`,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const sidebarLinks: SidebarLink[] = [
|
||||
...staticSidebarLinks,
|
||||
{
|
||||
name: "Insight",
|
||||
isCollapsible: false,
|
||||
expanded: false,
|
||||
links: insightLinks,
|
||||
},
|
||||
engineSidebarLinks,
|
||||
];
|
||||
|
||||
return sidebarLinks;
|
||||
}
|
||||
|
||||
@@ -1,95 +1,5 @@
|
||||
import {
|
||||
PayIcon,
|
||||
WalletsAuthIcon,
|
||||
WalletsConnectIcon,
|
||||
WalletsInAppIcon,
|
||||
WalletsSmartIcon,
|
||||
} from "@/icons";
|
||||
import { CodeIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<main className="container flex-1">
|
||||
<WalletsSection />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function WalletsSection() {
|
||||
return (
|
||||
<section className="my-12">
|
||||
<h2 className="break-words font-semibold text-2xl tracking-tight md:text-3xl">
|
||||
Connect
|
||||
</h2>
|
||||
|
||||
<div className="my-4 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<ArticleCardIndex
|
||||
href="/connect/sign-in"
|
||||
title="Sign In"
|
||||
description="Integrate 500+ web3 wallets, in-app wallets, and smart accounts"
|
||||
icon={WalletsConnectIcon}
|
||||
/>
|
||||
<ArticleCardIndex
|
||||
href="/connect/account-abstraction"
|
||||
title="Account abstraction"
|
||||
description="Complete toolkit for Account Abstraction"
|
||||
icon={WalletsSmartIcon}
|
||||
/>
|
||||
<ArticleCardIndex
|
||||
title="In-App Wallet"
|
||||
description="Onboard users with an email, social logins, passkey or your own auth system"
|
||||
href="/connect/in-app-wallet"
|
||||
icon={WalletsInAppIcon}
|
||||
/>
|
||||
<ArticleCardIndex
|
||||
href="/connect/auth"
|
||||
title="Auth"
|
||||
description="Authenticate users with their wallets"
|
||||
icon={WalletsAuthIcon}
|
||||
/>
|
||||
<ArticleCardIndex
|
||||
href="/connect/pay"
|
||||
title="Pay"
|
||||
description="Add fiat and cross-chain crypto payments in your apps"
|
||||
icon={PayIcon}
|
||||
/>
|
||||
<ArticleCardIndex
|
||||
href="/connect/blockchain-api"
|
||||
title="Blockchain API"
|
||||
description="Performant, and reliable blockchain API"
|
||||
icon={CodeIcon}
|
||||
/>
|
||||
<ArticleCardIndex
|
||||
href="/connect/ui"
|
||||
title="Headless UI"
|
||||
description="Modular & composable set of UIs for your web3 applications"
|
||||
icon={CodeIcon}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/***
|
||||
* This component is only for the index page
|
||||
*/
|
||||
function ArticleCardIndex(props: {
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
icon?: React.FC<{ className?: string }>;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={props.href}
|
||||
className="flex min-h-32 items-center gap-4 rounded-lg border p-4 transition-colors hover:border-ring hover:bg-ring/20"
|
||||
>
|
||||
{props.icon && <props.icon className="size-10 shrink-0" />}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="font-semibold text-lg">{props.title}</h3>
|
||||
<p className="font-medium text-muted-foreground">{props.description}</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
redirect("/connect/sign-in/button");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type ReactNode, useEffect, useState } from "react";
|
||||
|
||||
interface ClientOnlyProps {
|
||||
/**
|
||||
* Use this to server render a skeleton or loading state
|
||||
*/
|
||||
ssr: ReactNode;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const ClientOnly: React.FC<ClientOnlyProps> = ({
|
||||
children,
|
||||
ssr,
|
||||
className,
|
||||
}) => {
|
||||
const hasMounted = useIsClientMounted();
|
||||
|
||||
if (!hasMounted) {
|
||||
return <> {ssr} </>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("fade-in-0 animate-in duration-300", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function useIsClientMounted() {
|
||||
const [hasMounted, setHasMounted] = useState(false);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
setHasMounted(true);
|
||||
}, []);
|
||||
|
||||
return hasMounted;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export function APIHeader(props: {
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="mb-12 flex flex-col-reverse items-center justify-between gap-6 rounded-3xl px-6 py-8 lg:flex-row lg:px-14 lg:py-14"
|
||||
className="mb-10 flex flex-col-reverse items-center justify-between gap-6 rounded-3xl px-6 py-8 lg:flex-row lg:px-10"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to top, hsl(var(--secondary)/80%), transparent)",
|
||||
@@ -21,7 +21,7 @@ export function APIHeader(props: {
|
||||
<h1 className="mb-2 font-bold text-4xl tracking-tighter md:text-5xl">
|
||||
{props.title}
|
||||
</h1>
|
||||
<p className="mb-5 max-w-[700px] text-balance text-muted-foreground">
|
||||
<p className="mb-7 max-w-[700px] text-balance text-muted-foreground leading-relaxed">
|
||||
{props.description}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Img } from "../ui/Img";
|
||||
|
||||
const fallbackChainIcon =
|
||||
"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOTYiIGhlaWdodD0iOTYiIHZpZXdCb3g9IjAgMCA5NiA5NiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTY4LjE1MTkgNzUuNzM3MkM2Mi4yOTQzIDc5Ljk5MyA1NS4yMzk3IDgyLjI4NTIgNDcuOTk5MyA4Mi4yODUyQzQwLjc1ODkgODIuMjg1MiAzMy43MDQzIDc5Ljk5MyAyNy44NDY2IDc1LjczNzJNNjMuMDI5MSAxNy4xODM3QzY5LjUzNjggMjAuMzU3NyA3NC44NzI2IDI1LjUxMDQgNzguMjcxOCAzMS45MDMzQzgxLjY3MDkgMzguMjk2MiA4Mi45NTkgNDUuNjAxMiA4MS45NTEzIDUyLjc3MTFNMTQuMDQ3NiA1Mi43NzA4QzEzLjAzOTkgNDUuNjAwOCAxNC4zMjggMzguMjk1OSAxNy43MjcxIDMxLjkwM0MyMS4xMjYzIDI1LjUxMDEgMjYuNDYyMSAyMC4zNTczIDMyLjk2OTggMTcuMTgzM000Ni4wNTk4IDI5LjM2NzVMMjkuMzY3MyA0Ni4wNkMyOC42ODg1IDQ2LjczODkgMjguMzQ5IDQ3LjA3ODMgMjguMjIxOCA0Ny40Njk3QzI4LjExIDQ3LjgxNCAyOC4xMSA0OC4xODQ5IDI4LjIyMTggNDguNTI5MkMyOC4zNDkgNDguOTIwNiAyOC42ODg1IDQ5LjI2MDEgMjkuMzY3MyA0OS45MzlMNDYuMDU5OCA2Ni42MzE0QzQ2LjczODcgNjcuMzEwMyA0Ny4wNzgxIDY3LjY0OTcgNDcuNDY5NSA2Ny43NzY5QzQ3LjgxMzggNjcuODg4OCA0OC4xODQ3IDY3Ljg4ODggNDguNTI5IDY3Ljc3NjlDNDguOTIwNCA2Ny42NDk3IDQ5LjI1OTkgNjcuMzEwMyA0OS45Mzg4IDY2LjYzMTRMNjYuNjMxMiA0OS45MzlDNjcuMzEwMSA0OS4yNjAxIDY3LjY0OTUgNDguOTIwNiA2Ny43NzY3IDQ4LjUyOTJDNjcuODg4NiA0OC4xODQ5IDY3Ljg4ODYgNDcuODE0IDY3Ljc3NjcgNDcuNDY5N0M2Ny42NDk1IDQ3LjA3ODMgNjcuMzEwMSA0Ni43Mzg5IDY2LjYzMTIgNDYuMDZMNDkuOTM4OCAyOS4zNjc1QzQ5LjI1OTkgMjguNjg4NyA0OC45MjA0IDI4LjM0OTIgNDguNTI5IDI4LjIyMkM0OC4xODQ3IDI4LjExMDIgNDcuODEzOCAyOC4xMTAyIDQ3LjQ2OTUgMjguMjIyQzQ3LjA3ODEgMjguMzQ5MiA0Ni43Mzg3IDI4LjY4ODcgNDYuMDU5OCAyOS4zNjc1WiIgc3Ryb2tlPSIjNDA0MDQwIiBzdHJva2Utd2lkdGg9IjYuODU3MTQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K";
|
||||
|
||||
import { resolveScheme } from "thirdweb/storage";
|
||||
import { THIRDWEB_CLIENT } from "../../lib/client";
|
||||
|
||||
type ImageProps = React.ComponentProps<"img">;
|
||||
|
||||
type ChainIconProps = ImageProps & {
|
||||
ipfsSrc?: string;
|
||||
};
|
||||
|
||||
export const ChainIcon = ({ ipfsSrc, ...restProps }: ChainIconProps) => {
|
||||
const src = ipfsSrc ? replaceIpfsUrl(ipfsSrc) : fallbackChainIcon;
|
||||
|
||||
return (
|
||||
<Img
|
||||
{...restProps}
|
||||
// render different image element if src changes to avoid showing old image while loading new one
|
||||
key={src}
|
||||
className={cn("object-contain", restProps.className)}
|
||||
src={src}
|
||||
loading={restProps.loading || "lazy"}
|
||||
alt=""
|
||||
fallback={<img src={fallbackChainIcon} alt="" />}
|
||||
skeleton={<div className="animate-pulse rounded-full bg-border" />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function replaceIpfsUrl(uri: string) {
|
||||
try {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return resolveScheme({
|
||||
uri,
|
||||
client: THIRDWEB_CLIENT,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("error resolving ipfs url", uri, err);
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useAllChainsData } from "../../app/hooks/chains";
|
||||
import { ChainIcon } from "./ChainIcon";
|
||||
import { SelectWithSearch } from "./select-with-search";
|
||||
|
||||
function cleanChainName(chainName: string) {
|
||||
return chainName.replace("Mainnet", "");
|
||||
}
|
||||
|
||||
type Option = { label: string; value: string };
|
||||
|
||||
export function SingleNetworkSelector(props: {
|
||||
chainId: number | undefined;
|
||||
onChange: (chainId: number) => void;
|
||||
className?: string;
|
||||
popoverContentClassName?: string;
|
||||
// if specified - only these chains will be shown
|
||||
chainIds?: number[];
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
disableChainId?: boolean;
|
||||
align?: "center" | "start" | "end";
|
||||
}) {
|
||||
const { data } = useAllChainsData();
|
||||
const { allChains, idToChain } = data;
|
||||
|
||||
const chainsToShow = useMemo(() => {
|
||||
if (!props.chainIds) {
|
||||
return allChains;
|
||||
}
|
||||
const chainIdSet = new Set(props.chainIds);
|
||||
return allChains.filter((chain) => chainIdSet.has(chain.chainId));
|
||||
}, [allChains, props.chainIds]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
return chainsToShow.map((chain) => {
|
||||
return {
|
||||
label: chain.name,
|
||||
value: String(chain.chainId),
|
||||
};
|
||||
});
|
||||
}, [chainsToShow]);
|
||||
|
||||
const searchFn = useCallback(
|
||||
(option: Option, searchValue: string) => {
|
||||
const chain = idToChain.get(Number(option.value));
|
||||
if (!chain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Number.isInteger(Number.parseInt(searchValue))) {
|
||||
return String(chain.chainId).startsWith(searchValue);
|
||||
}
|
||||
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
|
||||
},
|
||||
[idToChain],
|
||||
);
|
||||
|
||||
const renderOption = useCallback(
|
||||
(option: Option) => {
|
||||
const chain = idToChain.get(Number(option.value));
|
||||
if (!chain) {
|
||||
return option.label;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-between gap-4">
|
||||
<span className="flex grow gap-2 truncate text-left">
|
||||
<ChainIcon
|
||||
className="size-5"
|
||||
ipfsSrc={chain.icon?.url}
|
||||
loading="lazy"
|
||||
/>
|
||||
{cleanChainName(chain.name)}
|
||||
</span>
|
||||
|
||||
{!props.disableChainId && (
|
||||
<Badge variant="outline" className="gap-2 max-sm:hidden">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[idToChain, props.disableChainId],
|
||||
);
|
||||
|
||||
const isLoadingChains = allChains.length === 0;
|
||||
|
||||
return (
|
||||
<SelectWithSearch
|
||||
searchPlaceholder="Search by Name or Chain ID"
|
||||
value={String(props.chainId)}
|
||||
options={options}
|
||||
onValueChange={(chainId) => {
|
||||
props.onChange(Number(chainId));
|
||||
}}
|
||||
closeOnSelect={true}
|
||||
placeholder={isLoadingChains ? "Loading Chains..." : "Select Chain"}
|
||||
overrideSearchFn={searchFn}
|
||||
renderOption={renderOption}
|
||||
className={props.className}
|
||||
popoverContentClassName={props.popoverContentClassName}
|
||||
disabled={isLoadingChains}
|
||||
side={props.side}
|
||||
align={props.align}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon, ChevronDown, SearchIcon } from "lucide-react";
|
||||
import React, { useRef, useMemo, useEffect } from "react";
|
||||
import { useShowMore } from "../../app/hooks/useShowMore";
|
||||
import { ScrollShadow } from "../ui/ScrollShadow/ScrollShadow";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
interface SelectWithSearchProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
options: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
value: string | undefined;
|
||||
onValueChange: (value: string) => void;
|
||||
placeholder: string;
|
||||
searchPlaceholder?: string;
|
||||
className?: string;
|
||||
overrideSearchFn?: (
|
||||
option: { value: string; label: string },
|
||||
searchTerm: string,
|
||||
) => boolean;
|
||||
renderOption?: (option: { value: string; label: string }) => React.ReactNode;
|
||||
popoverContentClassName?: string;
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
align?: "center" | "start" | "end";
|
||||
closeOnSelect?: boolean;
|
||||
}
|
||||
|
||||
export const SelectWithSearch = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
SelectWithSearchProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
options,
|
||||
onValueChange,
|
||||
placeholder,
|
||||
className,
|
||||
value,
|
||||
renderOption,
|
||||
overrideSearchFn,
|
||||
popoverContentClassName,
|
||||
searchPlaceholder,
|
||||
closeOnSelect,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
|
||||
const [searchValue, setSearchValue] = React.useState("");
|
||||
const selectedOption = useMemo(
|
||||
() => options.find((option) => option.value === value),
|
||||
[options, value],
|
||||
);
|
||||
|
||||
// show 50 initially and then 20 more when reaching the end
|
||||
const { itemsToShow, lastItemRef } = useShowMore<HTMLButtonElement>(50, 20);
|
||||
|
||||
const optionsToShow = useMemo(() => {
|
||||
const filteredOptions: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[] = [];
|
||||
|
||||
const searchValLowercase = searchValue.toLowerCase();
|
||||
|
||||
for (let i = 0; i <= options.length - 1; i++) {
|
||||
if (filteredOptions.length >= itemsToShow) {
|
||||
break;
|
||||
}
|
||||
const option = options[i];
|
||||
if (!option) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (overrideSearchFn) {
|
||||
if (overrideSearchFn(option, searchValLowercase)) {
|
||||
filteredOptions.push(option);
|
||||
}
|
||||
} else {
|
||||
if (option.label.toLowerCase().includes(searchValLowercase)) {
|
||||
filteredOptions.push(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredOptions;
|
||||
}, [options, searchValue, itemsToShow, overrideSearchFn]);
|
||||
|
||||
// scroll to top when options change
|
||||
const popoverElRef = useRef<HTMLDivElement>(null);
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
useEffect(() => {
|
||||
const scrollContainer =
|
||||
popoverElRef.current?.querySelector("[data-scrollable]");
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTo({
|
||||
top: 0,
|
||||
});
|
||||
}
|
||||
}, [searchValue]);
|
||||
|
||||
return (
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
ref={ref}
|
||||
{...props}
|
||||
onClick={() => setIsPopoverOpen(true)}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between rounded-md border border-border bg-inherit p-3 hover:bg-inherit",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"truncate text-muted-foreground text-sm",
|
||||
selectedOption && "text-foreground",
|
||||
)}
|
||||
>
|
||||
{selectedOption?.label || placeholder}
|
||||
</span>
|
||||
<ChevronDown className="size-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className={cn("z-[10001] p-0", popoverContentClassName)}
|
||||
align={props.align || "center"}
|
||||
side={props.side}
|
||||
sideOffset={10}
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
style={{
|
||||
width: "var(--radix-popover-trigger-width)",
|
||||
maxHeight: "var(--radix-popover-content-available-height)",
|
||||
}}
|
||||
ref={popoverElRef}
|
||||
>
|
||||
<div>
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder={searchPlaceholder || "Search"}
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
className="!h-auto rounded-b-none border-0 border-border border-b py-3 pl-10 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
/>
|
||||
<SearchIcon className="-translate-y-1/2 absolute top-1/2 left-4 size-4 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<ScrollShadow
|
||||
scrollableClassName="max-h-[min(calc(var(--radix-popover-content-available-height)-60px),350px)] p-1"
|
||||
className="rounded"
|
||||
>
|
||||
{/* List */}
|
||||
<div>
|
||||
{optionsToShow.length === 0 && (
|
||||
<div className="flex justify-center py-10">
|
||||
No results found
|
||||
</div>
|
||||
)}
|
||||
|
||||
{optionsToShow.map((option, i) => {
|
||||
const isSelected = value === option.value;
|
||||
return (
|
||||
<Button
|
||||
key={option.value}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
onClick={() => {
|
||||
onValueChange(option.value);
|
||||
if (closeOnSelect) {
|
||||
setIsPopoverOpen(false);
|
||||
}
|
||||
}}
|
||||
variant="ghost"
|
||||
className="flex w-full cursor-pointer justify-start gap-3 rounded-sm px-3 py-2 text-left"
|
||||
ref={
|
||||
i === optionsToShow.length - 1 ? lastItemRef : undefined
|
||||
}
|
||||
>
|
||||
<div className="flex size-4 items-center justify-center">
|
||||
{isSelected && <CheckIcon className="size-4" />}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 grow">
|
||||
{renderOption ? renderOption(option) : option.label}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollShadow>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
SelectWithSearch.displayName = "SelectWithSearch";
|
||||
@@ -7,17 +7,21 @@ export function RenderCode(props: {
|
||||
html: string;
|
||||
className?: string;
|
||||
scrollableClassName?: string;
|
||||
scrollableContainerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group relative max-w-full rounded-xl border bg-code",
|
||||
"group relative max-w-full overflow-hidden rounded-xl border bg-code",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<ScrollShadow
|
||||
scrollableClassName={cn("p-4 lg:p-6", props.scrollableClassName)}
|
||||
className="text-xs md:text-sm [&_*]:leading-relaxed"
|
||||
className={cn(
|
||||
"text-xs md:text-sm [&_*]:leading-relaxed",
|
||||
props.scrollableContainerClassName,
|
||||
)}
|
||||
shadowColor="hsl(var(--code))"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { JSX } from "react";
|
||||
import type { BundledLanguage } from "shiki";
|
||||
import { ClientOnly } from "../ClientOnly";
|
||||
import { Code } from "./code";
|
||||
|
||||
export type CodeExampleProps = {
|
||||
@@ -23,7 +24,7 @@ export const CodeExample: React.FC<CodeExampleProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="relative grid h-full min-h-[300px] place-items-center bg-secondary/10 py-8">
|
||||
{preview}
|
||||
<ClientOnly ssr={null}>{preview}</ClientOnly>
|
||||
<BackgroundPattern />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ export type CodeProps = {
|
||||
loader: React.ReactNode;
|
||||
className?: string;
|
||||
scrollableClassName?: string;
|
||||
scrollableContainerClassName?: string;
|
||||
};
|
||||
|
||||
export function CodeLoading() {
|
||||
@@ -29,6 +30,7 @@ export const CodeClient: React.FC<CodeProps> = ({
|
||||
loader,
|
||||
className,
|
||||
scrollableClassName,
|
||||
scrollableContainerClassName,
|
||||
}) => {
|
||||
const codeQuery = useQuery({
|
||||
queryKey: ["html", code],
|
||||
@@ -46,6 +48,7 @@ export const CodeClient: React.FC<CodeProps> = ({
|
||||
html={codeQuery.data.html}
|
||||
className={className}
|
||||
scrollableClassName={scrollableClassName}
|
||||
scrollableContainerClassName={scrollableContainerClassName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
import * as parserBabel from "prettier/plugins/babel";
|
||||
import * as estree from "prettier/plugins/estree";
|
||||
import { format } from "prettier/standalone";
|
||||
import { codeToHtml } from "shiki";
|
||||
import { type BundledLanguage, codeToHtml } from "shiki";
|
||||
|
||||
export async function getCodeHtml(code: string, lang: string) {
|
||||
const formattedCode = await format(code, {
|
||||
parser: "babel-ts",
|
||||
plugins: [parserBabel, estree],
|
||||
printWidth: 60,
|
||||
});
|
||||
function isPrettierSupportedLang(lang: BundledLanguage) {
|
||||
return (
|
||||
lang === "js" ||
|
||||
lang === "jsx" ||
|
||||
lang === "ts" ||
|
||||
lang === "tsx" ||
|
||||
lang === "javascript" ||
|
||||
lang === "typescript"
|
||||
);
|
||||
}
|
||||
|
||||
export async function getCodeHtml(code: string, lang: BundledLanguage) {
|
||||
const formattedCode = isPrettierSupportedLang(lang)
|
||||
? await format(code, {
|
||||
parser: "babel-ts",
|
||||
plugins: [parserBabel, estree],
|
||||
printWidth: 60,
|
||||
}).catch(() => {
|
||||
return code;
|
||||
})
|
||||
: code;
|
||||
|
||||
const html = await codeToHtml(formattedCode, {
|
||||
lang: lang,
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { THIRDWEB_CLIENT } from "@/lib/client";
|
||||
import { ethereum } from "thirdweb/chains";
|
||||
import {
|
||||
AccountAddress,
|
||||
AccountAvatar,
|
||||
AccountBalance,
|
||||
type AccountBalanceInfo,
|
||||
AccountBlobbie,
|
||||
AccountName,
|
||||
AccountProvider,
|
||||
} from "thirdweb/react";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
import { CodeExample } from "../code/code-example";
|
||||
|
||||
const vitalikAddress = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045";
|
||||
import {
|
||||
AccountAddressBasicPreview,
|
||||
AccountAddressFormatPreview,
|
||||
AccountAvatarBasicPreview,
|
||||
AccountBalanceBasicPreview,
|
||||
AccountBalanceCustomTokenPreview,
|
||||
AccountBalanceFormatPreview,
|
||||
AccountBalanceUSDPreview,
|
||||
AccountBlobbieBasicPreview,
|
||||
AccountNameBasicPreview,
|
||||
AccountNameCustomPreview,
|
||||
ConnectDetailsButtonClonePreview,
|
||||
} from "./account-previews";
|
||||
|
||||
/**
|
||||
* Show full wallet address with AccountAddress
|
||||
@@ -32,11 +29,7 @@ export function AccountAddressBasic() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountAddress />
|
||||
</AccountProvider>
|
||||
}
|
||||
preview={<AccountAddressBasicPreview />}
|
||||
code={`import { AccountProvider, AccountAddress } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -63,11 +56,7 @@ export function AccountAddressFormat() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountAddress formatFn={shortenAddress} />
|
||||
</AccountProvider>
|
||||
}
|
||||
preview={<AccountAddressFormatPreview />}
|
||||
code={`import { AccountProvider, AccountAddress } from "thirdweb/react";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
|
||||
@@ -98,11 +87,7 @@ export function AccountNameBasic() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountName loadingComponent={<span>Loading...</span>} />
|
||||
</AccountProvider>
|
||||
}
|
||||
preview={<AccountNameBasicPreview />}
|
||||
code={`import { AccountProvider, AccountName } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -126,14 +111,7 @@ export function AccountNameCustom() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountName
|
||||
socialType="lens"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</AccountProvider>
|
||||
}
|
||||
preview={<AccountNameCustomPreview />}
|
||||
code={`import { AccountProvider, AccountName } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -164,21 +142,14 @@ export function AccountBalanceBasic() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</AccountProvider>
|
||||
}
|
||||
preview={<AccountBalanceBasicPreview />}
|
||||
code={`import { AccountProvider, AccountAddress } from "thirdweb/react";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
|
||||
function App() {
|
||||
return <AccountProvider address="0xd8da6bf26964af9d7eed9e03e53415d37aa96045" client={thirdwebClient}>
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</AccountProvider>
|
||||
@@ -202,15 +173,7 @@ export function AccountBalanceCustomToken() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
tokenAddress="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</AccountProvider>
|
||||
}
|
||||
preview={<AccountBalanceCustomTokenPreview />}
|
||||
code={`import { AccountProvider, AccountAddress } from "thirdweb/react";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
|
||||
@@ -218,9 +181,9 @@ const USDC_ETHEREUM = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
|
||||
|
||||
function App() {
|
||||
return <AccountProvider address="0xd8da6bf26964af9d7eed9e03e53415d37aa96045" client={thirdwebClient}>
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
tokenAddress={USDC_ETHEREUM}
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
tokenAddress={USDC_ETHEREUM}
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</AccountProvider>
|
||||
@@ -244,23 +207,13 @@ export function AccountBalanceFormat() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
formatFn={(props: AccountBalanceInfo) =>
|
||||
`${Math.ceil(props.balance * 1000) / 1000} ${props.symbol}`
|
||||
}
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</AccountProvider>
|
||||
}
|
||||
preview={<AccountBalanceFormatPreview />}
|
||||
code={`import { AccountProvider, AccountAddress, type AccountBalanceFormatParams } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
return <AccountProvider address="0xd8da6bf26964af9d7eed9e03e53415d37aa96045" client={thirdwebClient}>
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
formatFn={(props: AccountBalanceInfo) => \`\${Math.ceil(props.balance * 1000) / 1000} \${props.symbol}\`} />
|
||||
</AccountProvider>
|
||||
@@ -284,23 +237,15 @@ export function AccountBalanceUSD() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
showBalanceInFiat="USD"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</AccountProvider>
|
||||
}
|
||||
preview={<AccountBalanceUSDPreview />}
|
||||
code={`import { AccountProvider, AccountAddress } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AccountProvider address="0xd8da6bf26964af9d7eed9e03e53415d37aa96045" client={thirdwebClient}>
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
showBalanceInFiat="USD"
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
showBalanceInFiat="USD"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</AccountProvider>
|
||||
@@ -328,14 +273,7 @@ export function AccountAvatarBasic() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountAvatar
|
||||
className="h-20 w-20 rounded-full"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</AccountProvider>
|
||||
}
|
||||
preview={<AccountAvatarBasicPreview />}
|
||||
code={`import { AccountProvider, AccountAvatar } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -362,11 +300,7 @@ export function AccountBlobbieBasic() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountBlobbie className="h-20 w-20 rounded-full" />
|
||||
</AccountProvider>
|
||||
}
|
||||
preview={<AccountBlobbieBasicPreview />}
|
||||
code={`import { AccountProvider, AccountBlobbie } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -394,58 +328,7 @@ export function ConnectDetailsButtonClone() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-w-[200px] flex-row items-center gap-2 rounded-md border bg-gray-800 px-4 py-2"
|
||||
>
|
||||
<AccountAvatar
|
||||
className="h-10 w-10 rounded-full"
|
||||
loadingComponent={
|
||||
<AccountBlobbie className="h-10 w-10 rounded-full" />
|
||||
}
|
||||
fallbackComponent={
|
||||
<AccountBlobbie className="h-10 w-10 rounded-full" />
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<AccountName
|
||||
className="text-left"
|
||||
loadingComponent={
|
||||
<AccountAddress
|
||||
formatFn={shortenAddress}
|
||||
className="text-left"
|
||||
/>
|
||||
}
|
||||
fallbackComponent={
|
||||
<AccountAddress
|
||||
formatFn={shortenAddress}
|
||||
className="text-left"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<AccountBalance
|
||||
className="text-left text-sm"
|
||||
chain={ethereum}
|
||||
loadingComponent={
|
||||
<span className="text-left text-sm">Loading...</span>
|
||||
}
|
||||
fallbackComponent={
|
||||
<span className="text-left text-sm">Loading...</span>
|
||||
}
|
||||
/>
|
||||
<AccountBalance
|
||||
className="text-left text-sm"
|
||||
chain={ethereum}
|
||||
showBalanceInFiat="USD"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</AccountProvider>
|
||||
}
|
||||
preview={<ConnectDetailsButtonClonePreview />}
|
||||
code={`import { AccountProvider, AccountAvatar, AccountName, AccountBalance, AccountAddress, AccountBlobbie } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -499,7 +382,7 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</AccountProvider>
|
||||
</AccountProvider>
|
||||
)
|
||||
}`}
|
||||
lang="tsx"
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import { THIRDWEB_CLIENT } from "@/lib/client";
|
||||
import { ethereum } from "thirdweb/chains";
|
||||
import {
|
||||
AccountAddress,
|
||||
AccountAvatar,
|
||||
AccountBalance,
|
||||
type AccountBalanceInfo,
|
||||
AccountBlobbie,
|
||||
AccountName,
|
||||
AccountProvider,
|
||||
} from "thirdweb/react";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
|
||||
const vitalikAddress = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045";
|
||||
|
||||
export function AccountAddressBasicPreview() {
|
||||
return (
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountAddress />
|
||||
</AccountProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountAddressFormatPreview() {
|
||||
return (
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountAddress formatFn={shortenAddress} />
|
||||
</AccountProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountNameBasicPreview() {
|
||||
return (
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountName loadingComponent={<span>Loading...</span>} />
|
||||
</AccountProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountNameCustomPreview() {
|
||||
return (
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountName
|
||||
socialType="lens"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</AccountProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountBalanceBasicPreview() {
|
||||
return (
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</AccountProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountBalanceCustomTokenPreview() {
|
||||
return (
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
tokenAddress="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</AccountProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountBalanceFormatPreview() {
|
||||
return (
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
formatFn={(props: AccountBalanceInfo) =>
|
||||
`${Math.ceil(props.balance * 1000) / 1000} ${props.symbol}`
|
||||
}
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</AccountProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountBalanceUSDPreview() {
|
||||
return (
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
showBalanceInFiat="USD"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</AccountProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountAvatarBasicPreview() {
|
||||
return (
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountAvatar
|
||||
className="h-20 w-20 rounded-full"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</AccountProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountBlobbieBasicPreview() {
|
||||
return (
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<AccountBlobbie className="h-20 w-20 rounded-full" />
|
||||
</AccountProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConnectDetailsButtonClonePreview() {
|
||||
return (
|
||||
<AccountProvider address={vitalikAddress} client={THIRDWEB_CLIENT}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-w-[200px] flex-row items-center gap-2 rounded-md border bg-zinc-900 px-4 py-2"
|
||||
>
|
||||
<AccountAvatar
|
||||
className="h-10 w-10 rounded-full"
|
||||
loadingComponent={
|
||||
<AccountBlobbie className="h-10 w-10 rounded-full" />
|
||||
}
|
||||
fallbackComponent={
|
||||
<AccountBlobbie className="h-10 w-10 rounded-full" />
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<AccountName
|
||||
className="text-left"
|
||||
loadingComponent={
|
||||
<AccountAddress formatFn={shortenAddress} className="text-left" />
|
||||
}
|
||||
fallbackComponent={
|
||||
<AccountAddress formatFn={shortenAddress} className="text-left" />
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<AccountBalance
|
||||
className="text-left text-sm"
|
||||
chain={ethereum}
|
||||
loadingComponent={
|
||||
<span className="text-left text-sm">Loading...</span>
|
||||
}
|
||||
fallbackComponent={
|
||||
<span className="text-left text-sm">Loading...</span>
|
||||
}
|
||||
/>
|
||||
<AccountBalance
|
||||
className="text-left text-sm"
|
||||
chain={ethereum}
|
||||
showBalanceInFiat="USD"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</AccountProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { THIRDWEB_CLIENT } from "@/lib/client";
|
||||
import { avalanche } from "thirdweb/chains";
|
||||
import { ChainIcon, ChainName, ChainProvider } from "thirdweb/react";
|
||||
import { CodeExample } from "../code/code-example";
|
||||
import { ChainIconPreview, ChainNamePreview } from "./chain-previews";
|
||||
|
||||
export function ChainIconBasic() {
|
||||
return (
|
||||
@@ -18,15 +14,7 @@ export function ChainIconBasic() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<ChainProvider chain={avalanche}>
|
||||
<ChainIcon
|
||||
client={THIRDWEB_CLIENT}
|
||||
className="h-auto w-20 rounded-full"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</ChainProvider>
|
||||
}
|
||||
preview={<ChainIconPreview />}
|
||||
code={`import { ChainProvider, ChainIcon } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -57,11 +45,7 @@ export function ChainNameBasic() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<ChainProvider chain={avalanche}>
|
||||
<ChainName loadingComponent={<span>Loading...</span>} />
|
||||
</ChainProvider>
|
||||
}
|
||||
preview={<ChainNamePreview />}
|
||||
code={`import { ChainProvider, ChainName } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { THIRDWEB_CLIENT } from "@/lib/client";
|
||||
import { avalanche } from "thirdweb/chains";
|
||||
import { ChainIcon, ChainName, ChainProvider } from "thirdweb/react";
|
||||
|
||||
export function ChainIconPreview() {
|
||||
return (
|
||||
<ChainProvider chain={avalanche}>
|
||||
<ChainIcon
|
||||
client={THIRDWEB_CLIENT}
|
||||
className="h-auto w-20 rounded-full"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</ChainProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChainNamePreview() {
|
||||
return (
|
||||
<ChainProvider chain={avalanche}>
|
||||
<ChainName loadingComponent={<span>Loading...</span>} />
|
||||
</ChainProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { THIRDWEB_CLIENT } from "@/lib/client";
|
||||
import { getContract } from "thirdweb";
|
||||
import { ethereum } from "thirdweb/chains";
|
||||
import { NFTDescription, NFTMedia, NFTName, NFTProvider } from "thirdweb/react";
|
||||
import { CodeExample } from "../code/code-example";
|
||||
|
||||
const nftContract = getContract({
|
||||
address: "0xbd3531da5cf5857e7cfaa92426877b022e612cf8",
|
||||
chain: ethereum,
|
||||
client: THIRDWEB_CLIENT,
|
||||
});
|
||||
import {
|
||||
NftCardDemoPreview,
|
||||
NftDescriptionBasicPreview,
|
||||
NftMediaBasicPreview,
|
||||
NftMediaOverridePreview,
|
||||
NftNameBasicPreview,
|
||||
} from "./nft-previews";
|
||||
|
||||
export function NftMediaBasic() {
|
||||
return (
|
||||
@@ -25,14 +20,7 @@ export function NftMediaBasic() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<NFTProvider tokenId={0n} contract={nftContract}>
|
||||
<NFTMedia
|
||||
className="h-40 w-40 rounded-md"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</NFTProvider>
|
||||
}
|
||||
preview={<NftMediaBasicPreview />}
|
||||
code={`import { NFTProvider, NFTMedia } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -60,18 +48,7 @@ export function NftMediaOverride() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<NFTProvider tokenId={0n} contract={nftContract}>
|
||||
<NFTMedia
|
||||
className="h-40 w-40 rounded-md border"
|
||||
mediaResolver={{
|
||||
src: "ipfs://QmeGCqV1mSHTZrvuFzW1XZdCRRGXB6AmSotTqHoxA2xfDo/1.mp4",
|
||||
poster:
|
||||
"ipfs://QmeGCqV1mSHTZrvuFzW1XZdCRRGXB6AmSotTqHoxA2xfDo/0.png",
|
||||
}}
|
||||
/>
|
||||
</NFTProvider>
|
||||
}
|
||||
preview={<NftMediaOverridePreview />}
|
||||
code={`import { NFTProvider, NFTMedia } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -104,11 +81,7 @@ export function NftNameBasic() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<NFTProvider tokenId={0n} contract={nftContract}>
|
||||
<NFTName loadingComponent={<span>Loading...</span>} />
|
||||
</NFTProvider>
|
||||
}
|
||||
preview={<NftNameBasicPreview />}
|
||||
code={`import { NFTProvider, NFTName } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -135,14 +108,7 @@ export function NftDescriptionBasic() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<NFTProvider tokenId={0n} contract={nftContract}>
|
||||
<NFTDescription
|
||||
className="text-center"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</NFTProvider>
|
||||
}
|
||||
preview={<NftDescriptionBasicPreview />}
|
||||
code={`import { NFTProvider, NFTDescription } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -170,24 +136,13 @@ export function NftCardDemo() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<NFTProvider tokenId={0n} contract={nftContract}>
|
||||
<div className="flex w-[230px] flex-col gap-3 rounded-lg border bg-gray-900 px-1 py-3">
|
||||
<NFTMedia className="rounded-md px-2 text-center" />
|
||||
<NFTName className="px-2 font-bold" />
|
||||
<NFTDescription
|
||||
className="px-2 text-sm"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</div>
|
||||
</NFTProvider>
|
||||
}
|
||||
preview={<NftCardDemoPreview />}
|
||||
code={`import { NFTProvider, NFTDescription, NFTName, NFTMedia } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<NFTProvider tokenId={0n} contract={nftContract}>
|
||||
<div className="flex w-[230px] flex-col gap-3 rounded-lg border bg-gray-900 px-1 py-3">
|
||||
<div className="flex w-[230px] flex-col gap-3 rounded-lg border bg-zinc-900 px-1 py-3">
|
||||
<NFTMedia className="rounded-md px-2 text-center" />
|
||||
<NFTName className="px-2 font-bold" />
|
||||
<NFTDescription
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import { THIRDWEB_CLIENT } from "@/lib/client";
|
||||
import { getContract } from "thirdweb";
|
||||
import { ethereum } from "thirdweb/chains";
|
||||
import { NFTDescription, NFTMedia, NFTName, NFTProvider } from "thirdweb/react";
|
||||
|
||||
const nftContract = getContract({
|
||||
address: "0xbd3531da5cf5857e7cfaa92426877b022e612cf8",
|
||||
chain: ethereum,
|
||||
client: THIRDWEB_CLIENT,
|
||||
});
|
||||
|
||||
export function NftMediaBasicPreview() {
|
||||
return (
|
||||
<NFTProvider tokenId={0n} contract={nftContract}>
|
||||
<NFTMedia
|
||||
className="h-40 w-40 rounded-md"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</NFTProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function NftMediaOverridePreview() {
|
||||
return (
|
||||
<NFTProvider tokenId={0n} contract={nftContract}>
|
||||
<NFTMedia
|
||||
className="h-40 w-40 rounded-md border"
|
||||
mediaResolver={{
|
||||
src: "ipfs://QmeGCqV1mSHTZrvuFzW1XZdCRRGXB6AmSotTqHoxA2xfDo/1.mp4",
|
||||
poster: "ipfs://QmeGCqV1mSHTZrvuFzW1XZdCRRGXB6AmSotTqHoxA2xfDo/0.png",
|
||||
}}
|
||||
/>
|
||||
</NFTProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function NftNameBasicPreview() {
|
||||
return (
|
||||
<NFTProvider tokenId={0n} contract={nftContract}>
|
||||
<NFTName loadingComponent={<span>Loading...</span>} />
|
||||
</NFTProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function NftDescriptionBasicPreview() {
|
||||
return (
|
||||
<NFTProvider tokenId={0n} contract={nftContract}>
|
||||
<NFTDescription
|
||||
className="block px-6 text-center"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</NFTProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function NftCardDemoPreview() {
|
||||
return (
|
||||
<NFTProvider tokenId={0n} contract={nftContract}>
|
||||
<div className="flex w-[230px] flex-col gap-3 rounded-lg border bg-zinc-900 px-1 py-3">
|
||||
<NFTMedia className="rounded-md px-2 text-center" />
|
||||
<NFTName className="px-2 font-bold" />
|
||||
<NFTDescription
|
||||
className="px-2 text-sm"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</div>
|
||||
</NFTProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { THIRDWEB_CLIENT } from "@/lib/client";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "thirdweb";
|
||||
import { ethereum } from "thirdweb/chains";
|
||||
import {
|
||||
TokenIcon,
|
||||
TokenName,
|
||||
TokenProvider,
|
||||
TokenSymbol,
|
||||
} from "thirdweb/react";
|
||||
import { CodeExample } from "../code/code-example";
|
||||
|
||||
const USDC_ADDRESS = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
|
||||
import {
|
||||
TokenCardPreview,
|
||||
TokenImageBasicPreview,
|
||||
TokenImageOverridePreview,
|
||||
TokenNameBasicPreview,
|
||||
TokenSymbolBasicPreview,
|
||||
} from "./token-previews";
|
||||
|
||||
export function TokenImageBasic() {
|
||||
return (
|
||||
@@ -26,15 +20,7 @@ export function TokenImageBasic() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<TokenProvider
|
||||
address={NATIVE_TOKEN_ADDRESS}
|
||||
client={THIRDWEB_CLIENT}
|
||||
chain={ethereum}
|
||||
>
|
||||
<TokenIcon className="h-auto w-20 rounded-full" />
|
||||
</TokenProvider>
|
||||
}
|
||||
preview={<TokenImageBasicPreview />}
|
||||
code={`import { TokenProvider, TokenIcon } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -68,18 +54,7 @@ export function TokenImageOverride() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<TokenProvider
|
||||
address={USDC_ADDRESS}
|
||||
client={THIRDWEB_CLIENT}
|
||||
chain={ethereum}
|
||||
>
|
||||
<TokenIcon
|
||||
className="h-auto w-20 rounded-full"
|
||||
iconResolver="/usdc.svg"
|
||||
/>
|
||||
</TokenProvider>
|
||||
}
|
||||
preview={<TokenImageOverridePreview />}
|
||||
code={`import { TokenProvider, TokenIcon } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -114,15 +89,7 @@ export function TokenNameBasic() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<TokenProvider
|
||||
address={USDC_ADDRESS}
|
||||
client={THIRDWEB_CLIENT}
|
||||
chain={ethereum}
|
||||
>
|
||||
<TokenName loadingComponent={<span>Loading...</span>} />
|
||||
</TokenProvider>
|
||||
}
|
||||
preview={<TokenNameBasicPreview />}
|
||||
code={`import { TokenProvider, TokenName } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -153,15 +120,7 @@ export function TokenSymbolBasic() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<TokenProvider
|
||||
address={USDC_ADDRESS}
|
||||
client={THIRDWEB_CLIENT}
|
||||
chain={ethereum}
|
||||
>
|
||||
<TokenSymbol loadingComponent={<span>Loading...</span>} />
|
||||
</TokenProvider>
|
||||
}
|
||||
preview={<TokenSymbolBasicPreview />}
|
||||
code={`import { TokenProvider, TokenSymbol } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -194,27 +153,7 @@ export function TokenCard() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<TokenProvider
|
||||
address={USDC_ADDRESS}
|
||||
client={THIRDWEB_CLIENT}
|
||||
chain={ethereum}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2 rounded-lg border bg-slate-950 px-4 py-1">
|
||||
<TokenIcon className="h-10 w-10" iconResolver="/usdc.svg" />
|
||||
<div className="flex flex-col">
|
||||
<TokenName
|
||||
className="font-bold"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
<TokenSymbol
|
||||
className="text-gray-500"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TokenProvider>
|
||||
}
|
||||
preview={<TokenCardPreview />}
|
||||
code={`import { TokenProvider, TokenSymbol } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { THIRDWEB_CLIENT } from "@/lib/client";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "thirdweb";
|
||||
import { ethereum } from "thirdweb/chains";
|
||||
import {
|
||||
TokenIcon,
|
||||
TokenName,
|
||||
TokenProvider,
|
||||
TokenSymbol,
|
||||
} from "thirdweb/react";
|
||||
|
||||
const USDC_ADDRESS = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
|
||||
|
||||
export function TokenImageBasicPreview() {
|
||||
return (
|
||||
<TokenProvider
|
||||
address={NATIVE_TOKEN_ADDRESS}
|
||||
client={THIRDWEB_CLIENT}
|
||||
chain={ethereum}
|
||||
>
|
||||
<TokenIcon className="h-auto w-20 rounded-full" />
|
||||
</TokenProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function TokenImageOverridePreview() {
|
||||
return (
|
||||
<TokenProvider
|
||||
address={USDC_ADDRESS}
|
||||
client={THIRDWEB_CLIENT}
|
||||
chain={ethereum}
|
||||
>
|
||||
<TokenIcon
|
||||
className="h-auto w-20 rounded-full"
|
||||
iconResolver="/usdc.svg"
|
||||
/>
|
||||
</TokenProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function TokenNameBasicPreview() {
|
||||
return (
|
||||
<TokenProvider
|
||||
address={USDC_ADDRESS}
|
||||
client={THIRDWEB_CLIENT}
|
||||
chain={ethereum}
|
||||
>
|
||||
<TokenName loadingComponent={<span>Loading...</span>} />
|
||||
</TokenProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function TokenSymbolBasicPreview() {
|
||||
return (
|
||||
<TokenProvider
|
||||
address={USDC_ADDRESS}
|
||||
client={THIRDWEB_CLIENT}
|
||||
chain={ethereum}
|
||||
>
|
||||
<TokenSymbol loadingComponent={<span>Loading...</span>} />
|
||||
</TokenProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function TokenCardPreview() {
|
||||
return (
|
||||
<TokenProvider
|
||||
address={USDC_ADDRESS}
|
||||
client={THIRDWEB_CLIENT}
|
||||
chain={ethereum}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2 rounded-lg border bg-slate-950 px-4 py-1">
|
||||
<TokenIcon className="h-10 w-10" iconResolver="/usdc.svg" />
|
||||
<div className="flex flex-col">
|
||||
<TokenName
|
||||
className="font-bold"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
<TokenSymbol
|
||||
className="text-gray-500"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TokenProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { WalletIcon, WalletName, WalletProvider } from "thirdweb/react";
|
||||
import { CodeExample } from "../code/code-example";
|
||||
import {
|
||||
WalletIconBasicPreview,
|
||||
WalletNameBasicPreview,
|
||||
WalletNameFormatPreview,
|
||||
} from "./wallet-previews";
|
||||
|
||||
export function WalletIconBasic() {
|
||||
return (
|
||||
@@ -16,14 +18,7 @@ export function WalletIconBasic() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletIcon
|
||||
className="h-20 w-20 rounded-full"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</WalletProvider>
|
||||
}
|
||||
preview={<WalletIconBasicPreview />}
|
||||
code={`import { WalletProvider, WalletIcon } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -55,11 +50,7 @@ export function WalletNameBasic() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletName loadingComponent={<span>Loading...</span>} />
|
||||
</WalletProvider>
|
||||
}
|
||||
preview={<WalletNameBasicPreview />}
|
||||
code={`import { WalletProvider, WalletName } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
@@ -85,18 +76,14 @@ export function WalletNameFormat() {
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletName formatFn={(str: string) => `${str} Wallet`} />
|
||||
</WalletProvider>
|
||||
}
|
||||
preview={<WalletNameFormatPreview />}
|
||||
code={`import { WalletProvider, WalletName } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletName
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
<WalletName
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
formatFn={(str: string) => \`\${str} Wallet\`}
|
||||
/>
|
||||
</WalletProvider>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { WalletIcon, WalletName, WalletProvider } from "thirdweb/react";
|
||||
|
||||
export function WalletIconBasicPreview() {
|
||||
return (
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletIcon
|
||||
className="h-20 w-20 rounded-full"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</WalletProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function WalletNameBasicPreview() {
|
||||
return (
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletName loadingComponent={<span>Loading...</span>} />
|
||||
</WalletProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function WalletNameFormatPreview() {
|
||||
return (
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletName
|
||||
formatFn={(str: string) => `${str} Wallet`}
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</WalletProvider>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export function StyledPayEmbedPreview() {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<StyledConnectButton />
|
||||
<div className="h-10" />
|
||||
<PayEmbed
|
||||
@@ -27,6 +27,6 @@ export function StyledPayEmbedPreview() {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Link as LinkIcon } from "lucide-react";
|
||||
|
||||
export function Anchor(props: {
|
||||
id: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group/anchor flex scroll-mt-offset-top-mobile items-center gap-2 xl:scroll-mt-offset-top",
|
||||
props.className,
|
||||
)}
|
||||
id={props.id}
|
||||
>
|
||||
{props.children}
|
||||
{props.id && (
|
||||
<a
|
||||
href={`#${props.id}`}
|
||||
className="text-muted-foreground no-underline opacity-0 transition-opacity group-hover/anchor:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<LinkIcon className="size-4" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,9 +3,11 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { useEffect, useId, useRef, useState } from "react";
|
||||
import { Anchor } from "./Anchor";
|
||||
import { DynamicHeight } from "./DynamicHeight";
|
||||
|
||||
type CustomAccordionProps = {
|
||||
id?: string;
|
||||
chevronPosition?: "left" | "right";
|
||||
trigger: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
@@ -18,52 +20,73 @@ type CustomAccordionProps = {
|
||||
|
||||
export function CustomAccordion(props: CustomAccordionProps) {
|
||||
const [isOpen, setIsOpen] = useState(props.defaultOpen || false);
|
||||
const elRef = useRef<HTMLDivElement>(null);
|
||||
const contentId = useId();
|
||||
const buttonId = useId();
|
||||
const accordionContentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// when another accordion is opened, close this one
|
||||
const accordionAnchorId = props.id;
|
||||
|
||||
// if window hash matches current accordion's id, open it
|
||||
const accordionIdMatchChecked = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
if (accordionIdMatchChecked.current) {
|
||||
return;
|
||||
}
|
||||
accordionIdMatchChecked.current = true;
|
||||
const hash = window.location.hash;
|
||||
if (hash && hash === `#${accordionAnchorId}`) {
|
||||
setTimeout(() => {
|
||||
setIsOpen(true);
|
||||
setTimeout(() => {
|
||||
accordionContentRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
});
|
||||
}, 500);
|
||||
}, 500);
|
||||
}
|
||||
}, [accordionAnchorId]);
|
||||
|
||||
// if window hash matches any child accordion's id, open it
|
||||
const accordionContentChecked = useRef(false);
|
||||
useEffect(() => {
|
||||
const accordionContentEl = accordionContentRef.current;
|
||||
if (!accordionContentEl || accordionContentChecked.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selfEl = elRef.current;
|
||||
accordionContentChecked.current = true;
|
||||
|
||||
if (!selfEl) {
|
||||
const hash = window.location.hash;
|
||||
if (!hash) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allAccordions = selfEl.parentElement?.querySelectorAll(
|
||||
"[data-custom-accordion]",
|
||||
);
|
||||
// if any child element has an anchor tag with href that matches the hash, open the accordion
|
||||
const containsMatchingAnchor = Array.from(
|
||||
accordionContentEl.querySelectorAll("a[href^='#']"),
|
||||
).find((childAccordion) => {
|
||||
const href = childAccordion.getAttribute("href");
|
||||
return href === hash;
|
||||
});
|
||||
|
||||
if (!allAccordions) {
|
||||
return;
|
||||
if (containsMatchingAnchor) {
|
||||
setTimeout(() => {
|
||||
setIsOpen(true);
|
||||
setTimeout(() => {
|
||||
containsMatchingAnchor.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
});
|
||||
}, 500);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
for (const accordion of allAccordions) {
|
||||
if (!(accordion instanceof HTMLElement)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (accordion === selfEl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
accordion.addEventListener("click", () => {
|
||||
if (isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [isOpen]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("border-b", props.containerClassName)}
|
||||
data-custom-accordion
|
||||
ref={elRef}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -91,7 +114,11 @@ export function CustomAccordion(props: CustomAccordionProps) {
|
||||
)}
|
||||
/>
|
||||
|
||||
{props.trigger}
|
||||
{props.id ? (
|
||||
<Anchor id={props.id}> {props.trigger} </Anchor>
|
||||
) : (
|
||||
props.trigger
|
||||
)}
|
||||
</button>
|
||||
|
||||
<DynamicHeight>
|
||||
@@ -107,7 +134,9 @@ export function CustomAccordion(props: CustomAccordionProps) {
|
||||
!isOpen && "hidden",
|
||||
)}
|
||||
>
|
||||
<div className="pt-1 pb-2">{props.children}</div>
|
||||
<div className="pt-1 pb-4" ref={accordionContentRef}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DynamicHeight>
|
||||
|
||||
@@ -1,29 +1,84 @@
|
||||
"use client";
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
|
||||
import { useState } from "react";
|
||||
"use client";
|
||||
import { useLayoutEffect, useRef, useState } from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
type imgElementProps = React.DetailedHTMLProps<
|
||||
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||
HTMLImageElement
|
||||
>;
|
||||
> & {
|
||||
skeleton?: React.ReactNode;
|
||||
fallback?: React.ReactNode;
|
||||
src: string | undefined;
|
||||
};
|
||||
|
||||
export function Img(props: imgElementProps) {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [_status, setStatus] = useState<"pending" | "fallback" | "loaded">(
|
||||
"pending",
|
||||
);
|
||||
const status =
|
||||
props.src === undefined
|
||||
? "pending"
|
||||
: props.src === ""
|
||||
? "fallback"
|
||||
: _status;
|
||||
const { className, fallback, skeleton, ...restProps } = props;
|
||||
const defaultSkeleton = <div className="animate-pulse bg-accent" />;
|
||||
const defaultFallback = <div className="bg-accent" />;
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const imgEl = imgRef.current;
|
||||
if (!imgEl) {
|
||||
return;
|
||||
}
|
||||
if (imgEl.complete) {
|
||||
setStatus("loaded");
|
||||
} else {
|
||||
function handleLoad() {
|
||||
setStatus("loaded");
|
||||
}
|
||||
imgEl.addEventListener("load", handleLoad);
|
||||
return () => {
|
||||
imgEl.removeEventListener("load", handleLoad);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* biome-ignore lint/a11y/useAltText: <explanation> */}
|
||||
<img
|
||||
{...props}
|
||||
onLoad={() => {
|
||||
setIsLoading(false);
|
||||
{...restProps}
|
||||
// avoid setting empty src string to prevent request to the entire page
|
||||
src={restProps.src || undefined}
|
||||
ref={imgRef}
|
||||
onError={() => {
|
||||
setStatus("fallback");
|
||||
}}
|
||||
style={{
|
||||
opacity: isLoading ? 0 : 1,
|
||||
opacity: status === "loaded" ? 1 : 0,
|
||||
...restProps.style,
|
||||
}}
|
||||
alt={restProps.alt || ""}
|
||||
className={cn(
|
||||
"fade-in-0 object-cover transition-opacity duration-300",
|
||||
className,
|
||||
)}
|
||||
decoding="async"
|
||||
/>
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex animate-pulse items-center justify-center rounded-lg bg-accent" />
|
||||
|
||||
{status !== "loaded" && (
|
||||
<div
|
||||
style={restProps.style}
|
||||
className={cn(
|
||||
"fade-in-0 absolute inset-0 overflow-hidden transition-opacity duration-300 [&>*]:h-full [&>*]:w-full",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{status === "pending" && (skeleton || defaultSkeleton)}
|
||||
{status === "fallback" && (fallback || defaultFallback)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.scrollShadowY {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
pointer-events: none;
|
||||
@@ -10,7 +10,7 @@
|
||||
.scrollShadowX {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
z-index: 10;
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -10,6 +10,7 @@ export function ScrollShadow(props: {
|
||||
scrollableClassName?: string;
|
||||
disableTopShadow?: boolean;
|
||||
shadowColor?: string;
|
||||
shadowClassName?: string;
|
||||
}) {
|
||||
const scrollableEl = useRef<HTMLDivElement>(null);
|
||||
const shadowTopEl = useRef<HTMLDivElement>(null);
|
||||
@@ -30,20 +31,21 @@ export function ScrollShadow(props: {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentScrollHeight = content.scrollHeight - wrapper.offsetHeight;
|
||||
const contentScrollWidth = content.scrollWidth - wrapper.offsetWidth;
|
||||
|
||||
function handleScroll() {
|
||||
if (
|
||||
!content ||
|
||||
!shadowTop ||
|
||||
!shadowBottom ||
|
||||
!shadowLeft ||
|
||||
!shadowRight
|
||||
!shadowRight ||
|
||||
!wrapper
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentScrollHeight = content.scrollHeight - wrapper.offsetHeight;
|
||||
const contentScrollWidth = content.scrollWidth - wrapper.offsetWidth;
|
||||
|
||||
if (contentScrollHeight > 10) {
|
||||
const currentScroll = content.scrollTop / contentScrollHeight;
|
||||
shadowTop.style.opacity = `${currentScroll}`;
|
||||
@@ -63,25 +65,40 @@ export function ScrollShadow(props: {
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll();
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
handleScroll();
|
||||
});
|
||||
});
|
||||
content.addEventListener("scroll", handleScroll);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
handleScroll();
|
||||
});
|
||||
|
||||
resizeObserver.observe(content);
|
||||
return () => {
|
||||
content.removeEventListener("scroll", handleScroll);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(props.className, "relative z-base overflow-hidden")}
|
||||
className={cn(props.className, "relative overflow-hidden")}
|
||||
ref={wrapperEl}
|
||||
style={
|
||||
{
|
||||
"--shadow": props.shadowColor || "hsl(var(--background))",
|
||||
"--shadow": props.shadowColor || "hsl(var(--muted))",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(styles.scrollShadowTop, styles.scrollShadowY)}
|
||||
className={cn(
|
||||
styles.scrollShadowTop,
|
||||
styles.scrollShadowY,
|
||||
props.shadowClassName,
|
||||
)}
|
||||
ref={shadowTopEl}
|
||||
style={{
|
||||
opacity: "0",
|
||||
@@ -89,32 +106,42 @@ export function ScrollShadow(props: {
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={cn(styles.scrollShadowBottom, styles.scrollShadowY)}
|
||||
className={cn(
|
||||
styles.scrollShadowBottom,
|
||||
styles.scrollShadowY,
|
||||
props.shadowClassName,
|
||||
)}
|
||||
ref={shadowBottomEl}
|
||||
style={{
|
||||
opacity: "0",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={cn(styles.scrollShadowLeft, styles.scrollShadowX)}
|
||||
className={cn(
|
||||
styles.scrollShadowLeft,
|
||||
styles.scrollShadowX,
|
||||
props.shadowClassName,
|
||||
)}
|
||||
ref={shadowLeftEl}
|
||||
style={{
|
||||
opacity: "0",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={cn(styles.scrollShadowRight, styles.scrollShadowX)}
|
||||
className={cn(
|
||||
styles.scrollShadowRight,
|
||||
styles.scrollShadowX,
|
||||
props.shadowClassName,
|
||||
)}
|
||||
ref={shadowRightEl}
|
||||
style={{
|
||||
opacity: "0",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={cn("overflow-auto", props.scrollableClassName)}
|
||||
style={{
|
||||
scrollbarWidth: "none",
|
||||
}}
|
||||
className={cn("no-scrollbar overflow-auto", props.scrollableClassName)}
|
||||
ref={scrollableEl}
|
||||
data-scrollable
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
.loader {
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
animation: rotate 1s linear infinite;
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
.loader circle {
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid #fff;
|
||||
animation: prixClipFix 2s linear infinite;
|
||||
stroke-linecap: round;
|
||||
animation: dash 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
0% {
|
||||
stroke-dasharray: 1, 150;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
50% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -35;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -124;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { cn } from "../../../lib/utils";
|
||||
import style from "./Spinner.module.css";
|
||||
|
||||
export function Spinner(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 50 50"
|
||||
className={cn(style.loader, props.className || "size-4")}
|
||||
>
|
||||
<title>loading</title>
|
||||
<circle
|
||||
cx="25"
|
||||
cy="25"
|
||||
r="20"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"bg-card relative w-full rounded-lg border border-border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "text-foreground",
|
||||
destructive: "text-destructive-text [&>svg]:text-destructive-text",
|
||||
info: "text-link-foreground [&>svg]:text-link-foreground",
|
||||
warning: "text-warning-text [&>svg]:text-warning-text",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Alert.displayName = "Alert";
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("font-medium leading-tight tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertTitle.displayName = "AlertTitle";
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-1.5 text-muted-foreground text-sm [&_p]:leading-relaxed",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDescription.displayName = "AlertDescription";
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
@@ -4,17 +4,20 @@ import type * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
"inline-flex items-center rounded-full border border-border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 leading-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
default: "border-transparent bg-primary/20 text-primary",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
"border-transparent bg-accent text-accent-foreground hover:bg-accent/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
"border-transparent dark:bg-red-950 dark:text-red-400 bg-red-500/20 text-red-800",
|
||||
warning:
|
||||
"border-transparent dark:bg-yellow-600/20 dark:text-yellow-500 bg-yellow-500/20 text-yellow-900",
|
||||
outline: "text-foreground",
|
||||
success:
|
||||
"border-transparent dark:bg-green-950/50 dark:text-green-400 bg-green-200 text-green-950",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Breadcrumb = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.ComponentPropsWithoutRef<"nav"> & {
|
||||
separator?: React.ReactNode;
|
||||
}
|
||||
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
|
||||
Breadcrumb.displayName = "Breadcrumb";
|
||||
|
||||
const BreadcrumbList = React.forwardRef<
|
||||
HTMLOListElement,
|
||||
React.ComponentPropsWithoutRef<"ol">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 break-words text-muted-foreground text-sm sm:gap-2.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbList.displayName = "BreadcrumbList";
|
||||
|
||||
const BreadcrumbItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentPropsWithoutRef<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbItem.displayName = "BreadcrumbItem";
|
||||
|
||||
const BreadcrumbLink = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentPropsWithoutRef<"a"> & {
|
||||
asChild?: boolean;
|
||||
}
|
||||
>(({ asChild, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn("transition-colors hover:text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
BreadcrumbLink.displayName = "BreadcrumbLink";
|
||||
|
||||
const BreadcrumbPage = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.ComponentPropsWithoutRef<"span">
|
||||
>(({ className, ...props }, ref) => (
|
||||
// biome-ignore lint/a11y/useFocusableInteractive: required
|
||||
<span
|
||||
ref={ref}
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("font-normal text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbPage.displayName = "BreadcrumbPage";
|
||||
|
||||
const BreadcrumbSeparator = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) => (
|
||||
<li
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
);
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
||||
|
||||
const BreadcrumbEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
};
|
||||
@@ -5,26 +5,29 @@ import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
primary:
|
||||
"bg-primary hover:bg-primary/90 text-semibold text-primary-foreground ",
|
||||
default: "bg-foreground text-background hover:bg-foreground/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
"bg-destructive hover:bg-destructive/90 text-semibold text-destructive-foreground ",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground text-semibold",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
"bg-secondary hover:bg-secondary/80 text-semibold text-secondary-foreground ",
|
||||
ghost: "hover:bg-accent text-semibold hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline text-semibold",
|
||||
upsell:
|
||||
"bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:from-purple-600 hover:to-pink-600 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -43,10 +46,13 @@ export interface ButtonProps
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const btnOnlyProps =
|
||||
Comp === "button" ? { type: "button" as const } : undefined;
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...btnOnlyProps}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import type * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import * as React from "react";
|
||||
import {
|
||||
Controller,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from "react-hook-form";
|
||||
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ToolTipLabel } from "./tooltip";
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue,
|
||||
);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState, formState } = useFormContext();
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>");
|
||||
}
|
||||
|
||||
const { id } = itemContext;
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
};
|
||||
};
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue,
|
||||
);
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
});
|
||||
FormItem.displayName = "FormItem";
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive-text", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = "FormLabel";
|
||||
|
||||
const RequiredFormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<ToolTipLabel label="Required">
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive-text", "relative", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
<span className="-top-1.5 -right-2 absolute text-destructive-text">
|
||||
•
|
||||
</span>
|
||||
</Label>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
});
|
||||
RequiredFormLabel.displayName = "RequiredFormLabel";
|
||||
FormLabel.displayName = "FormLabel";
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormControl.displayName = "FormControl";
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormDescription.displayName = "FormDescription";
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message) : children;
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("font-medium text-destructive-text text-sm", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
});
|
||||
FormMessage.displayName = "FormMessage";
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
RequiredFormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -14,58 +14,60 @@ const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
||||
chevronClassName?: string;
|
||||
}
|
||||
>(({ className, children, chevronClassName, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
"flex h-10 w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
<ChevronDown className={cn("h-4 w-4 opacity-50", chevronClassName)} />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
// const SelectScrollUpButton = React.forwardRef<
|
||||
// React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
// React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
// >(({ className, ...props }, ref) => (
|
||||
// <SelectPrimitive.ScrollUpButton
|
||||
// ref={ref}
|
||||
// className={cn(
|
||||
// "flex cursor-default items-center justify-center py-1",
|
||||
// className,
|
||||
// )}
|
||||
// {...props}
|
||||
// >
|
||||
// <ChevronUp className="h-4 w-4" />
|
||||
// </SelectPrimitive.ScrollUpButton>
|
||||
// ));
|
||||
// SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName;
|
||||
// const SelectScrollDownButton = React.forwardRef<
|
||||
// React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
// React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
// >(({ className, ...props }, ref) => (
|
||||
// <SelectPrimitive.ScrollDownButton
|
||||
// ref={ref}
|
||||
// className={cn(
|
||||
// "flex cursor-default items-center justify-center py-1",
|
||||
// className,
|
||||
// )}
|
||||
// {...props}
|
||||
// >
|
||||
// <ChevronDown className="h-4 w-4" />
|
||||
// </SelectPrimitive.ScrollDownButton>
|
||||
// ));
|
||||
// SelectScrollDownButton.displayName =
|
||||
// SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
@@ -75,7 +77,7 @@ const SelectContent = React.forwardRef<
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
position === "popper" &&
|
||||
"data-[side=left]:-translate-x-1 data-[side=top]:-translate-y-1 data-[side=right]:translate-x-1 data-[side=bottom]:translate-y-1",
|
||||
className,
|
||||
@@ -83,7 +85,7 @@ const SelectContent = React.forwardRef<
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
{/* <SelectScrollUpButton /> */}
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
@@ -93,7 +95,7 @@ const SelectContent = React.forwardRef<
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
{/* <SelectScrollDownButton /> */}
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
@@ -155,6 +157,6 @@ export {
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
// SelectScrollUpButton,
|
||||
// SelectScrollDownButton,
|
||||
};
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useRef } from "react";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { Suspense, useRef } from "react";
|
||||
import { ClientOnly } from "../ClientOnly";
|
||||
import { CustomAccordion } from "./CustomAccordion";
|
||||
|
||||
export type LinkMeta = {
|
||||
@@ -33,11 +34,13 @@ type ReferenceSideBarProps = {
|
||||
|
||||
export function Sidebar(props: ReferenceSideBarProps) {
|
||||
return (
|
||||
<ul className="transform-gpu pb-10">
|
||||
<ul className="transform-gpu">
|
||||
{props.links.map((link, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||
<li key={i}>
|
||||
<SidebarItem link={link} onLinkClick={props.onLinkClick} />
|
||||
<Suspense fallback={null}>
|
||||
<SidebarItem link={link} onLinkClick={props.onLinkClick} />
|
||||
</Suspense>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -45,15 +48,13 @@ export function Sidebar(props: ReferenceSideBarProps) {
|
||||
}
|
||||
|
||||
function SidebarItem(props: { link: SidebarLink; onLinkClick?: () => void }) {
|
||||
const pathname = usePathname();
|
||||
const href = useCurrentHref();
|
||||
|
||||
if ("separator" in props.link) {
|
||||
return <hr className="my-2 border-t" />;
|
||||
}
|
||||
|
||||
const isActive = props.link.href
|
||||
? isSamePage(pathname, props.link.href)
|
||||
: false;
|
||||
const isActive = props.link.href ? isSamePage(href, props.link.href) : false;
|
||||
|
||||
const { link } = props;
|
||||
if ("links" in link) {
|
||||
@@ -93,13 +94,13 @@ function DocSidebarNonCollapsible(props: {
|
||||
linkGroup: LinkGroup;
|
||||
onLinkClick?: () => void;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const currentHref = useCurrentHref();
|
||||
const { href, name, links } = props.linkGroup;
|
||||
const isCategoryActive = href ? isSamePage(pathname, href) : false;
|
||||
const isCategoryActive = href ? isSamePage(currentHref, href) : false;
|
||||
|
||||
return (
|
||||
<div className="my-4">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<div className="mb-4">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
{href ? (
|
||||
<Link
|
||||
className={cn(
|
||||
@@ -111,10 +112,10 @@ function DocSidebarNonCollapsible(props: {
|
||||
{name}
|
||||
</Link>
|
||||
) : (
|
||||
<div className="font-semibold lg:text-base">{name}</div>
|
||||
<div className="font-medium text-lg">{name}</div>
|
||||
)}
|
||||
</div>
|
||||
<ul className="flex flex-col">
|
||||
<ul className="mb-8 flex flex-col">
|
||||
{links.map((link, i) => {
|
||||
return (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||
@@ -128,13 +129,21 @@ function DocSidebarNonCollapsible(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function useCurrentHref() {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
return decodeURIComponent(
|
||||
`${pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarCategory(props: {
|
||||
linkGroup: LinkGroup;
|
||||
onLinkClick?: () => void;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const currentPageHref = useCurrentHref();
|
||||
const { href, name, links, expanded } = props.linkGroup;
|
||||
const isCategoryActive = href ? isSamePage(pathname, href) : false;
|
||||
const isCategoryActive = href ? isSamePage(currentPageHref, href) : false;
|
||||
|
||||
const hasActiveHref = containsActiveHref(
|
||||
{
|
||||
@@ -142,8 +151,9 @@ function SidebarCategory(props: {
|
||||
links: links,
|
||||
href: href,
|
||||
},
|
||||
pathname,
|
||||
currentPageHref,
|
||||
);
|
||||
|
||||
const defaultOpen = isCategoryActive || !!(hasActiveHref || expanded);
|
||||
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -152,7 +162,7 @@ function SidebarCategory(props: {
|
||||
<div
|
||||
className={cn(
|
||||
isCategoryActive && "!font-semibold !text-foreground",
|
||||
"text-muted-foreground",
|
||||
"group-hover:!text-foreground text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-2 py-1 font-medium" ref={triggerRef}>
|
||||
@@ -170,24 +180,26 @@ function SidebarCategory(props: {
|
||||
);
|
||||
|
||||
return (
|
||||
<CustomAccordion
|
||||
defaultOpen={defaultOpen}
|
||||
containerClassName="border-none"
|
||||
triggerContainerClassName="lg:text-base"
|
||||
trigger={triggerEl}
|
||||
chevronPosition="right"
|
||||
>
|
||||
<ul className="flex flex-col border-l-2 pl-4">
|
||||
{links.map((link, i) => {
|
||||
return (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||
<li key={i}>
|
||||
<SidebarItem link={link} onLinkClick={props.onLinkClick} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</CustomAccordion>
|
||||
<ClientOnly ssr={triggerEl}>
|
||||
<CustomAccordion
|
||||
defaultOpen={defaultOpen}
|
||||
containerClassName="border-none"
|
||||
triggerContainerClassName="lg:text-base group"
|
||||
trigger={triggerEl}
|
||||
chevronPosition="right"
|
||||
>
|
||||
<ul className="flex flex-col border-l pl-4">
|
||||
{links.map((link, i) => {
|
||||
return (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||
<li key={i}>
|
||||
<SidebarItem link={link} onLinkClick={props.onLinkClick} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</CustomAccordion>
|
||||
</ClientOnly>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -210,20 +222,6 @@ function containsActiveHref(
|
||||
return false;
|
||||
}
|
||||
|
||||
function isSamePage(pathname: string, pathOrHref: string): boolean {
|
||||
try {
|
||||
if (pathOrHref === pathname) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const u1 = new URL(pathname, window.location.href);
|
||||
const u2 = new URL(pathOrHref, window.location.href);
|
||||
if (u1.pathname === u2.pathname && u1.origin === u2.origin) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return false;
|
||||
function isSamePage(currentPageHref: string, testHref: string): boolean {
|
||||
return currentPageHref === testHref;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ScrollShadow } from "./ScrollShadow/ScrollShadow";
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"w-full caption-bottom border-collapse align-top text-sm lining-nums tabular-nums",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Table.displayName = "Table";
|
||||
|
||||
@@ -20,8 +22,17 @@ const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
<thead
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-border border-b bg-background",
|
||||
"sticky top-0 z-20 border-none before:absolute before:inset-0 before:border-border before:border-b",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
@@ -43,7 +54,7 @@ const TableFooter = React.forwardRef<
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
"border-border border-t bg-card font-medium [&>tr]:last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -53,12 +64,19 @@ TableFooter.displayName = "TableFooter";
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
React.HTMLAttributes<HTMLTableRowElement> & {
|
||||
/**
|
||||
* Contain the absolutely position elements inside the row with position:relative + transform:translate(0)
|
||||
* transform:translate(0) is required because position:relative on tr element does not work on webkit
|
||||
*/
|
||||
linkBox?: boolean;
|
||||
}
|
||||
>(({ className, linkBox, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
"border-border border-b last:border-0 data-[state=selected]:bg-muted",
|
||||
linkBox && "relative translate-x-0 translate-y-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -73,7 +91,7 @@ const TableHead = React.forwardRef<
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
"px-6 py-4 text-left align-middle font-medium text-muted-foreground text-xs uppercase tracking-wider [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -87,7 +105,10 @@ const TableCell = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
className={cn(
|
||||
"px-6 py-4 text-start align-middle text-sm [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -99,12 +120,33 @@ const TableCaption = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-muted-foreground text-sm", className)}
|
||||
className={cn("py-4 text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
function TableContainer(props: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
scrollableContainerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<ScrollShadow
|
||||
shadowClassName="z-30"
|
||||
disableTopShadow
|
||||
className={cn(
|
||||
"relative whitespace-nowrap rounded-lg border border-border bg-card",
|
||||
props.className,
|
||||
)}
|
||||
scrollableClassName={props.scrollableContainerClassName}
|
||||
shadowColor="hsl(var(--muted))"
|
||||
>
|
||||
{props.children}
|
||||
</ScrollShadow>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
@@ -114,4 +156,5 @@ export {
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
TableContainer,
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"fade-in-0 zoom-in-95 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 animate-in overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-popover-foreground text-sm shadow-md data-[state=closed]:animate-out",
|
||||
"fade-in-0 zoom-in-95 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 animate-in overflow-hidden rounded-md border border-border bg-popover px-3 py-1.5 text-popover-foreground text-sm shadow-md data-[state=closed]:animate-out",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -34,6 +34,10 @@ export function ToolTipLabel(props: {
|
||||
label: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
leftIcon?: React.ReactNode;
|
||||
hoverable?: boolean;
|
||||
contentClassName?: string;
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
align?: "center" | "start" | "end";
|
||||
}) {
|
||||
if (!props.label) {
|
||||
return props.children;
|
||||
@@ -41,13 +45,18 @@ export function ToolTipLabel(props: {
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={100} disableHoverableContent={true}>
|
||||
<Tooltip delayDuration={100} disableHoverableContent={!props.hoverable}>
|
||||
<TooltipTrigger asChild className="!pointer-events-auto">
|
||||
{props.children}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side={props.side}
|
||||
align={props.align}
|
||||
sideOffset={10}
|
||||
className="max-w-[400px] leading-relaxed"
|
||||
className={cn(
|
||||
"max-w-[400px] whitespace-normal leading-relaxed",
|
||||
props.contentClassName,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 p-2 text-sm">
|
||||
{props.leftIcon}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const isProd =
|
||||
(process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV) ===
|
||||
"production";
|
||||
@@ -16,6 +16,7 @@ module.exports = {
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
"active-border": "hsl(var(--active-border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
@@ -31,6 +32,7 @@ module.exports = {
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
text: "hsl(var(--destructive-text))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
@@ -48,9 +50,22 @@ module.exports = {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
success: {
|
||||
text: "hsl(var(--success-text))",
|
||||
},
|
||||
inverted: {
|
||||
DEFAULT: "hsl(var(--inverted))",
|
||||
foreground: "hsl(var(--inverted-foreground))",
|
||||
},
|
||||
code: {
|
||||
DEFAULT: "hsl(var(--code))",
|
||||
},
|
||||
warning: {
|
||||
text: "hsl(var(--warning-text))",
|
||||
},
|
||||
link: {
|
||||
foreground: "hsl(var(--link-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19 10.4399C18.3096 10.4399 17.75 10.9996 17.75 11.6899C17.75 12.3803 18.3096 12.9399 19 12.9399C19.6904 12.9399 20.25 12.3803 20.25 11.6899C20.25 10.9996 19.6904 10.4399 19 10.4399ZM16.25 11.6899C16.25 10.1712 17.4812 8.93994 19 8.93994C20.5188 8.93994 21.75 10.1712 21.75 11.6899C21.75 13.2087 20.5188 14.4399 19 14.4399C17.4812 14.4399 16.25 13.2087 16.25 11.6899Z" fill="white" stroke="white" stroke-width="0.25" stroke-miterlimit="10"/>
|
||||
<path d="M12 13.9341C13.1046 13.9341 14 13.0387 14 11.9341C14 10.8295 13.1046 9.93408 12 9.93408C10.8954 9.93408 10 10.8295 10 11.9341C10 13.0387 10.8954 13.9341 12 13.9341Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 10.4399C4.30964 10.4399 3.75 10.9996 3.75 11.6899C3.75 12.3803 4.30964 12.9399 5 12.9399C5.69036 12.9399 6.25 12.3803 6.25 11.6899C6.25 10.9996 5.69036 10.4399 5 10.4399ZM2.25 11.6899C2.25 10.1712 3.48122 8.93994 5 8.93994C6.51878 8.93994 7.75 10.1712 7.75 11.6899C7.75 13.2087 6.51878 14.4399 5 14.4399C3.48122 14.4399 2.25 13.2087 2.25 11.6899Z" fill="white" stroke="white" stroke-width="0.25" stroke-miterlimit="10"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.7277 14.182C4.24985 16.0934 4.54726 17.6287 5.45936 18.5408C6.37146 19.4529 7.90677 19.7503 9.8182 19.2725C11.7127 18.7989 13.8394 17.5854 15.7124 15.7124C16.5051 14.9198 17.1796 14.0817 17.7299 13.235C18.0753 13.5193 18.5178 13.6901 19.0001 13.6901C19.0756 13.6901 19.1502 13.6859 19.2235 13.6777C18.5704 14.7489 17.7476 15.7985 16.7731 16.7731C14.7409 18.8053 12.3822 20.1776 10.182 20.7277C7.99867 21.2735 5.82974 21.0325 4.3987 19.6015C2.96766 18.1705 2.72666 16.0015 3.27249 13.8182C3.33854 13.554 3.41645 13.2875 3.50594 13.0196C3.8447 13.4 4.32658 13.6502 4.86709 13.6857C4.81514 13.8527 4.76865 14.0182 4.7277 14.182ZM6.5215 10.3918C7.02185 9.67265 7.61187 8.9637 8.28779 8.28779C10.1608 6.41476 12.2875 5.20134 14.182 4.7277C16.0934 4.24985 17.6287 4.54726 18.5408 5.45936C19.436 6.35453 19.7391 7.84998 19.2983 9.71214C19.8482 9.79435 20.325 10.1005 20.6329 10.5348C20.6668 10.4168 20.6984 10.2992 20.7277 10.182C21.2735 7.99867 21.0325 5.82974 19.6015 4.3987C18.1705 2.96766 16.0015 2.72666 13.8182 3.27249C11.618 3.82255 9.25934 5.19491 7.22713 7.22713C6.44286 8.01139 5.75687 8.84427 5.17848 9.6979C5.71512 9.74534 6.19088 10.0047 6.5215 10.3918Z" fill="white"/>
|
||||
<mask id="mask0_4_23" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="2" y="2" width="21" height="21">
|
||||
<path d="M22.0034 2.00342H2.00342V22.0034H22.0034V2.00342Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.7277 14.182C4.24985 16.0934 4.54726 17.6287 5.45936 18.5408C6.37146 19.4529 7.90677 19.7503 9.8182 19.2725C11.7127 18.7989 13.8394 17.5854 15.7124 15.7124C16.5051 14.9198 17.1796 14.0817 17.7299 13.235C18.0753 13.5193 18.5178 13.6901 19.0001 13.6901C19.0756 13.6901 19.1502 13.6859 19.2235 13.6777C18.5704 14.7489 17.7476 15.7985 16.7731 16.7731C14.7409 18.8053 12.3822 20.1776 10.182 20.7277C7.99867 21.2735 5.82974 21.0325 4.3987 19.6015C2.96766 18.1705 2.72666 16.0015 3.27249 13.8182C3.33854 13.554 3.41645 13.2875 3.50594 13.0196C3.8447 13.4 4.32658 13.6502 4.86709 13.6857C4.81514 13.8527 4.76865 14.0182 4.7277 14.182ZM6.5215 10.3918C7.02185 9.67265 7.61187 8.9637 8.28779 8.28779C10.1608 6.41476 12.2875 5.20134 14.182 4.7277C16.0934 4.24985 17.6287 4.54726 18.5408 5.45936C19.436 6.35453 19.7391 7.84998 19.2983 9.71214C19.8482 9.79435 20.325 10.1005 20.6329 10.5348C20.6668 10.4168 20.6984 10.2992 20.7277 10.182C21.2735 7.99867 21.0325 5.82974 19.6015 4.3987C18.1705 2.96766 16.0015 2.72666 13.8182 3.27249C11.618 3.82255 9.25934 5.19491 7.22713 7.22713C6.44286 8.01139 5.75687 8.84427 5.17848 9.6979C5.71512 9.74534 6.19088 10.0047 6.5215 10.3918Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_4_23)">
|
||||
<path d="M17.7299 13.235L17.8888 13.042L17.6728 12.8642L17.5203 13.0988L17.7299 13.235ZM19.2235 13.6777L19.437 13.8079L19.7022 13.373L19.1959 13.4292L19.2235 13.6777ZM3.50594 13.0196L3.69265 12.8533L3.40557 12.5309L3.26882 12.9404L3.50594 13.0196ZM4.86709 13.6857L5.1058 13.76L5.20009 13.457L4.88347 13.4362L4.86709 13.6857ZM6.5215 10.3918L6.3314 10.5542L6.54175 10.8004L6.72672 10.5346L6.5215 10.3918ZM19.2983 9.71212L19.055 9.65453L18.9924 9.91917L19.2613 9.95937L19.2983 9.71212ZM20.6329 10.5348L20.4289 10.6793L20.7295 11.1034L20.8731 10.6039L20.6329 10.5348ZM5.17848 9.69788L4.97152 9.55765L4.73312 9.90949L5.15647 9.94691L5.17848 9.69788ZM5.63614 18.3641C4.81019 17.5381 4.50387 16.1081 4.97024 14.2426L4.48517 14.1214C3.99582 16.0788 4.28432 17.7193 5.28258 18.7176L5.63614 18.3641ZM9.75756 19.03C7.8921 19.4963 6.46208 19.19 5.63614 18.3641L5.28258 18.7176C6.28085 19.7159 7.92144 20.0044 9.87883 19.515L9.75756 19.03ZM15.5356 15.5356C13.6891 17.3821 11.6012 18.5691 9.75756 19.03L9.87883 19.515C11.8243 19.0287 13.9896 17.7888 15.8892 15.8892L15.5356 15.5356ZM17.5203 13.0988C16.9797 13.9305 16.3162 14.755 15.5356 15.5356L15.8892 15.8892C16.6939 15.0845 17.3795 14.2328 17.9395 13.3712L17.5203 13.0988ZM19.0001 13.4401C18.5778 13.4401 18.1911 13.2908 17.8888 13.042L17.571 13.428C17.9596 13.7478 18.4577 13.9401 19.0001 13.9401V13.4401ZM19.1959 13.4292C19.1317 13.4364 19.0664 13.4401 19.0001 13.4401V13.9401C19.0849 13.9401 19.1687 13.9354 19.2512 13.9262L19.1959 13.4292ZM16.9498 16.9498C17.9379 15.9617 18.7732 14.8966 19.437 13.8079L19.0101 13.5476C18.3676 14.6013 17.5573 15.6353 16.5963 16.5963L16.9498 16.9498ZM10.2426 20.9702C12.4938 20.4074 14.8911 19.0086 16.9498 16.9498L16.5963 16.5963C14.5906 18.602 12.2706 19.9479 10.1214 20.4852L10.2426 20.9702ZM4.22192 19.7783C5.7397 21.296 8.01447 21.5273 10.2426 20.9702L10.1214 20.4852C7.98287 21.0198 5.91978 20.769 4.57548 19.4247L4.22192 19.7783ZM3.02996 13.7576C2.47291 15.9857 2.70415 18.2605 4.22192 19.7783L4.57548 19.4247C3.23117 18.0804 2.9804 16.0173 3.51503 13.8788L3.02996 13.7576ZM3.26882 12.9404C3.17736 13.2142 3.09764 13.4869 3.02996 13.7576L3.51503 13.8788C3.57946 13.6211 3.65554 13.3608 3.74306 13.0987L3.26882 12.9404ZM3.31923 13.1858C3.69996 13.6134 4.24222 13.8952 4.85072 13.9352L4.88347 13.4362C4.41095 13.4052 3.98945 13.1866 3.69265 12.8533L3.31923 13.1858ZM4.97024 14.2426C5.01001 14.0836 5.0552 13.9226 5.1058 13.76L4.62839 13.6114C4.57507 13.7827 4.5273 13.9528 4.48517 14.1214L4.97024 14.2426ZM6.72672 10.5346C7.21849 9.82774 7.79896 9.13015 8.46456 8.46454L8.11101 8.11099C7.42478 8.79722 6.82521 9.51752 6.31628 10.249L6.72672 10.5346ZM8.46456 8.46454C10.3111 6.61805 12.399 5.43112 14.2426 4.97022L14.1214 4.48515C12.1759 4.97152 10.0106 6.21142 8.11101 8.11099L8.46456 8.46454ZM14.2426 4.97022C16.1081 4.50385 17.5381 4.81017 18.3641 5.63612L18.7176 5.28256C17.7193 4.2843 16.0788 3.9958 14.1214 4.48515L14.2426 4.97022ZM18.3641 5.63612C19.1747 6.44678 19.4848 7.83893 19.055 9.65453L19.5416 9.76971C19.9934 7.861 19.6973 6.26225 18.7176 5.28256L18.3641 5.63612ZM19.2613 9.95937C19.7418 10.0312 20.1592 10.2988 20.4289 10.6793L20.8368 10.3902C20.4909 9.90213 19.9547 9.55746 19.3353 9.46487L19.2613 9.95937ZM20.8731 10.6039C20.9078 10.4831 20.9402 10.3627 20.9702 10.2426L20.4852 10.1214C20.4566 10.2356 20.4257 10.3504 20.3926 10.4657L20.8731 10.6039ZM20.9702 10.2426C21.5273 8.01445 21.296 5.73968 19.7783 4.2219L19.4247 4.57546C20.769 5.91976 21.0198 7.98285 20.4852 10.1214L20.9702 10.2426ZM19.7783 4.2219C18.2605 2.70413 15.9857 2.47289 13.7576 3.02994L13.8788 3.51501C16.0173 2.98038 18.0804 3.23115 19.4247 4.57546L19.7783 4.2219ZM13.7576 3.02994C11.5064 3.59273 9.10909 4.99159 7.05035 7.05033L7.4039 7.40388C9.40959 5.39819 11.7296 4.05232 13.8788 3.51501L13.7576 3.02994ZM7.05035 7.05033C6.25512 7.84556 5.55897 8.69064 4.97152 9.55765L5.38545 9.83812C5.95476 8.99787 6.6306 8.17719 7.4039 7.40388L7.05035 7.05033ZM6.71159 10.2294C6.34004 9.7944 5.80474 9.50226 5.20049 9.44885L5.15647 9.94691C5.6255 9.98837 6.04171 10.215 6.3314 10.5542L6.71159 10.2294Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.8 KiB |
@@ -64,4 +64,19 @@ Options for configuring timeouts for fetching data. Useful for fine-tuning reque
|
||||
TimeoutOptions(int? storage = null, int? rpc = null, int? other = null)
|
||||
```
|
||||
|
||||
### rpcOverrides (optional)
|
||||
|
||||
Overrides for the default RPC endpoints. Will use your custom endpoints instead of thirdweb RPC endpoints.
|
||||
|
||||
```csharp
|
||||
var client = ThirdwebClient.Create(
|
||||
...,
|
||||
rpcOverrides: new()
|
||||
{
|
||||
{ 1, "https://eth.llamarpc.com" },
|
||||
{ 42161, "https://arbitrum.llamarpc.com" }
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Details, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "ThirdwebContract.Create | Thirdweb .NET SDK",
|
||||
description:
|
||||
"Instantiate a ThirdwebContract to interact with smart contracts.",
|
||||
});
|
||||
|
||||
# [Nebula AI](https://thirdweb.com/nebula) .NET Integration
|
||||
The last piece of the puzzle required to create .NET apps and games leveraging a blockchain-powered AI assistant or NPC that also has the power to fully execute transactions.
|
||||
|
||||
## Read, Write, Reason.
|
||||
The best way to understand is to look at examples.
|
||||
|
||||
We'll prepare some context for the AI - here, we'll generate a basic `PrivateKeyWallet` and upgrade it to a `SmartWallet` on Sepolia.
|
||||
```csharp
|
||||
// Prepare some context
|
||||
var myChain = 11155111; // Sepolia
|
||||
var mySigner = await PrivateKeyWallet.Generate(client);
|
||||
var myWallet = await SmartWallet.Create(mySigner, myChain);
|
||||
var myContractAddress = "0xe2cb0eb5147b42095c2FfA6F7ec953bb0bE347D8"; // DropERC1155
|
||||
var usdcAddress = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"; // Sepolia USDC
|
||||
```
|
||||
|
||||
A one liner creates a Nebula session.
|
||||
```csharp
|
||||
// Create a Nebula session
|
||||
var nebula = await ThirdwebNebula.Create(client);
|
||||
```
|
||||
|
||||
You can **Chat** with Nebula. The Chat method accepts any `IThirdwebWallet` as an optional parameter, context will automatically be updated.
|
||||
```csharp
|
||||
// Chat, passing wallet context
|
||||
var response1 = await nebula.Chat(message: "What is my wallet address?", wallet: myWallet);
|
||||
Console.WriteLine($"Response 1: {response1.Message}");
|
||||
```
|
||||
|
||||
You may also pass it smart contract context.
|
||||
```csharp
|
||||
// Chat, passing contract context
|
||||
var response2 = await nebula.Chat(
|
||||
message: "What's the total supply of token id 0 for this contract?",
|
||||
context: new NebulaContext(contractAddresses: new List<string> { myContractAddress }, chainIds: new List<BigInteger> { myChain })
|
||||
);
|
||||
Console.WriteLine($"Response 2: {response2.Message}");
|
||||
```
|
||||
|
||||
You can have a full on conversation with it with our **Chat** override accepting multiple messages.
|
||||
```csharp
|
||||
// Chat, passing multiple messages and context
|
||||
var response3 = await nebula.Chat(
|
||||
messages: new List<NebulaChatMessage>
|
||||
{
|
||||
new($"Tell me the name of this contract: {myContractAddress}", NebulaChatRole.User),
|
||||
new("The name of the contract is CatDrop", NebulaChatRole.Assistant),
|
||||
new("What's the symbol of this contract?", NebulaChatRole.User),
|
||||
},
|
||||
context: new NebulaContext(contractAddresses: new List<string> { myContractAddress }, chainIds: new List<BigInteger> { myChain })
|
||||
);
|
||||
Console.WriteLine($"Response 3: {response3.Message}");
|
||||
```
|
||||
|
||||
Chatting is cool, but what if I just want it to _**do**_ things and stop talking so much?
|
||||
|
||||
You can **Execute** transactions directly with a simple prompt.
|
||||
```csharp
|
||||
// Execute, this directly sends transactions
|
||||
var executionResult = await nebula.Execute("Approve 1 USDC to vitalik.eth", wallet: myWallet, context: new NebulaContext(contractAddresses: new List<string>() { usdcAddress }));
|
||||
if (executionResult.TransactionReceipts != null && executionResult.TransactionReceipts.Count > 0)
|
||||
{
|
||||
Console.WriteLine($"Receipt: {executionResult.TransactionReceipts[0]}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Message: {executionResult.Message}");
|
||||
}
|
||||
```
|
||||
|
||||
Similarly, **Execute** can take in multiple messages.
|
||||
```csharp
|
||||
// Batch execute
|
||||
var batchExecutionResult = await nebula.Execute(
|
||||
new List<NebulaChatMessage>
|
||||
{
|
||||
new("What's the address of vitalik.eth", NebulaChatRole.User),
|
||||
new("The address of vitalik.eth is 0xd8dA6BF26964aF8E437eEa5e3616511D7G3a3298", NebulaChatRole.Assistant),
|
||||
new("Approve 1 USDC to them", NebulaChatRole.User),
|
||||
},
|
||||
wallet: myWallet,
|
||||
context: new NebulaContext(contractAddresses: new List<string>() { usdcAddress })
|
||||
);
|
||||
if (batchExecutionResult.TransactionReceipts != null && batchExecutionResult.TransactionReceipts.Count > 0)
|
||||
{
|
||||
Console.WriteLine($"Receipts: {JsonConvert.SerializeObject(batchExecutionResult.TransactionReceipts, Formatting.Indented)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Message: {batchExecutionResult.Message}");
|
||||
}
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SideBar } from "@/components/Layouts/DocLayout";
|
||||
import type { SidebarLink } from "@/components/others/Sidebar";
|
||||
import { ZapIcon } from "lucide-react";
|
||||
import { CodeIcon, ExternalLink, ZapIcon } from "lucide-react";
|
||||
|
||||
const walletProviders: SidebarLink = (() => {
|
||||
const parentSlug = "/dotnet/wallets/providers";
|
||||
@@ -261,16 +261,29 @@ export const sidebar: SideBar = {
|
||||
icon: <ZapIcon />,
|
||||
},
|
||||
{
|
||||
name: "Godot Integration",
|
||||
href: "/dotnet/godot",
|
||||
name: "API Reference",
|
||||
href: "https://thirdweb-dev.github.io/dotnet/index.html",
|
||||
isCollapsible: false,
|
||||
icon: <ExternalLink />,
|
||||
},
|
||||
{
|
||||
name: "Unity Integration",
|
||||
href: "/unity/v5",
|
||||
},
|
||||
{
|
||||
name: "MAUI Integration",
|
||||
href: "/dotnet/maui",
|
||||
name: "Integrations",
|
||||
isCollapsible: true,
|
||||
icon: <CodeIcon />,
|
||||
links: [
|
||||
{
|
||||
name: "Unity",
|
||||
href: "/unity/v5",
|
||||
},
|
||||
{
|
||||
name: "Godot",
|
||||
href: "/dotnet/godot",
|
||||
},
|
||||
{
|
||||
name: "MAUI",
|
||||
href: "/dotnet/maui",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Core",
|
||||
@@ -291,7 +304,6 @@ export const sidebar: SideBar = {
|
||||
isCollapsible: false,
|
||||
links: [walletProviders, walletActions],
|
||||
},
|
||||
pay,
|
||||
{
|
||||
name: "Blockchain API",
|
||||
isCollapsible: false,
|
||||
@@ -304,10 +316,20 @@ export const sidebar: SideBar = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
name: "Full Reference",
|
||||
href: "https://thirdweb-dev.github.io/dotnet/index.html",
|
||||
name: "Nebula AI",
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
name: "Quickstart",
|
||||
href: "/dotnet/nebula/quickstart",
|
||||
},
|
||||
{
|
||||
name: "Nebula Full Reference",
|
||||
href: "https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.AI.ThirdwebNebula.html",
|
||||
},
|
||||
],
|
||||
},
|
||||
pay,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -15,11 +15,13 @@ Create an instance of `EcosystemWallet` using a user's email, phone number or OA
|
||||
Ecosystem Wallets support a variety of login methods:
|
||||
- Email (OTP Login)
|
||||
- Phone (OTP Login)
|
||||
- Socials (Google, Apple, Facebook, Telegram, Farcaster, Line, Github, Twitch, Steam, etc.)
|
||||
- SIWE (Sign-In with Ethereum)
|
||||
- Socials (Google, Apple, Facebook, Telegram, Farcaster, Line, Github, Twitch, Steam etc.)
|
||||
- Custom Auth (OIDC Compatible)
|
||||
- Custom Auth (Generic Auth Endpoint)
|
||||
- Guest (Onboard easily, link other accounts later)
|
||||
- Backend (Server Wallets)
|
||||
- Siwe (Login with a seperate wallet supported by the SDK)
|
||||
- SiweExternal (Login with an external wallet that only supports web using a browser loading a static thirdweb React page temporarily)
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -32,14 +34,18 @@ var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosyste
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", phoneNumber: "+1234567890");
|
||||
// Google, Apple, Facebook, etc.
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Google);
|
||||
// SIWE
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Siwe, siweSigner: anyExternalWallet);
|
||||
// Custom Auth - JWT
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.JWT);
|
||||
// Custom Auth - AuthEndpoint
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.AuthEndpoint);
|
||||
// Guest Login
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Guest);
|
||||
// Server Login
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Backend, walletSecret: "very-secret");
|
||||
// Siwe
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Siwe, siweSigner: anyExternalWallet);
|
||||
// SiweExternal
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.SiweExternal);
|
||||
|
||||
// Session resuming supported for all methods
|
||||
var isConnected = await wallet.IsConnected();
|
||||
@@ -62,9 +68,6 @@ var address = await wallet.LoginWithOauth(
|
||||
mobileRedirectScheme: "myBundleId://"
|
||||
);
|
||||
|
||||
// SIWE (Wallet)
|
||||
var address = await siweWallet.LoginWithSiwe(chainId: 1);
|
||||
|
||||
// Custom Auth (JWT)
|
||||
var address = await wallet.LoginWithCustomAuth(jwt: "myjwt");
|
||||
|
||||
@@ -73,11 +76,26 @@ var address = await wallet.LoginWithAuthEndpoint(payload: "mypayload");
|
||||
|
||||
// Guest Login (Easy onboarding)
|
||||
var address = await wallet.LoginWithGuest();
|
||||
|
||||
// Backend (Server Wallets)
|
||||
var address = await wallet.LoginWithBackend();
|
||||
|
||||
// SIWE (Wallet)
|
||||
var address = await siweWallet.LoginWithSiwe(chainId: 1);
|
||||
|
||||
// SiweExternal (React-only wallet)
|
||||
var address = await wallet.LoginWithSiweExternal(
|
||||
// Windows console app example, adaptable to any runtime
|
||||
isMobile: false,
|
||||
browserOpenAction: (url) =>
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
|
||||
_ = Process.Start(psi);
|
||||
},
|
||||
forceWalletIds: new List<string> { "io.metamask", "com.coinbase.wallet", "xyz.abs" }
|
||||
);
|
||||
```
|
||||
|
||||
<Callout variant="info" title="Client-Side Use">
|
||||
This wallet is designed for client-side use in applications where direct access to the user's private keys is not safe or necessary. It leverages OTP for secure authentication, allowing users to interact with blockchain applications seamlessly.
|
||||
</Callout>
|
||||
<Details summary="Parameters">
|
||||
|
||||
### client (required)
|
||||
@@ -108,6 +126,18 @@ The OAuth provider to use for authentication. Supported values are `AuthProvider
|
||||
|
||||
The path to the directory where the wallet data is stored. Defaults to the application's data directory.
|
||||
|
||||
### siweSigner (optional)
|
||||
|
||||
An external wallet instance to use for SIWE authentication.
|
||||
|
||||
### legacyEncryptionKey (optional)
|
||||
|
||||
The encryption key that is no longer required but was used in the past. Only pass this if you had used custom auth before this was deprecated.
|
||||
|
||||
### walletSecret (optional)
|
||||
|
||||
The secret identifier to use when creating server-side wallets with backend authentication.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
@@ -246,6 +276,31 @@ if (!await ecosystemWallet.IsConnected())
|
||||
|
||||
**Note:** The `LoginWithOauth` API allows for custom browser handling, making it suitable for various application types and platforms.
|
||||
|
||||
## External Wallet Auth (Siwe & SiweExternal)
|
||||
|
||||
**LoginWithSiwe:** Initiate the login process by calling LoginWithSiwe on the EcosystemWallet instance. This will prompt the external wallet to sign a message instantly.
|
||||
|
||||
```csharp
|
||||
var address = await siweWallet.LoginWithSiwe(chainId: 1);
|
||||
```
|
||||
|
||||
**LoginWithSiweExternal:** Initiate the login process by calling LoginWithSiweExternal on the EcosystemWallet instance. This will initiate a browser-based login flow for external wallets that only support web platforms.
|
||||
|
||||
```csharp
|
||||
var address = await wallet.LoginWithSiweExternal(
|
||||
// Windows console app example, adaptable to any runtime
|
||||
isMobile: false,
|
||||
browserOpenAction: (url) =>
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
|
||||
_ = Process.Start(psi);
|
||||
},
|
||||
forceWalletIds: new List<string> { "io.metamask", "com.coinbase.wallet", "xyz.abs" }
|
||||
);
|
||||
```
|
||||
|
||||
Note: The parameters are similar to the OAuth flow, with the addition of `forceWalletIds` to specify the wallet IDs to force the user to use. Using a single wallet id will skip the wallet selection screen and directly open the wallet.
|
||||
|
||||
## Unified Identity - Account Linking
|
||||
|
||||
EcosystemWallet supports linking multiple authentication methods to a single user account. This feature enables users to access their account using different authentication methods, such as email, phone, or OAuth, without creating separate accounts for each method.
|
||||
|
||||
@@ -15,11 +15,13 @@ Create an instance of `InAppWallet` using a user's email, phone number or OAuth.
|
||||
In-App Wallets support a variety of login methods:
|
||||
- Email (OTP Login)
|
||||
- Phone (OTP Login)
|
||||
- Socials (Google, Apple, Facebook, Telegram, Farcaster, Line, Github, Twitch etc.)
|
||||
- SIWE (Sign-In with Ethereum)
|
||||
- Socials (Google, Apple, Facebook, Telegram, Farcaster, Line, Github, Twitch, Steam etc.)
|
||||
- Custom Auth (OIDC Compatible)
|
||||
- Custom Auth (Generic Auth Endpoint)
|
||||
- Guest (Onboard easily, link other accounts later)
|
||||
- Backend (Server Wallets)
|
||||
- Siwe (Login with a seperate wallet supported by the SDK)
|
||||
- SiweExternal (Login with an external wallet that only supports web using a browser loading a static thirdweb React page temporarily)
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -30,14 +32,18 @@ var wallet = await InAppWallet.Create(client: client, email: "userEmail");
|
||||
var wallet = await InAppWallet.Create(client: client, phoneNumber: "+1234567890");
|
||||
// Google, Apple, Facebook, etc.
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.Google);
|
||||
// SIWE
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.Siwe, siweSigner: anyExternalWallet);
|
||||
// Custom Auth - JWT
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.JWT);
|
||||
// Custom Auth - AuthEndpoint
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.AuthEndpoint);
|
||||
// Guest Login
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.Guest);
|
||||
// Server Login
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.Backend, walletSecret: "very-secret");
|
||||
// Siwe
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.Siwe, siweSigner: anyExternalWallet);
|
||||
// SiweExternal
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.SiweExternal);
|
||||
|
||||
// Session resuming supported for all methods
|
||||
var isConnected = await wallet.IsConnected();
|
||||
@@ -60,9 +66,6 @@ var address = await wallet.LoginWithOauth(
|
||||
mobileRedirectScheme: "myBundleId://"
|
||||
);
|
||||
|
||||
// SIWE (Wallet)
|
||||
var address = await siweWallet.LoginWithSiwe(chainId: 1);
|
||||
|
||||
// Custom Auth (JWT)
|
||||
var address = await wallet.LoginWithCustomAuth(jwt: "myjwt");
|
||||
|
||||
@@ -71,11 +74,26 @@ var address = await wallet.LoginWithAuthEndpoint(payload: "mypayload");
|
||||
|
||||
// Guest Login (Easy onboarding)
|
||||
var address = await wallet.LoginWithGuest();
|
||||
|
||||
// Backend (Server Wallets)
|
||||
var address = await wallet.LoginWithBackend();
|
||||
|
||||
// SIWE (Wallet)
|
||||
var address = await siweWallet.LoginWithSiwe(chainId: 1);
|
||||
|
||||
// SiweExternal (React-only wallet)
|
||||
var address = await wallet.LoginWithSiweExternal(
|
||||
// Windows console app example, adaptable to any runtime
|
||||
isMobile: false,
|
||||
browserOpenAction: (url) =>
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
|
||||
_ = Process.Start(psi);
|
||||
},
|
||||
forceWalletIds: new List<string> { "io.metamask", "com.coinbase.wallet", "xyz.abs" }
|
||||
);
|
||||
```
|
||||
|
||||
<Callout variant="info" title="Client-Side Use">
|
||||
This wallet is designed for client-side use in applications where direct access to the user's private keys is not safe or necessary. It leverages OTP for secure authentication, allowing users to interact with blockchain applications seamlessly.
|
||||
</Callout>
|
||||
<Details summary="Parameters">
|
||||
|
||||
### client (required)
|
||||
@@ -98,6 +116,18 @@ The OAuth provider to use for authentication. Supported values are `AuthProvider
|
||||
|
||||
The path to the directory where the wallet data is stored. Defaults to the application's data directory.
|
||||
|
||||
### siweSigner (optional)
|
||||
|
||||
An external wallet instance to use for SIWE authentication.
|
||||
|
||||
### legacyEncryptionKey (optional)
|
||||
|
||||
The encryption key that is no longer required but was used in the past. Only pass this if you had used custom auth before this was deprecated.
|
||||
|
||||
### walletSecret (optional)
|
||||
|
||||
The secret identifier to use when creating server-side wallets with backend authentication.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
@@ -108,179 +138,7 @@ Returns an instance of InAppWallet, initialized for the user based on the provid
|
||||
|
||||
</Details>
|
||||
|
||||
## OTP Authentication Flow
|
||||
## Authentication, Linking and more.
|
||||
|
||||
The OTP authentication flow involves sending an OTP to the user's email or phone and then verifying the OTP to complete authentication:
|
||||
`InAppWallet` extends `EcosystemWallet`, refer to the [EcosystemWallet](/dotnet/wallets/providers/ecosystem-wallet) documentation for information about authentication, linking multiple auth providers to the same wallet, and more. All functionality outside of creation and third-party integrations is the same as `EcosystemWallet`.
|
||||
|
||||
**Send OTP:** Initiate the login process by calling SendOTP on the InAppWallet instance. This sends an OTP to the user's email or phone number.
|
||||
|
||||
```csharp
|
||||
await wallet.SendOTP();
|
||||
```
|
||||
|
||||
**Submit OTP:** Once the user receives the OTP, they submit it back to the application, which then calls LoginWithOtp on the InAppWallet instance to verify the OTP and complete the login process.
|
||||
|
||||
```csharp
|
||||
var address = await wallet.LoginWithOtp("userEnteredOTP");
|
||||
// If this fails, feel free to catch and take in another OTP and retry the login process
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Here's an example of creating an `InAppWallet` with a user's email and completing the OTP authentication flow:
|
||||
|
||||
```csharp
|
||||
// Create InAppWallet wallet as signer to unlock web2 auth
|
||||
var inAppWallet = await InAppWallet.Create(client: client, email: "[email protected]"); // or email: null, phoneNumber: "+1234567890"
|
||||
|
||||
// Resume session (if `InAppWallet` wallet was not logged in)
|
||||
if (!await inAppWallet.IsConnected())
|
||||
{
|
||||
await inAppWallet.SendOTP();
|
||||
Console.WriteLine("Please submit the OTP.");
|
||||
var otp = Console.ReadLine();
|
||||
var inAppWalletAddress = await inAppWallet.LoginWithOtp(otp); // try catch and retry if needed
|
||||
}
|
||||
|
||||
Console.WriteLine($"InAppWallet address: {await inAppWallet.GetAddress()}");
|
||||
|
||||
// Sign a message
|
||||
var message = "Hello, Thirdweb!";
|
||||
var signature = await wallet.PersonalSign(message);
|
||||
Console.WriteLine($"Signature: {signature}");
|
||||
```
|
||||
|
||||
**Note:** InAppWallet leverages the security of OTP-based authentication to ensure a secure and user-friendly experience in blockchain applications.
|
||||
|
||||
## OAuth Authentication Flow
|
||||
|
||||
**LoginWithOauth:** Initiate the login process by calling LoginWithOauth on the InAppWallet instance. This redirects the user to the OAuth provider's login page.
|
||||
|
||||
```csharp
|
||||
// Windows console app example
|
||||
var address = await inAppWallet.LoginWithOauth(
|
||||
isMobile: false,
|
||||
browserOpenAction: (url) =>
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
|
||||
_ = Process.Start(psi);
|
||||
},
|
||||
);
|
||||
// Godot standalone example
|
||||
var address = await ThirdwebManager.Instance.InAppWallet.LoginWithOauth(
|
||||
isMobile: OS.GetName() == "Android" || OS.GetName() == "iOS",
|
||||
browserOpenAction: (url) => OS.ShellOpen(url),
|
||||
mobileRedirectScheme: "thirdweb://"
|
||||
);
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### isMobile
|
||||
|
||||
A `bool` indicating whether the application is running on a mobile platform.
|
||||
|
||||
### browserOpenAction
|
||||
|
||||
An `Action<string>` that opens the OAuth provider's login page in a browser.
|
||||
|
||||
### mobileRedirectScheme
|
||||
|
||||
The redirect scheme to use for mobile platforms. Defaults to `"thirdweb://"`.
|
||||
|
||||
### browser
|
||||
|
||||
An instance of `IThirdwebBrowser` to use for the OAuth login process. Defaults to `null`.
|
||||
|
||||
### cancellationToken
|
||||
|
||||
A `CancellationToken` to cancel the operation. Defaults to `default`.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### string
|
||||
|
||||
The InAppWallet address as a hexadecimal `string`.
|
||||
|
||||
</Details>
|
||||
|
||||
## Example
|
||||
|
||||
Here's an example of creating an `InAppWallet` using OAuth.
|
||||
|
||||
```csharp
|
||||
// Create InAppWallet wallet as signer to unlock web2 auth
|
||||
var inAppWallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.Google);
|
||||
|
||||
// Resume session (if `InAppWallet` wallet was not logged in)
|
||||
if (!await inAppWallet.IsConnected())
|
||||
{
|
||||
try {
|
||||
var address = await inAppWallet.LoginWithOauth(
|
||||
isMobile: false,
|
||||
browserOpenAction: (url) =>
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
|
||||
_ = Process.Start(psi);
|
||||
},
|
||||
);
|
||||
Console.WriteLine($"OAuth login successful. InAppWallet address: {address}");
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine($"OAuth login failed: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The `LoginWithOauth` API allows for custom browser handling, making it suitable for various application types and platforms.
|
||||
|
||||
## Unified Identity - Account Linking
|
||||
|
||||
InAppWallet supports linking multiple authentication methods to a single user account. This feature enables users to access their account using different authentication methods, such as email, phone, or OAuth, without creating separate accounts for each method.
|
||||
|
||||
### Linking Accounts
|
||||
|
||||
```csharp
|
||||
var inAppWalletMain = await InAppWallet.Create(client: client, authProvider: AuthProvider.Google);
|
||||
if (!await inAppWalletMain.IsConnected())
|
||||
{
|
||||
_ = await inAppWalletMain.LoginWithOauth(
|
||||
isMobile: false,
|
||||
(url) =>
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
|
||||
_ = Process.Start(psi);
|
||||
},
|
||||
"thirdweb://",
|
||||
new InAppWalletBrowser()
|
||||
);
|
||||
}
|
||||
Console.WriteLine($"Main InAppWallet address: {await inAppWalletMain.GetAddress()}");
|
||||
|
||||
// Prepare Telegram
|
||||
var socialWallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.Telegram);
|
||||
// Link Telegram
|
||||
_ = await inAppWalletMain.LinkAccount(walletToLink: socialWallet,);
|
||||
|
||||
// Prepare Phone
|
||||
var phoneWallet = await InAppWallet.Create(client: client, phoneNumber: "+1234567890");
|
||||
_ = await phoneWallet.SendOTP();
|
||||
var otp = Console.ReadLine();
|
||||
// Link Phone
|
||||
_ = await inAppWalletMain.LinkAccount(walletToLink: phoneWallet, otp: otp);
|
||||
```
|
||||
|
||||
### Getting Linked Accounts
|
||||
|
||||
```csharp
|
||||
List<LinkedAccount> linkedAccounts = await inAppWalletMain.GetLinkedAccounts();
|
||||
```
|
||||
|
||||
### Unlinking Accounts
|
||||
|
||||
```csharp
|
||||
List<LinkedAccount> linkedAccounts = await inAppWallet.GetLinkedAccounts();
|
||||
List<LinkedAccount> linkedAccountsAfterUnlinking = await inAppWallet.UnlinkAccount(linkedAccounts[0]);
|
||||
```
|
||||
@@ -12,6 +12,10 @@ export default async function Layout(props: { children: React.ReactNode }) {
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "thirdweb Nebula Documentation",
|
||||
image: {
|
||||
title: "Nebula Docs",
|
||||
icon: "nebula",
|
||||
},
|
||||
description:
|
||||
"thirdweb Nebula Docs : explore the Nebula API Reference and unlock the most powerful AI to interact with the blockchain yet.",
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SideBar } from "@/components/Layouts/DocLayout";
|
||||
import { ZapIcon } from "lucide-react";
|
||||
import { CodeIcon, ZapIcon } from "lucide-react";
|
||||
|
||||
const sdkSlug = "/unity/v5";
|
||||
const walletProvidersSlug = `${sdkSlug}/wallets`;
|
||||
@@ -17,6 +17,12 @@ export const sidebar: SideBar = {
|
||||
href: `${sdkSlug}/getting-started`,
|
||||
icon: <ZapIcon />,
|
||||
},
|
||||
{
|
||||
name: "API Reference",
|
||||
href: "/dotnet",
|
||||
isCollapsible: false,
|
||||
icon: <CodeIcon />,
|
||||
},
|
||||
{
|
||||
name: "Core",
|
||||
isCollapsible: false,
|
||||
@@ -61,6 +67,20 @@ export const sidebar: SideBar = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Blockchain API",
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
name: "Interacting with Contracts",
|
||||
href: `${sdkSlug}/contracts`,
|
||||
},
|
||||
{
|
||||
name: "Full Reference",
|
||||
href: "https://thirdweb-dev.github.io/dotnet/index.html",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Pay",
|
||||
isCollapsible: false,
|
||||
@@ -72,27 +92,19 @@ export const sidebar: SideBar = {
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Blockchain API",
|
||||
name: "Nebula AI",
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
name: "Interacting with Contracts",
|
||||
href: `${sdkSlug}/contracts`,
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
name: "Migrate from v4",
|
||||
href: `${sdkSlug}/migration-guide`,
|
||||
},
|
||||
{
|
||||
name: ".NET SDK Portal",
|
||||
href: "/dotnet",
|
||||
},
|
||||
{
|
||||
name: "Full Reference",
|
||||
href: "https://thirdweb-dev.github.io/dotnet/index.html",
|
||||
name: ".NET SDK QuickStart",
|
||||
href: "/dotnet/nebula/quickstart",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
name: "Migrate from v4",
|
||||
href: `${sdkSlug}/migration-guide`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ This section allows you to define the default preferences for the SDK:
|
||||
|
||||
- `Initialize On Awake`: Whether the SDK should initialize on awake or not. If not, you can call `ThirdwebManager.Instance.Initialize()` to initialize it manually.
|
||||
- `Show Debug Logs`: Whether to show thirdweb SDK debug logs.
|
||||
- `Opt Out Usage Analytics`: Whether to opt out of Connect usage analytics, such as the number of wallets connected to your game.
|
||||
- `Auto-Connect Last Wallet`: If enabled, we will automatically connect to the last connected wallet on initialization (this behavior does not apply to the WalletConnectWallet provider option). If a wallet was connected as a non smart wallet, then later upgraded, the smart wallet is saved as the last wallet, and the next session will autoconnect to the smart wallet. Any failure during this entire flow should not throw.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
@@ -52,8 +52,10 @@ This section allows you to define the default preferences for the SDK:
|
||||
|
||||
This section allows you to customize the SDK's behavior:
|
||||
|
||||
- `Supported Chains`: A list of chains that the wallets should be able to connect to by default when using `WalletConnect` as a wallet provider.
|
||||
- `Redirect Page HTML Override`: Raw HTML to override the default redirect page when connecting to `InAppWallet` or `EcosystemWallet` providers.
|
||||
- `RPC Overrides`: A list of RPC overrides to use when connecting to a chain. You can bypass using thirdweb's RPC that way (not recommended).
|
||||
- `OAuth Redirect Page HTML Override`: Raw HTML to override the default redirect page when connecting to `InAppWallet` or `EcosystemWallet` providers.
|
||||
- `WalletConnect Supported Chains`: A list of chains that the wallets should be able to connect to by default when using `WalletConnect` as a wallet provider.
|
||||
- `WalletConnect Included Wallet IDs`: A list of wallet IDs that should be included in the WalletConnect modal by default (optional override).
|
||||
|
||||
### Debug
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 24 KiB |
@@ -20,11 +20,13 @@ Ecosystem Wallets have a very similar API to the [In-App Wallet](/unity/v5/walle
|
||||
Ecosystem Wallets support a variety of login methods:
|
||||
- Email (OTP Login)
|
||||
- Phone (OTP Login)
|
||||
- Socials (Google, Apple, Facebook, Telegram, Farcaster, Line, Github, Twitch, Steam, etc.)
|
||||
- SIWE (Sign-In with Ethereum)
|
||||
- Socials (Google, Apple, Facebook, Telegram, Farcaster, Line, Github, Twitch, Steam etc.)
|
||||
- Custom Auth (OIDC Compatible)
|
||||
- Custom Auth (Generic Auth Endpoint)
|
||||
- Guest (Onboard easily, link other accounts later)
|
||||
- Backend (Server Wallets)
|
||||
- Siwe (Login with a seperate wallet supported by the SDK)
|
||||
- SiweExternal (Login with an external wallet that only supports web using a browser loading a static thirdweb React page temporarily)
|
||||
|
||||
### Login with Email
|
||||
|
||||
@@ -95,6 +97,29 @@ var wallet = await ThirdwebManager.Instance.ConnectWallet(options);
|
||||
|
||||
Will use the external wallet to sign a message and login to the EcosystemWallet.
|
||||
|
||||
### Login with SiweExternal
|
||||
|
||||
```csharp
|
||||
var ecosystemWalletOptions = new EcosystemWalletOptions(ecosystemId: "ecosystem.your-ecosystem", authprovider: AuthProvider.SiweExternal)
|
||||
var options = new WalletOptions(
|
||||
provider: WalletProvider.EcosystemWallet,
|
||||
chainId: 421614,
|
||||
ecosystemWalletOptions: ecosystemWalletOptions
|
||||
);
|
||||
var wallet = await ConnectWallet(options);
|
||||
```
|
||||
|
||||
Will open a browser and load a static thirdweb React page to authenticate the user and redirect back to the game.
|
||||
You can pass `forceSiweExternalWalletIds` to force the page to use one or more wallets.
|
||||
|
||||
Example:
|
||||
```csharp
|
||||
var ecosystemWalletOptions = new EcosystemWalletOptions(
|
||||
ecosystemId: "ecosystem.your-ecosystem",
|
||||
authprovider: AuthProvider.SiweExternal,
|
||||
forceSiweExternalWalletIds: new List<string> { "xyz.abs" });
|
||||
```
|
||||
|
||||
### Login with Custom Auth - OIDC Compatible
|
||||
|
||||
```csharp
|
||||
@@ -142,6 +167,22 @@ var options = new WalletOptions(
|
||||
var wallet = await ThirdwebManager.Instance.ConnectWallet(options);
|
||||
```
|
||||
|
||||
### Login with Backend - Server Wallets
|
||||
|
||||
```csharp
|
||||
var ecosystemWalletOptions = new EcosystemWalletOptions(
|
||||
ecosystemId: "ecosystem.your-ecosystem",
|
||||
authprovider: AuthProvider.Backend,
|
||||
walletSecret: "very-secret"
|
||||
);
|
||||
var options = new WalletOptions(
|
||||
provider: WalletProvider.EcosystemWallet,
|
||||
chainId: 1,
|
||||
ecosystemWalletOptions: ecosystemWalletOptions
|
||||
);
|
||||
var wallet = await ThirdwebManager.Instance.ConnectWallet(options);
|
||||
```
|
||||
|
||||
## Account Linking
|
||||
|
||||
EcosystemWallets support linking multiple authentication methods to a single wallet, for instance linking Google to your Email-based In-App-Wallet. This is useful to have a unified identity across platforms.
|
||||
|
||||
@@ -18,11 +18,13 @@ It makes for a fantastic [SmartWallet](/unity/v5/wallets/account-abstraction) ad
|
||||
In-App Wallets support a variety of login methods:
|
||||
- Email (OTP Login)
|
||||
- Phone (OTP Login)
|
||||
- Socials (Google, Apple, Facebook, Telegram, Farcaster, Line, Github, Twitch, Steam, etc.)
|
||||
- SIWE (Sign-In with Ethereum)
|
||||
- Socials (Google, Apple, Facebook, Telegram, Farcaster, Line, Github, Twitch, Steam etc.)
|
||||
- Custom Auth (OIDC Compatible)
|
||||
- Custom Auth (Generic Auth Endpoint)
|
||||
- Guest (Onboard easily, link other accounts later)
|
||||
- Backend (Server Wallets)
|
||||
- Siwe (Login with a seperate wallet supported by the SDK)
|
||||
- SiweExternal (Login with an external wallet that only supports web using a browser loading a static thirdweb React page temporarily)
|
||||
|
||||
### Login with Email
|
||||
|
||||
@@ -66,10 +68,10 @@ var wallet = await ThirdwebManager.Instance.ConnectWallet(options);
|
||||
|
||||
Will open a native browser or oauth session to authenticate the user and redirect back to the game.
|
||||
|
||||
### Login with SIWE
|
||||
### Login with Siwe
|
||||
|
||||
```csharp
|
||||
var inAppWalletOptions = new InAppWalletOptions(authprovider: AuthProvider.Siwe, siweSigner: anyExternalWallet);
|
||||
var inAppWalletOptions = new InAppWalletOptions(authprovider: AuthProvider.Siwe, siweSigner: sdkSupportedExternalWallet);
|
||||
var options = new WalletOptions(
|
||||
provider: WalletProvider.InAppWallet,
|
||||
chainId: 1,
|
||||
@@ -78,7 +80,29 @@ var options = new WalletOptions(
|
||||
var wallet = await ThirdwebManager.Instance.ConnectWallet(options);
|
||||
```
|
||||
|
||||
Will use the external wallet to sign a message and login to the InAppWallet.
|
||||
Will use a separate wallet to sign a message and login to the InAppWallet.
|
||||
|
||||
### Login with SiweExternal
|
||||
|
||||
```csharp
|
||||
var inAppWalletOptions = new InAppWalletOptions(authprovider: AuthProvider.SiweExternal)
|
||||
var options = new WalletOptions(
|
||||
provider: WalletProvider.InAppWallet,
|
||||
chainId: 421614,
|
||||
inAppWalletOptions: inAppWalletOptions
|
||||
);
|
||||
var wallet = await ConnectWallet(inAppWalletOptions);
|
||||
```
|
||||
|
||||
Will open a browser and load a static thirdweb React page to authenticate the user and redirect back to the game.
|
||||
You can pass `forceSiweExternalWalletIds` to force the page to use one or more wallets.
|
||||
|
||||
Example:
|
||||
```csharp
|
||||
var inAppWalletOptions = new InAppWalletOptions(
|
||||
authprovider: AuthProvider.SiweExternal,
|
||||
forceSiweExternalWalletIds: new List<string> { "xyz.abs" });
|
||||
```
|
||||
|
||||
### Login with Custom Auth - OIDC Compatible
|
||||
|
||||
@@ -122,6 +146,21 @@ var options = new WalletOptions(
|
||||
var wallet = await ThirdwebManager.Instance.ConnectWallet(options);
|
||||
```
|
||||
|
||||
### Login with Backend - Server Wallets
|
||||
|
||||
```csharp
|
||||
var inAppWalletOptions = new InAppWalletOptions(
|
||||
authprovider: AuthProvider.Backend,
|
||||
walletSecret: "very-secret"
|
||||
);
|
||||
var options = new WalletOptions(
|
||||
provider: WalletProvider.InAppWallet,
|
||||
chainId: 1,
|
||||
inAppWalletOptions: inAppWalletOptions
|
||||
);
|
||||
var wallet = await ThirdwebManager.Instance.ConnectWallet(options);
|
||||
```
|
||||
|
||||
## Account Linking
|
||||
|
||||
InAppWallets support linking multiple authentication methods to a single wallet, for instance linking Google to your Email-based In-App-Wallet. This is useful to have a unified identity across platforms.
|
||||
|
||||
@@ -20,6 +20,7 @@ type DynamicImageOptions = {
|
||||
| "storage"
|
||||
| "changelog"
|
||||
| "dotnet"
|
||||
| "nebula"
|
||||
| "unreal-engine"
|
||||
| "insight";
|
||||
};
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
coverage:
|
||||
ignore:
|
||||
- "**/__generated__/**"
|
||||
- "**/contract/deployment/zksync/**"
|
||||
- "*/thirdweb/src/contract/deployment/zksync/zkDeployContract.ts"
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
@@ -10,6 +14,8 @@ coverage:
|
||||
- packages
|
||||
ignore:
|
||||
- "**/__generated__/**"
|
||||
- "**/contract/deployment/zksync/**"
|
||||
- "*/thirdweb/src/contract/deployment/zksync/zkDeployContract.ts"
|
||||
|
||||
|
||||
github_checks:
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @thirdweb-dev/service-utils
|
||||
|
||||
## 0.8.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6206](https://github.com/thirdweb-dev/js/pull/6206) [`8aeb4d6`](https://github.com/thirdweb-dev/js/commit/8aeb4d6308a2624a3e483aa4da408bbbdc8da688) Thanks [@arcoraven](https://github.com/arcoraven)! - Export usageV2 sources
|
||||
|
||||
## 0.8.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6172](https://github.com/thirdweb-dev/js/pull/6172) [`a8ac98b`](https://github.com/thirdweb-dev/js/commit/a8ac98bd493d1d6e824aad868dea1ec42ff22768) Thanks [@catalyst17](https://github.com/catalyst17)! - add `canCreatePublicChains` to TeamResponse
|
||||
|
||||
- [#6183](https://github.com/thirdweb-dev/js/pull/6183) [`18f0e57`](https://github.com/thirdweb-dev/js/commit/18f0e575c4a9764b3192931451cbd3f632a21e3c) Thanks [@arcoraven](https://github.com/arcoraven)! - [service-utils] Update list of usageV2 sources"
|
||||
|
||||
## 0.8.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/service-utils",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.7",
|
||||
"type": "module",
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
|
||||
@@ -51,6 +51,7 @@ export type TeamResponse = {
|
||||
billingEmail: string | null;
|
||||
billingStatus: "noPayment" | "validPayment" | "invalidPayment" | null;
|
||||
growthTrialEligible: false;
|
||||
canCreatePublicChains: boolean | null;
|
||||
enabledScopes: ServiceName[];
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { ServiceName } from "./services.js";
|
||||
|
||||
export type UsageV2Source = ServiceName | "sdk";
|
||||
export const USAGE_V2_SOURCES = [
|
||||
"bundler",
|
||||
"engine",
|
||||
"insight",
|
||||
"nebula",
|
||||
"rpc",
|
||||
"sdk",
|
||||
"storage",
|
||||
"wallet",
|
||||
] as const;
|
||||
export type UsageV2Source = (typeof USAGE_V2_SOURCES)[number];
|
||||
export function getTopicName(source: UsageV2Source) {
|
||||
return `usage_v2.raw_${source}`;
|
||||
}
|
||||
|
||||
export interface UsageV2Event {
|
||||
/**
|
||||
@@ -54,7 +65,3 @@ export interface UsageV2Event {
|
||||
*/
|
||||
[key: string]: boolean | number | string | Date | null | undefined;
|
||||
}
|
||||
|
||||
export function getTopicName(source: UsageV2Source) {
|
||||
return `usage_v2.raw_${source}`;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export const validTeamResponse: TeamResponse = {
|
||||
billingEmail: "[email protected]",
|
||||
billingStatus: "noPayment",
|
||||
growthTrialEligible: false,
|
||||
canCreatePublicChains: false,
|
||||
enabledScopes: ["storage", "rpc", "bundler"],
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,87 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.88.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6204](https://github.com/thirdweb-dev/js/pull/6204) [`f1cd253`](https://github.com/thirdweb-dev/js/commit/f1cd2539d1be15eb18807b3f5f2b90509e3d58cf) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Update coinbase wallet sdk
|
||||
|
||||
## 5.88.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#6194](https://github.com/thirdweb-dev/js/pull/6194) [`9663079`](https://github.com/thirdweb-dev/js/commit/966307906212ac99dc0a2a9be88e514c920d39c4) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Added session keys to smart wallet options
|
||||
|
||||
You can now pass a `sessionKey` to the `smartWallet` options function to immediately add a session key to the smart wallet upon connection.
|
||||
|
||||
This is great in combination with an engine backend wallet! Let's you act on behalf of the user from your backend, making executing transactions as easy as a REST API call. Also unblocks automations, like renewing a subscription, or paying for a service.
|
||||
|
||||
```ts
|
||||
const wallet = smartWallet({
|
||||
sessionKey: {
|
||||
address: "0x...", // the session key address (ex: engine backend wallet)
|
||||
permissions: {
|
||||
approvedTargets: ["0x..."], // allowed contract addresses (or * for all)
|
||||
nativeTokenLimitPerTransaction: 0.1, // max spend per transaction in ETH
|
||||
permissionEndTimestamp: new Date(Date.now() + 1000 * 60 * 60), // expiration date
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// this will connect the user wallet and add the session key if not already added
|
||||
await wallet.connect({
|
||||
client: TEST_CLIENT,
|
||||
personalAccount,
|
||||
});
|
||||
```
|
||||
|
||||
You can also pass the `sessionKey` to the `ConnectButton`, `ConnectEmbed` components and `useConnect` hook.
|
||||
|
||||
```tsx
|
||||
<ConnectButton
|
||||
client={client}
|
||||
accountAbstraction={{
|
||||
chain,
|
||||
sponsorGas: true,
|
||||
sessionKey: {
|
||||
address: "0x...",
|
||||
permissions: {
|
||||
approvedTargets: "*",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
Also works for the `inAppWallet` `smartAccount` option!
|
||||
|
||||
```ts
|
||||
const wallet = inAppWallet({
|
||||
smartAccount: {
|
||||
chain,
|
||||
sponsorGas: true,
|
||||
sessionKey: {
|
||||
address: "0x...",
|
||||
permissions: {
|
||||
approvedTargets: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6193](https://github.com/thirdweb-dev/js/pull/6193) [`1f6bb7c`](https://github.com/thirdweb-dev/js/commit/1f6bb7c3294d70648b120a6a6a6cba13302a84fc) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Show fiat amount in PayEmbed main screen
|
||||
|
||||
- [#6192](https://github.com/thirdweb-dev/js/pull/6192) [`30e13e6`](https://github.com/thirdweb-dev/js/commit/30e13e6b9176265a2f4eddfa53578889abbcb750) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Allow editing pay amount mid flow
|
||||
|
||||
- [#6190](https://github.com/thirdweb-dev/js/pull/6190) [`2dfc245`](https://github.com/thirdweb-dev/js/commit/2dfc245d44dde86e42f6c799305db707316432aa) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Add fiat provider selection in PayEmbed
|
||||
|
||||
- [#6187](https://github.com/thirdweb-dev/js/pull/6187) [`ee57ded`](https://github.com/thirdweb-dev/js/commit/ee57ded902cb69da6fc171599a4a90776e650149) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Requery allowances when getting back to quote screen
|
||||
|
||||
- [#6188](https://github.com/thirdweb-dev/js/pull/6188) [`64d7bf3`](https://github.com/thirdweb-dev/js/commit/64d7bf358fe2014b684688d41d525a75e47f1b82) Thanks [@jnsdls](https://github.com/jnsdls)! - when explicitly passing `clientId` to `createThirdwebClient()` prefer it over computing the `clientId` from a passed `secretKey` option
|
||||
|
||||
## 5.87.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "thirdweb",
|
||||
"version": "5.87.4",
|
||||
"version": "5.88.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
@@ -207,7 +207,7 @@
|
||||
"!tsconfig.build.json"
|
||||
],
|
||||
"dependencies": {
|
||||
"@coinbase/wallet-sdk": "4.2.4",
|
||||
"@coinbase/wallet-sdk": "4.3.0",
|
||||
"@emotion/react": "11.14.0",
|
||||
"@emotion/styled": "11.14.0",
|
||||
"@google/model-viewer": "2.1.1",
|
||||
|
||||
@@ -13,9 +13,9 @@ describe("client", () => {
|
||||
expect(client.clientId).toBe(computeClientIdFromSecretKey("bar"));
|
||||
expect(client.secretKey).toBe("bar");
|
||||
});
|
||||
it("should ignore clientId if secretKey is provided", () => {
|
||||
it("should NOT ignore clientId if secretKey is provided", () => {
|
||||
const client = createThirdwebClient({ clientId: "foo", secretKey: "bar" });
|
||||
expect(client.clientId).toBe(computeClientIdFromSecretKey("bar"));
|
||||
expect(client.clientId).toBe("foo");
|
||||
expect(client.secretKey).toBe("bar");
|
||||
});
|
||||
it("should throw an error if neither clientId nor secretKey is provided", () => {
|
||||
|
||||
@@ -114,7 +114,8 @@ export function createThirdwebClient(
|
||||
throw new Error("clientId must be provided when using a JWT secretKey");
|
||||
}
|
||||
} else {
|
||||
realClientId = computeClientIdFromSecretKey(secretKey);
|
||||
// always PREFER the clientId if provided, only compute it from the secretKey if we don't have a clientId passed explicitly
|
||||
realClientId = clientId ?? computeClientIdFromSecretKey(secretKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and
|
||||
import { prepareTransaction } from "../../../transaction/prepare-transaction.js";
|
||||
import { normalizeFunctionParams } from "../../../utils/abi/normalizeFunctionParams.js";
|
||||
import { CONTRACT_DEPLOYER_ADDRESS } from "../../../utils/any-evm/zksync/constants.js";
|
||||
import { ensureBytecodePrefix } from "../../../utils/bytecode/prefix.js";
|
||||
import type { Hex } from "../../../utils/encoding/hex.js";
|
||||
import type { ClientAndChainAndAccount } from "../../../utils/types.js";
|
||||
import { zkDeployContractDeterministic } from "./zkDeployDeterministic.js";
|
||||
@@ -22,6 +23,8 @@ export async function zkDeployContract(
|
||||
deploymentType?: "create" | "create2";
|
||||
},
|
||||
) {
|
||||
const bytecode = ensureBytecodePrefix(options.bytecode);
|
||||
|
||||
if (options.salt !== undefined) {
|
||||
// if a salt is provided, use the deterministic deployer
|
||||
return zkDeployContractDeterministic(options);
|
||||
@@ -29,7 +32,7 @@ export async function zkDeployContract(
|
||||
|
||||
const data = encodeDeployData({
|
||||
abi: options.abi,
|
||||
bytecode: options.bytecode,
|
||||
bytecode,
|
||||
deploymentType: options.deploymentType ?? "create",
|
||||
args: normalizeFunctionParams(
|
||||
options.abi.find((abi) => abi.type === "constructor"),
|
||||
@@ -45,7 +48,7 @@ export async function zkDeployContract(
|
||||
to: CONTRACT_DEPLOYER_ADDRESS,
|
||||
data,
|
||||
eip712: {
|
||||
factoryDeps: [options.bytecode],
|
||||
factoryDeps: [bytecode],
|
||||
// TODO (zksync): allow passing in a paymaster
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { Hash } from "viem";
|
||||
import { getCachedChain } from "../../chains/utils.js";
|
||||
import type { ThirdwebClient } from "../../client/client.js";
|
||||
import { getContract } from "../../contract/contract.js";
|
||||
import { allowance } from "../../extensions/erc20/__generated__/IERC20/read/allowance.js";
|
||||
import { approve } from "../../extensions/erc20/write/approve.js";
|
||||
import type { PrepareTransactionOptions } from "../../transaction/prepare-transaction.js";
|
||||
import { getClientFetch } from "../../utils/fetch.js";
|
||||
import { stringify } from "../../utils/json.js";
|
||||
@@ -151,7 +148,7 @@ type BuyWithCryptoQuoteRouteResponse = {
|
||||
*/
|
||||
export type BuyWithCryptoQuote = {
|
||||
transactionRequest: PrepareTransactionOptions;
|
||||
approval?: PrepareTransactionOptions;
|
||||
approvalData?: QuoteApprovalInfo;
|
||||
|
||||
swapDetails: {
|
||||
fromAddress: string;
|
||||
@@ -254,28 +251,6 @@ export async function getBuyWithCryptoQuote(
|
||||
|
||||
// check if the fromAddress already has approval for the given amount
|
||||
const approvalData = data.approval;
|
||||
let approval = undefined;
|
||||
if (approvalData) {
|
||||
const contract = getContract({
|
||||
client: params.client,
|
||||
address: approvalData.tokenAddress,
|
||||
chain: getCachedChain(approvalData.chainId),
|
||||
});
|
||||
|
||||
const approvedAmount = await allowance({
|
||||
contract,
|
||||
spender: approvalData.spenderAddress,
|
||||
owner: params.fromAddress,
|
||||
});
|
||||
|
||||
if (approvedAmount < BigInt(approvalData.amountWei)) {
|
||||
approval = approve({
|
||||
contract,
|
||||
spender: approvalData.spenderAddress,
|
||||
amountWei: BigInt(approvalData.amountWei),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const swapRoute: BuyWithCryptoQuote = {
|
||||
transactionRequest: {
|
||||
@@ -287,7 +262,7 @@ export async function getBuyWithCryptoQuote(
|
||||
gas: BigInt(data.transactionRequest.gasLimit),
|
||||
gasPrice: undefined, // ignore gas price returned by the quote, we handle it ourselves
|
||||
},
|
||||
approval: approval,
|
||||
approvalData,
|
||||
swapDetails: {
|
||||
fromAddress: data.fromAddress,
|
||||
toAddress: data.toAddress,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { Hash } from "viem";
|
||||
import { getCachedChain } from "../../chains/utils.js";
|
||||
import type { ThirdwebClient } from "../../client/client.js";
|
||||
import { getContract } from "../../contract/contract.js";
|
||||
import { approve } from "../../extensions/erc20/write/approve.js";
|
||||
import type { PrepareTransactionOptions } from "../../transaction/prepare-transaction.js";
|
||||
import type { Address } from "../../utils/address.js";
|
||||
import { getClientFetch } from "../../utils/fetch.js";
|
||||
@@ -83,7 +81,7 @@ type BuyWithCryptoTransferResponse = {
|
||||
*/
|
||||
export type BuyWithCryptoTransfer = {
|
||||
transactionRequest: PrepareTransactionOptions;
|
||||
approval?: PrepareTransactionOptions;
|
||||
approvalData?: QuoteApprovalInfo;
|
||||
fromAddress: string;
|
||||
toAddress: string;
|
||||
paymentToken: QuotePaymentToken;
|
||||
@@ -159,17 +157,7 @@ export async function getBuyWithCryptoTransfer(
|
||||
value: BigInt(data.transactionRequest.value),
|
||||
gas: BigInt(data.transactionRequest.gasLimit),
|
||||
},
|
||||
approval: data.approval
|
||||
? approve({
|
||||
contract: getContract({
|
||||
client: params.client,
|
||||
address: data.approval.tokenAddress,
|
||||
chain: getCachedChain(data.approval.chainId),
|
||||
}),
|
||||
spender: data.approval.spenderAddress as Address,
|
||||
amountWei: BigInt(data.approval.amountWei),
|
||||
})
|
||||
: undefined,
|
||||
approvalData: data.approval,
|
||||
fromAddress: data.fromAddress,
|
||||
toAddress: data.toAddress,
|
||||
paymentToken: data.paymentToken,
|
||||
|
||||
@@ -238,6 +238,11 @@ export type BuyWithFiatQuote = {
|
||||
*
|
||||
*/
|
||||
onRampLink: string;
|
||||
|
||||
/**
|
||||
* The provider that was used to get the quote.
|
||||
*/
|
||||
provider: FiatProvider;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,4 +17,6 @@ export type PayOnChainTransactionDetails = {
|
||||
explorerLink?: string;
|
||||
};
|
||||
|
||||
export type FiatProvider = "STRIPE" | "TRANSAK" | "KADO" | "COINBASE";
|
||||
export type FiatProvider = (typeof FiatProviders)[number];
|
||||
|
||||
export const FiatProviders = ["COINBASE", "STRIPE", "TRANSAK", "KADO"] as const;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { AsyncStorage } from "../../../utils/storage/AsyncStorage.js";
|
||||
import type { AuthArgsType } from "../../../wallets/in-app/core/authentication/types.js";
|
||||
|
||||
export const LAST_AUTH_PROVIDER_STORAGE_KEY = "lastAuthProvider";
|
||||
export const PREFERRED_FIAT_PROVIDER_STORAGE_KEY = "preferredFiatProvider";
|
||||
|
||||
export async function setLastAuthProvider(
|
||||
authProvider: AuthArgsType["strategy"],
|
||||
|
||||
@@ -3,24 +3,19 @@ import { useCallback, useMemo, useState } from "react";
|
||||
import type { Chain } from "../../../../../../chains/types.js";
|
||||
import type { ThirdwebClient } from "../../../../../../client/client.js";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "../../../../../../constants/addresses.js";
|
||||
import type { GetBuyWithCryptoQuoteParams } from "../../../../../../pay/buyWithCrypto/getQuote.js";
|
||||
import type { BuyWithCryptoStatus } from "../../../../../../pay/buyWithCrypto/getStatus.js";
|
||||
import type { BuyWithFiatStatus } from "../../../../../../pay/buyWithFiat/getStatus.js";
|
||||
import { isSwapRequiredPostOnramp } from "../../../../../../pay/buyWithFiat/isSwapRequiredPostOnramp.js";
|
||||
import { formatNumber } from "../../../../../../utils/formatNumber.js";
|
||||
import type { Account } from "../../../../../../wallets/interfaces/wallet.js";
|
||||
import type { WalletId } from "../../../../../../wallets/wallet-types.js";
|
||||
import {
|
||||
type Theme,
|
||||
fontSize,
|
||||
spacing,
|
||||
} from "../../../../../core/design-system/index.js";
|
||||
import type {
|
||||
FundWalletOptions,
|
||||
PayUIOptions,
|
||||
} from "../../../../../core/hooks/connection/ConnectButtonProps.js";
|
||||
import { useWalletBalance } from "../../../../../core/hooks/others/useWalletBalance.js";
|
||||
import { useBuyWithCryptoQuote } from "../../../../../core/hooks/pay/useBuyWithCryptoQuote.js";
|
||||
import { useBuyWithFiatQuote } from "../../../../../core/hooks/pay/useBuyWithFiatQuote.js";
|
||||
import { useActiveAccount } from "../../../../../core/hooks/wallets/useActiveAccount.js";
|
||||
import { invalidateWalletBalance } from "../../../../../core/providers/invalidateWalletBalance.js";
|
||||
import type { SupportedTokens } from "../../../../../core/utils/defaultTokens.js";
|
||||
@@ -28,16 +23,10 @@ import { ErrorState } from "../../../../wallets/shared/ErrorState.js";
|
||||
import { LoadingScreen } from "../../../../wallets/shared/LoadingScreen.js";
|
||||
import type { PayEmbedConnectOptions } from "../../../PayEmbed.js";
|
||||
import { ChainName } from "../../../components/ChainName.js";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerOverlay,
|
||||
useDrawer,
|
||||
} from "../../../components/Drawer.js";
|
||||
import { Spacer } from "../../../components/Spacer.js";
|
||||
import { Spinner } from "../../../components/Spinner.js";
|
||||
import { SwitchNetworkButton } from "../../../components/SwitchNetwork.js";
|
||||
import { Container, Line, ModalHeader } from "../../../components/basic.js";
|
||||
import { Button } from "../../../components/buttons.js";
|
||||
import { Input } from "../../../components/formElements.js";
|
||||
import { Text } from "../../../components/text.js";
|
||||
import { TokenSymbol } from "../../../components/token/TokenSymbol.js";
|
||||
import { ConnectButton } from "../../ConnectButton.js";
|
||||
@@ -47,13 +36,11 @@ import { TokenSelector } from "../TokenSelector.js";
|
||||
import { WalletSwitcherConnectionScreen } from "../WalletSwitcherConnectionScreen.js";
|
||||
import { type ERC20OrNativeToken, isNativeToken } from "../nativeToken.js";
|
||||
import { DirectPaymentModeScreen } from "./DirectPaymentModeScreen.js";
|
||||
import { EstimatedTimeAndFees } from "./EstimatedTimeAndFees.js";
|
||||
import { PayTokenIcon } from "./PayTokenIcon.js";
|
||||
import { PayWithCreditCard } from "./PayWIthCreditCard.js";
|
||||
import { TransactionModeScreen } from "./TransactionModeScreen.js";
|
||||
import { CurrencySelection } from "./fiat/CurrencySelection.js";
|
||||
import { FiatFlow } from "./fiat/FiatFlow.js";
|
||||
import type { CurrencyMeta } from "./fiat/currencies.js";
|
||||
import { FiatScreenContent } from "./fiat/FiatScreenContent.js";
|
||||
import type { SelectedScreen } from "./main/types.js";
|
||||
import {
|
||||
type PaymentMethods,
|
||||
@@ -64,20 +51,16 @@ import {
|
||||
useFromTokenSelectionStates,
|
||||
useToTokenSelectionStates,
|
||||
} from "./main/useUISelectionStates.js";
|
||||
import { openOnrampPopup } from "./openOnRamppopup.js";
|
||||
import { BuyTokenInput } from "./swap/BuyTokenInput.js";
|
||||
import { FiatFees, SwapFees } from "./swap/Fees.js";
|
||||
import { PayWithCryptoQuoteInfo } from "./swap/PayWithCrypto.js";
|
||||
import { PaymentSelectionScreen } from "./swap/PaymentSelectionScreen.js";
|
||||
import { SwapFlow } from "./swap/SwapFlow.js";
|
||||
import { SwapScreenContent } from "./swap/SwapScreenContent.js";
|
||||
import { TransferFlow } from "./swap/TransferFlow.js";
|
||||
import { addPendingTx } from "./swap/pendingSwapTx.js";
|
||||
import {
|
||||
type SupportedChainAndTokens,
|
||||
useBuySupportedDestinations,
|
||||
useBuySupportedSources,
|
||||
} from "./swap/useSwapSupportedChains.js";
|
||||
import type { PayerInfo } from "./types.js";
|
||||
import { usePayerSetup } from "./usePayerSetup.js";
|
||||
|
||||
export type BuyScreenProps = {
|
||||
@@ -322,6 +305,7 @@ function BuyScreenContent(props: BuyScreenContentProps) {
|
||||
});
|
||||
}}
|
||||
onSuccess={onSwapSuccess}
|
||||
approvalAmount={screen.approvalAmount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -536,6 +520,7 @@ function BuyScreenContent(props: BuyScreenContentProps) {
|
||||
selectedChain={toChain}
|
||||
selectedToken={toToken}
|
||||
tokenAmount={tokenAmount}
|
||||
setTokenAmount={setTokenAmount}
|
||||
client={client}
|
||||
onBack={() => {
|
||||
if (
|
||||
@@ -663,8 +648,17 @@ function SelectedTokenInfo(props: {
|
||||
selectedToken: ERC20OrNativeToken;
|
||||
selectedChain: Chain;
|
||||
tokenAmount: string;
|
||||
setTokenAmount: (amount: string) => void;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const getWidth = () => {
|
||||
let chars = props.tokenAmount.replace(".", "").length;
|
||||
const hasDot = props.tokenAmount.includes(".");
|
||||
if (hasDot) {
|
||||
chars += 0.3;
|
||||
}
|
||||
return `calc(${`${Math.max(1, chars)}ch + 2px`})`;
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Container
|
||||
@@ -676,9 +670,59 @@ function SelectedTokenInfo(props: {
|
||||
}}
|
||||
>
|
||||
<Container flex="row" gap="xs" center="y">
|
||||
<Text color="primaryText" data-testid="tokenAmount" size="xl">
|
||||
{formatNumber(Number(props.tokenAmount), 6)}
|
||||
</Text>
|
||||
<Input
|
||||
variant="outline"
|
||||
pattern="^[0-9]*[.,]?[0-9]*$"
|
||||
inputMode="decimal"
|
||||
tabIndex={-1}
|
||||
placeholder="0"
|
||||
type="text"
|
||||
data-placeholder={props.tokenAmount === ""}
|
||||
value={props.tokenAmount || "0"}
|
||||
disabled={false} // TODO: add disabled freeze amount
|
||||
onClick={(e) => {
|
||||
// put cursor at the end of the input
|
||||
if (props.tokenAmount === "") {
|
||||
e.currentTarget.setSelectionRange(
|
||||
e.currentTarget.value.length,
|
||||
e.currentTarget.value.length,
|
||||
);
|
||||
}
|
||||
}}
|
||||
onChange={(e) => {
|
||||
let value = e.target.value;
|
||||
|
||||
if (value.startsWith(".")) {
|
||||
value = `0${value}`;
|
||||
}
|
||||
|
||||
if (value.length > 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
const numValue = Number(value);
|
||||
if (Number.isNaN(numValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.startsWith("0") && !value.startsWith("0.")) {
|
||||
props.setTokenAmount(value.slice(1));
|
||||
} else {
|
||||
props.setTokenAmount(value);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
border: "none",
|
||||
fontSize: fontSize.lg,
|
||||
boxShadow: "none",
|
||||
borderRadius: "0",
|
||||
padding: "0",
|
||||
paddingBlock: "2px",
|
||||
fontWeight: 600,
|
||||
textAlign: "left",
|
||||
width: getWidth(),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Container flex="row" gap="xxs" center="y">
|
||||
<TokenSymbol
|
||||
@@ -866,6 +910,7 @@ function TokenSelectedLayout(props: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
tokenAmount: string;
|
||||
setTokenAmount: (amount: string) => void;
|
||||
selectedToken: ERC20OrNativeToken;
|
||||
selectedChain: Chain;
|
||||
client: ThirdwebClient;
|
||||
@@ -888,6 +933,7 @@ function TokenSelectedLayout(props: {
|
||||
selectedToken={props.selectedToken}
|
||||
selectedChain={props.selectedChain}
|
||||
tokenAmount={props.tokenAmount}
|
||||
setTokenAmount={props.setTokenAmount}
|
||||
client={props.client}
|
||||
/>
|
||||
|
||||
@@ -904,490 +950,6 @@ function TokenSelectedLayout(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function SwapScreenContent(props: {
|
||||
setScreen: (screen: SelectedScreen) => void;
|
||||
tokenAmount: string;
|
||||
toToken: ERC20OrNativeToken;
|
||||
toChain: Chain;
|
||||
fromChain: Chain;
|
||||
fromToken: ERC20OrNativeToken;
|
||||
showFromTokenSelector: () => void;
|
||||
payer: PayerInfo;
|
||||
client: ThirdwebClient;
|
||||
payOptions: PayUIOptions;
|
||||
isEmbed: boolean;
|
||||
onDone: () => void;
|
||||
connectOptions: PayEmbedConnectOptions | undefined;
|
||||
connectLocale: ConnectLocale;
|
||||
setPayer: (payer: PayerInfo) => void;
|
||||
activeAccount: Account;
|
||||
setTokenAmount: (amount: string) => void;
|
||||
setHasEditedAmount: (hasEdited: boolean) => void;
|
||||
disableTokenSelection: boolean;
|
||||
}) {
|
||||
const {
|
||||
setScreen,
|
||||
payer,
|
||||
client,
|
||||
toChain,
|
||||
tokenAmount,
|
||||
toToken,
|
||||
fromChain,
|
||||
fromToken,
|
||||
payOptions,
|
||||
disableTokenSelection,
|
||||
} = props;
|
||||
|
||||
const defaultRecipientAddress = (
|
||||
props.payOptions as Extract<PayUIOptions, { mode: "direct_payment" }>
|
||||
)?.paymentInfo?.sellerAddress;
|
||||
const receiverAddress =
|
||||
defaultRecipientAddress || props.activeAccount.address;
|
||||
const { drawerRef, drawerOverlayRef, isOpen, setIsOpen } = useDrawer();
|
||||
const [drawerScreen, setDrawerScreen] = useState<
|
||||
"fees" | "receiver" | "payer"
|
||||
>("fees");
|
||||
|
||||
const fromTokenBalanceQuery = useWalletBalance({
|
||||
address: payer.account.address,
|
||||
chain: fromChain,
|
||||
tokenAddress: isNativeToken(fromToken) ? undefined : fromToken.address,
|
||||
client,
|
||||
});
|
||||
|
||||
const fromTokenId = isNativeToken(fromToken)
|
||||
? NATIVE_TOKEN_ADDRESS
|
||||
: fromToken.address.toLowerCase();
|
||||
const toTokenId = isNativeToken(toToken)
|
||||
? NATIVE_TOKEN_ADDRESS
|
||||
: toToken.address.toLowerCase();
|
||||
const swapRequired =
|
||||
!!tokenAmount &&
|
||||
!(fromChain.id === toChain.id && fromTokenId === toTokenId);
|
||||
const quoteParams: GetBuyWithCryptoQuoteParams | undefined = swapRequired
|
||||
? {
|
||||
// wallets
|
||||
fromAddress: payer.account.address,
|
||||
toAddress: receiverAddress,
|
||||
// from
|
||||
fromChainId: fromChain.id,
|
||||
fromTokenAddress: isNativeToken(fromToken)
|
||||
? NATIVE_TOKEN_ADDRESS
|
||||
: fromToken.address,
|
||||
// to
|
||||
toChainId: toChain.id,
|
||||
toTokenAddress: isNativeToken(toToken)
|
||||
? NATIVE_TOKEN_ADDRESS
|
||||
: toToken.address,
|
||||
toAmount: tokenAmount,
|
||||
client,
|
||||
purchaseData: payOptions.purchaseData,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const quoteQuery = useBuyWithCryptoQuote(quoteParams, {
|
||||
// refetch every 30 seconds
|
||||
staleTime: 30 * 1000,
|
||||
refetchInterval: 30 * 1000,
|
||||
gcTime: 30 * 1000,
|
||||
});
|
||||
|
||||
const sourceTokenAmount = swapRequired
|
||||
? quoteQuery.data?.swapDetails.fromAmount
|
||||
: tokenAmount;
|
||||
|
||||
const isNotEnoughBalance =
|
||||
!!sourceTokenAmount &&
|
||||
!!fromTokenBalanceQuery.data &&
|
||||
Number(fromTokenBalanceQuery.data.displayValue) < Number(sourceTokenAmount);
|
||||
|
||||
const disableContinue =
|
||||
(swapRequired && !quoteQuery.data) || isNotEnoughBalance;
|
||||
const switchChainRequired =
|
||||
props.payer.wallet.getChain()?.id !== fromChain.id;
|
||||
|
||||
const errorMsg =
|
||||
!quoteQuery.isLoading && quoteQuery.error
|
||||
? getErrorMessage(quoteQuery.error)
|
||||
: undefined;
|
||||
|
||||
function showSwapFlow() {
|
||||
if (
|
||||
(props.payOptions.mode === "direct_payment" ||
|
||||
props.payOptions.mode === "fund_wallet") &&
|
||||
!isNotEnoughBalance &&
|
||||
!swapRequired
|
||||
) {
|
||||
// same currency, just direct transfer
|
||||
setScreen({
|
||||
id: "transfer-flow",
|
||||
});
|
||||
} else if (
|
||||
props.payOptions.mode === "transaction" &&
|
||||
!isNotEnoughBalance &&
|
||||
!swapRequired
|
||||
) {
|
||||
if (payer.account.address !== receiverAddress) {
|
||||
// needs transfer from another wallet before executing the transaction
|
||||
setScreen({
|
||||
id: "transfer-flow",
|
||||
});
|
||||
} else {
|
||||
// has enough balance to just do the transaction directly
|
||||
props.onDone();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!quoteQuery.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
setScreen({
|
||||
id: "swap-flow",
|
||||
quote: quoteQuery.data,
|
||||
});
|
||||
}
|
||||
|
||||
function showFees() {
|
||||
if (!quoteQuery.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsOpen(true);
|
||||
setDrawerScreen("fees");
|
||||
}
|
||||
|
||||
return (
|
||||
<Container flex="column" gap="md" animate="fadein">
|
||||
{isOpen && (
|
||||
<>
|
||||
<DrawerOverlay ref={drawerOverlayRef} />
|
||||
<Drawer ref={drawerRef} close={() => setIsOpen(false)}>
|
||||
{drawerScreen === "fees" && quoteQuery.data && (
|
||||
<div>
|
||||
<Text size="lg" color="primaryText">
|
||||
Fees
|
||||
</Text>
|
||||
<Spacer y="lg" />
|
||||
<SwapFees quote={quoteQuery.data} />
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Quote info */}
|
||||
<div>
|
||||
<PayWithCryptoQuoteInfo
|
||||
value={sourceTokenAmount || ""}
|
||||
chain={fromChain}
|
||||
token={fromToken}
|
||||
isLoading={quoteQuery.isLoading && !sourceTokenAmount}
|
||||
client={client}
|
||||
freezeChainAndTokenSelection={disableTokenSelection}
|
||||
payerAccount={props.payer.account}
|
||||
swapRequired={swapRequired}
|
||||
/>
|
||||
{swapRequired && (
|
||||
<EstimatedTimeAndFees
|
||||
quoteIsLoading={quoteQuery.isLoading}
|
||||
estimatedSeconds={
|
||||
quoteQuery.data?.swapDetails.estimated.durationSeconds
|
||||
}
|
||||
onViewFees={showFees}
|
||||
/>
|
||||
)}
|
||||
<Spacer y="md" />
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{errorMsg && (
|
||||
<div>
|
||||
{errorMsg.data?.minimumAmountEth ? (
|
||||
<Text color="danger" size="sm" center multiline>
|
||||
Minimum amount is{" "}
|
||||
{formatNumber(Number(errorMsg.data.minimumAmountEth), 6)}{" "}
|
||||
<TokenSymbol
|
||||
token={toToken}
|
||||
chain={toChain}
|
||||
size="sm"
|
||||
inline
|
||||
color="danger"
|
||||
/>
|
||||
</Text>
|
||||
) : (
|
||||
<Text color="danger" size="sm" center multiline>
|
||||
{errorMsg.message || defaultMessage}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!errorMsg && isNotEnoughBalance && (
|
||||
<div>
|
||||
<Text color="danger" size="sm" center multiline>
|
||||
Not enough funds.
|
||||
</Text>
|
||||
<Text color="danger" size="sm" center multiline>
|
||||
Try a different wallet or token.
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Button */}
|
||||
{errorMsg?.data?.minimumAmountEth ? (
|
||||
<Button
|
||||
variant="accent"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
props.setTokenAmount(
|
||||
formatNumber(
|
||||
Number(errorMsg.data?.minimumAmountEth),
|
||||
6,
|
||||
).toString(),
|
||||
);
|
||||
props.setHasEditedAmount(true);
|
||||
}}
|
||||
>
|
||||
Set Minimum
|
||||
</Button>
|
||||
) : switchChainRequired &&
|
||||
!quoteQuery.isLoading &&
|
||||
!isNotEnoughBalance &&
|
||||
!quoteQuery.error ? (
|
||||
<SwitchNetworkButton
|
||||
variant="accent"
|
||||
fullWidth
|
||||
switchChain={async () => {
|
||||
await props.payer.wallet.switchChain(fromChain);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
variant={disableContinue ? "outline" : "accent"}
|
||||
fullWidth
|
||||
data-disabled={disableContinue}
|
||||
disabled={disableContinue}
|
||||
onClick={async () => {
|
||||
if (!disableContinue) {
|
||||
showSwapFlow();
|
||||
}
|
||||
}}
|
||||
gap="xs"
|
||||
>
|
||||
{quoteQuery.isLoading ? (
|
||||
<>
|
||||
Getting price quote
|
||||
<Spinner size="sm" color="accentText" />
|
||||
</>
|
||||
) : (
|
||||
"Continue"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function FiatScreenContent(props: {
|
||||
setScreen: (screen: SelectedScreen) => void;
|
||||
tokenAmount: string;
|
||||
toToken: ERC20OrNativeToken;
|
||||
toChain: Chain;
|
||||
selectedCurrency: CurrencyMeta;
|
||||
showCurrencySelector: () => void;
|
||||
payOptions: PayUIOptions;
|
||||
theme: "light" | "dark" | Theme;
|
||||
client: ThirdwebClient;
|
||||
onDone: () => void;
|
||||
isEmbed: boolean;
|
||||
payer: PayerInfo;
|
||||
setTokenAmount: (amount: string) => void;
|
||||
setHasEditedAmount: (hasEdited: boolean) => void;
|
||||
}) {
|
||||
const {
|
||||
toToken,
|
||||
tokenAmount,
|
||||
payer,
|
||||
client,
|
||||
setScreen,
|
||||
toChain,
|
||||
showCurrencySelector,
|
||||
selectedCurrency,
|
||||
} = props;
|
||||
const defaultRecipientAddress = (
|
||||
props.payOptions as Extract<PayUIOptions, { mode: "direct_payment" }>
|
||||
)?.paymentInfo?.sellerAddress;
|
||||
const receiverAddress =
|
||||
defaultRecipientAddress || props.payer.account.address;
|
||||
const { drawerRef, drawerOverlayRef, isOpen, setIsOpen } = useDrawer();
|
||||
const [drawerScreen, setDrawerScreen] = useState<"fees">("fees");
|
||||
|
||||
const buyWithFiatOptions = props.payOptions.buyWithFiat;
|
||||
|
||||
const fiatQuoteQuery = useBuyWithFiatQuote(
|
||||
buyWithFiatOptions !== false && tokenAmount
|
||||
? {
|
||||
fromCurrencySymbol: selectedCurrency.shorthand,
|
||||
toChainId: toChain.id,
|
||||
toAddress: receiverAddress,
|
||||
toTokenAddress: isNativeToken(toToken)
|
||||
? NATIVE_TOKEN_ADDRESS
|
||||
: toToken.address,
|
||||
toAmount: tokenAmount,
|
||||
client,
|
||||
isTestMode: buyWithFiatOptions?.testMode,
|
||||
purchaseData: props.payOptions.purchaseData,
|
||||
fromAddress: payer.account.address,
|
||||
preferredProvider: buyWithFiatOptions?.preferredProvider,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
|
||||
function handleSubmit() {
|
||||
if (!fiatQuoteQuery.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasTwoSteps = isSwapRequiredPostOnramp(fiatQuoteQuery.data);
|
||||
let openedWindow: Window | null = null;
|
||||
|
||||
if (!hasTwoSteps) {
|
||||
openedWindow = openOnrampPopup(
|
||||
fiatQuoteQuery.data.onRampLink,
|
||||
typeof props.theme === "string" ? props.theme : props.theme.type,
|
||||
);
|
||||
|
||||
addPendingTx({
|
||||
type: "fiat",
|
||||
intentId: fiatQuoteQuery.data.intentId,
|
||||
});
|
||||
}
|
||||
|
||||
setScreen({
|
||||
id: "fiat-flow",
|
||||
quote: fiatQuoteQuery.data,
|
||||
openedWindow,
|
||||
});
|
||||
}
|
||||
|
||||
function showFees() {
|
||||
if (!fiatQuoteQuery.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDrawerScreen("fees");
|
||||
setIsOpen(true);
|
||||
}
|
||||
|
||||
const disableSubmit = !fiatQuoteQuery.data;
|
||||
|
||||
const errorMsg =
|
||||
!fiatQuoteQuery.isLoading && fiatQuoteQuery.error
|
||||
? getErrorMessage(fiatQuoteQuery.error)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Container flex="column" gap="md" animate="fadein">
|
||||
{isOpen && (
|
||||
<>
|
||||
<DrawerOverlay ref={drawerOverlayRef} />
|
||||
<Drawer ref={drawerRef} close={() => setIsOpen(false)}>
|
||||
{drawerScreen === "fees" && fiatQuoteQuery.data && (
|
||||
<div>
|
||||
<Text size="lg" color="primaryText">
|
||||
Fees
|
||||
</Text>
|
||||
|
||||
<Spacer y="lg" />
|
||||
<FiatFees quote={fiatQuoteQuery.data} />
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<PayWithCreditCard
|
||||
isLoading={fiatQuoteQuery.isLoading}
|
||||
value={fiatQuoteQuery.data?.fromCurrencyWithFees.amount}
|
||||
client={client}
|
||||
currency={selectedCurrency}
|
||||
onSelectCurrency={showCurrencySelector}
|
||||
/>
|
||||
{/* Estimated time + View fees button */}
|
||||
<EstimatedTimeAndFees
|
||||
quoteIsLoading={fiatQuoteQuery.isLoading}
|
||||
estimatedSeconds={fiatQuoteQuery.data?.estimatedDurationSeconds}
|
||||
onViewFees={showFees}
|
||||
/>
|
||||
<Spacer y="md" />
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{errorMsg && (
|
||||
<div>
|
||||
{errorMsg.data?.minimumAmountEth ? (
|
||||
<Text color="danger" size="sm" center multiline>
|
||||
Minimum amount is{" "}
|
||||
{formatNumber(Number(errorMsg.data.minimumAmountEth), 6)}{" "}
|
||||
<TokenSymbol
|
||||
token={toToken}
|
||||
chain={toChain}
|
||||
size="sm"
|
||||
inline
|
||||
color="danger"
|
||||
/>
|
||||
</Text>
|
||||
) : (
|
||||
<Text color="danger" size="sm" center multiline>
|
||||
{errorMsg.message || defaultMessage}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errorMsg?.data?.minimumAmountEth ? (
|
||||
<Button
|
||||
variant="accent"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
props.setTokenAmount(
|
||||
formatNumber(
|
||||
Number(errorMsg.data?.minimumAmountEth),
|
||||
6,
|
||||
).toString(),
|
||||
);
|
||||
props.setHasEditedAmount(true);
|
||||
}}
|
||||
>
|
||||
Set Minimum
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant={disableSubmit ? "outline" : "accent"}
|
||||
data-disabled={disableSubmit}
|
||||
disabled={disableSubmit}
|
||||
fullWidth
|
||||
onClick={handleSubmit}
|
||||
gap="xs"
|
||||
>
|
||||
{fiatQuoteQuery.isLoading ? (
|
||||
<>
|
||||
Getting price quote
|
||||
<Spinner size="sm" color="accentText" />
|
||||
</>
|
||||
) : (
|
||||
"Continue"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function createSupportedTokens(
|
||||
data: SupportedChainAndTokens,
|
||||
payOptions: PayUIOptions,
|
||||
@@ -1478,26 +1040,3 @@ function ChainSelectionScreen(props: {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type ApiError = {
|
||||
code: string;
|
||||
message?: string;
|
||||
data?: {
|
||||
minimumAmountUSDCents?: string;
|
||||
requestedAmountUSDCents?: string;
|
||||
minimumAmountWei?: string;
|
||||
minimumAmountEth?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const defaultMessage = "Unable to get price quote";
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
function getErrorMessage(err: any): ApiError {
|
||||
if (typeof err.error === "object") {
|
||||
return err.error;
|
||||
}
|
||||
return {
|
||||
code: "UNABLE_TO_GET_PRICE_QUOTE",
|
||||
message: defaultMessage,
|
||||
};
|
||||
}
|
||||
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
import { ChevronDownIcon } from "@radix-ui/react-icons";
|
||||
import { useState } from "react";
|
||||
import type { Chain } from "../../../../../../../chains/types.js";
|
||||
import type { ThirdwebClient } from "../../../../../../../client/client.js";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "../../../../../../../constants/addresses.js";
|
||||
import { isSwapRequiredPostOnramp } from "../../../../../../../pay/buyWithFiat/isSwapRequiredPostOnramp.js";
|
||||
import type { FiatProvider } from "../../../../../../../pay/utils/commonTypes.js";
|
||||
import { formatNumber } from "../../../../../../../utils/formatNumber.js";
|
||||
import {
|
||||
type Theme,
|
||||
iconSize,
|
||||
spacing,
|
||||
} from "../../../../../../core/design-system/index.js";
|
||||
import type { PayUIOptions } from "../../../../../../core/hooks/connection/ConnectButtonProps.js";
|
||||
import { useBuyWithFiatQuote } from "../../../../../../core/hooks/pay/useBuyWithFiatQuote.js";
|
||||
import { PREFERRED_FIAT_PROVIDER_STORAGE_KEY } from "../../../../../../core/utils/storage.js";
|
||||
import {
|
||||
defaultMessage,
|
||||
getErrorMessage,
|
||||
} from "../../../../../utils/errors.js";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerOverlay,
|
||||
useDrawer,
|
||||
} from "../../../../components/Drawer.js";
|
||||
import { Spacer } from "../../../../components/Spacer.js";
|
||||
import { Spinner } from "../../../../components/Spinner.js";
|
||||
import { Container } from "../../../../components/basic.js";
|
||||
import { Button } from "../../../../components/buttons.js";
|
||||
import { Text } from "../../../../components/text.js";
|
||||
import { TokenSymbol } from "../../../../components/token/TokenSymbol.js";
|
||||
import { type ERC20OrNativeToken, isNativeToken } from "../../nativeToken.js";
|
||||
import { EstimatedTimeAndFees } from "../EstimatedTimeAndFees.js";
|
||||
import { PayWithCreditCard } from "../PayWIthCreditCard.js";
|
||||
import type { SelectedScreen } from "../main/types.js";
|
||||
import { openOnrampPopup } from "../openOnRamppopup.js";
|
||||
import { FiatFees } from "../swap/Fees.js";
|
||||
import { addPendingTx } from "../swap/pendingSwapTx.js";
|
||||
import type { PayerInfo } from "../types.js";
|
||||
import { Providers } from "./Providers.js";
|
||||
import type { CurrencyMeta } from "./currencies.js";
|
||||
|
||||
export function FiatScreenContent(props: {
|
||||
setScreen: (screen: SelectedScreen) => void;
|
||||
tokenAmount: string;
|
||||
toToken: ERC20OrNativeToken;
|
||||
toChain: Chain;
|
||||
selectedCurrency: CurrencyMeta;
|
||||
showCurrencySelector: () => void;
|
||||
payOptions: PayUIOptions;
|
||||
theme: "light" | "dark" | Theme;
|
||||
client: ThirdwebClient;
|
||||
onDone: () => void;
|
||||
isEmbed: boolean;
|
||||
payer: PayerInfo;
|
||||
setTokenAmount: (amount: string) => void;
|
||||
setHasEditedAmount: (hasEdited: boolean) => void;
|
||||
}) {
|
||||
const {
|
||||
toToken,
|
||||
tokenAmount,
|
||||
payer,
|
||||
client,
|
||||
setScreen,
|
||||
toChain,
|
||||
showCurrencySelector,
|
||||
selectedCurrency,
|
||||
} = props;
|
||||
const defaultRecipientAddress = (
|
||||
props.payOptions as Extract<PayUIOptions, { mode: "direct_payment" }>
|
||||
)?.paymentInfo?.sellerAddress;
|
||||
const receiverAddress =
|
||||
defaultRecipientAddress || props.payer.account.address;
|
||||
const { drawerRef, drawerOverlayRef, isOpen, setIsOpen } = useDrawer();
|
||||
const [drawerScreen, setDrawerScreen] = useState<"fees" | "providers">(
|
||||
"fees",
|
||||
);
|
||||
|
||||
const buyWithFiatOptions = props.payOptions.buyWithFiat;
|
||||
const [preferredProvider, setPreferredProvider] = useState<
|
||||
FiatProvider | undefined
|
||||
>(
|
||||
buyWithFiatOptions !== false
|
||||
? buyWithFiatOptions?.preferredProvider ||
|
||||
((localStorage.getItem(
|
||||
PREFERRED_FIAT_PROVIDER_STORAGE_KEY,
|
||||
) as FiatProvider | null) ??
|
||||
undefined)
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const fiatQuoteQuery = useBuyWithFiatQuote(
|
||||
buyWithFiatOptions !== false && tokenAmount
|
||||
? {
|
||||
fromCurrencySymbol: selectedCurrency.shorthand,
|
||||
toChainId: toChain.id,
|
||||
toAddress: receiverAddress,
|
||||
toTokenAddress: isNativeToken(toToken)
|
||||
? NATIVE_TOKEN_ADDRESS
|
||||
: toToken.address,
|
||||
toAmount: tokenAmount,
|
||||
client,
|
||||
isTestMode: buyWithFiatOptions?.testMode,
|
||||
purchaseData: props.payOptions.purchaseData,
|
||||
fromAddress: payer.account.address,
|
||||
preferredProvider: preferredProvider,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
|
||||
function handleSubmit() {
|
||||
if (!fiatQuoteQuery.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasTwoSteps = isSwapRequiredPostOnramp(fiatQuoteQuery.data);
|
||||
let openedWindow: Window | null = null;
|
||||
|
||||
if (!hasTwoSteps) {
|
||||
openedWindow = openOnrampPopup(
|
||||
fiatQuoteQuery.data.onRampLink,
|
||||
typeof props.theme === "string" ? props.theme : props.theme.type,
|
||||
);
|
||||
|
||||
addPendingTx({
|
||||
type: "fiat",
|
||||
intentId: fiatQuoteQuery.data.intentId,
|
||||
});
|
||||
}
|
||||
|
||||
setScreen({
|
||||
id: "fiat-flow",
|
||||
quote: fiatQuoteQuery.data,
|
||||
openedWindow,
|
||||
});
|
||||
}
|
||||
|
||||
function showFees() {
|
||||
if (!fiatQuoteQuery.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDrawerScreen("fees");
|
||||
setIsOpen(true);
|
||||
}
|
||||
|
||||
function showProviders() {
|
||||
setDrawerScreen("providers");
|
||||
setIsOpen(true);
|
||||
}
|
||||
|
||||
const disableSubmit = !fiatQuoteQuery.data;
|
||||
|
||||
const errorMsg =
|
||||
!fiatQuoteQuery.isLoading && fiatQuoteQuery.error
|
||||
? getErrorMessage(fiatQuoteQuery.error)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Container flex="column" gap="md" animate="fadein">
|
||||
{isOpen && (
|
||||
<>
|
||||
<DrawerOverlay ref={drawerOverlayRef} />
|
||||
<Drawer ref={drawerRef} close={() => setIsOpen(false)}>
|
||||
{drawerScreen === "fees" && fiatQuoteQuery.data && (
|
||||
<div>
|
||||
<Text size="lg" color="primaryText">
|
||||
Fees
|
||||
</Text>
|
||||
|
||||
<Spacer y="lg" />
|
||||
<FiatFees quote={fiatQuoteQuery.data} />
|
||||
</div>
|
||||
)}
|
||||
{drawerScreen === "providers" && (
|
||||
<div>
|
||||
<Text size="lg" color="primaryText">
|
||||
Providers
|
||||
</Text>
|
||||
<Spacer y="lg" />
|
||||
<Providers
|
||||
preferredProvider={
|
||||
preferredProvider || fiatQuoteQuery.data?.provider
|
||||
}
|
||||
onSelect={(provider) => {
|
||||
setPreferredProvider(provider);
|
||||
// save the pref in local storage
|
||||
localStorage.setItem(
|
||||
PREFERRED_FIAT_PROVIDER_STORAGE_KEY,
|
||||
provider,
|
||||
);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<PayWithCreditCard
|
||||
isLoading={fiatQuoteQuery.isLoading}
|
||||
value={fiatQuoteQuery.data?.fromCurrencyWithFees.amount}
|
||||
client={client}
|
||||
currency={selectedCurrency}
|
||||
onSelectCurrency={showCurrencySelector}
|
||||
/>
|
||||
<Container
|
||||
bg="tertiaryBg"
|
||||
flex="row"
|
||||
borderColor="borderColor"
|
||||
style={{
|
||||
paddingLeft: spacing.md,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
borderWidth: "1px",
|
||||
borderStyle: "solid",
|
||||
borderBottom: "none",
|
||||
}}
|
||||
>
|
||||
<Text size="xs" color="secondaryText">
|
||||
Provider
|
||||
</Text>
|
||||
<Button variant="ghost" onClick={showProviders}>
|
||||
<Container flex="row" center="y" gap="xxs" color="secondaryText">
|
||||
<Text size="xs">
|
||||
{preferredProvider
|
||||
? `${preferredProvider.charAt(0).toUpperCase() + preferredProvider.slice(1).toLowerCase()}`
|
||||
: fiatQuoteQuery.data?.provider
|
||||
? `${fiatQuoteQuery.data?.provider.charAt(0).toUpperCase() + fiatQuoteQuery.data?.provider.slice(1).toLowerCase()}`
|
||||
: ""}
|
||||
</Text>
|
||||
<ChevronDownIcon width={iconSize.sm} height={iconSize.sm} />
|
||||
</Container>
|
||||
</Button>
|
||||
</Container>
|
||||
{/* Estimated time + View fees button */}
|
||||
<EstimatedTimeAndFees
|
||||
quoteIsLoading={fiatQuoteQuery.isLoading}
|
||||
estimatedSeconds={fiatQuoteQuery.data?.estimatedDurationSeconds}
|
||||
onViewFees={showFees}
|
||||
/>
|
||||
<Spacer y="md" />
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{errorMsg && (
|
||||
<div>
|
||||
{errorMsg.data?.minimumAmountEth ? (
|
||||
<Text color="danger" size="sm" center multiline>
|
||||
Minimum amount is{" "}
|
||||
{formatNumber(Number(errorMsg.data.minimumAmountEth), 6)}{" "}
|
||||
<TokenSymbol
|
||||
token={toToken}
|
||||
chain={toChain}
|
||||
size="sm"
|
||||
inline
|
||||
color="danger"
|
||||
/>
|
||||
</Text>
|
||||
) : (
|
||||
<Text color="danger" size="sm" center multiline>
|
||||
{errorMsg.message || defaultMessage}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errorMsg?.data?.minimumAmountEth ? (
|
||||
<Button
|
||||
variant="accent"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
props.setTokenAmount(
|
||||
formatNumber(
|
||||
Number(errorMsg.data?.minimumAmountEth),
|
||||
6,
|
||||
).toString(),
|
||||
);
|
||||
props.setHasEditedAmount(true);
|
||||
}}
|
||||
>
|
||||
Set Minimum
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant={disableSubmit ? "outline" : "accent"}
|
||||
data-disabled={disableSubmit}
|
||||
disabled={disableSubmit}
|
||||
fullWidth
|
||||
onClick={handleSubmit}
|
||||
gap="xs"
|
||||
>
|
||||
{fiatQuoteQuery.isLoading ? (
|
||||
<>
|
||||
Getting price quote
|
||||
<Spinner size="sm" color="accentText" />
|
||||
</>
|
||||
) : (
|
||||
"Continue"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
+33
-1
@@ -1,6 +1,9 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getCachedChain } from "../../../../../../../chains/utils.js";
|
||||
import type { ThirdwebClient } from "../../../../../../../client/client.js";
|
||||
import { getContract } from "../../../../../../../contract/contract.js";
|
||||
import { allowance } from "../../../../../../../extensions/erc20/__generated__/IERC20/read/allowance.js";
|
||||
import type { BuyWithCryptoQuote } from "../../../../../../../pay/buyWithCrypto/getQuote.js";
|
||||
import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js";
|
||||
import { getPostOnRampQuote } from "../../../../../../../pay/buyWithFiat/getPostOnRampQuote.js";
|
||||
@@ -43,11 +46,38 @@ export function PostOnRampSwap(props: {
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const allowanceQuery = useQuery({
|
||||
queryKey: [
|
||||
"allowance",
|
||||
props.payer.account.address,
|
||||
postOnRampQuoteQuery.data?.approvalData,
|
||||
],
|
||||
queryFn: () => {
|
||||
if (!postOnRampQuoteQuery.data?.approvalData) {
|
||||
return null;
|
||||
}
|
||||
return allowance({
|
||||
contract: getContract({
|
||||
client: props.client,
|
||||
address: postOnRampQuoteQuery.data.swapDetails.fromToken.tokenAddress,
|
||||
chain: getCachedChain(
|
||||
postOnRampQuoteQuery.data.swapDetails.fromToken.chainId,
|
||||
),
|
||||
}),
|
||||
spender: postOnRampQuoteQuery.data.approvalData.spenderAddress,
|
||||
owner: props.payer.account.address,
|
||||
});
|
||||
},
|
||||
enabled: !!postOnRampQuoteQuery.data?.approvalData,
|
||||
refetchOnMount: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
postOnRampQuoteQuery.data &&
|
||||
!lockedOnRampQuote &&
|
||||
!postOnRampQuoteQuery.isRefetching
|
||||
!postOnRampQuoteQuery.isRefetching &&
|
||||
!allowanceQuery.isLoading
|
||||
) {
|
||||
setLockedOnRampQuote(postOnRampQuoteQuery.data);
|
||||
}
|
||||
@@ -55,6 +85,7 @@ export function PostOnRampSwap(props: {
|
||||
postOnRampQuoteQuery.data,
|
||||
lockedOnRampQuote,
|
||||
postOnRampQuoteQuery.isRefetching,
|
||||
allowanceQuery.isLoading,
|
||||
]);
|
||||
|
||||
if (postOnRampQuoteQuery.isError) {
|
||||
@@ -133,6 +164,7 @@ export function PostOnRampSwap(props: {
|
||||
transactionMode={props.transactionMode}
|
||||
isEmbed={props.isEmbed}
|
||||
onSuccess={props.onSuccess}
|
||||
approvalAmount={allowanceQuery.data ?? undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
type FiatProvider,
|
||||
FiatProviders,
|
||||
} from "../../../../../../../pay/utils/commonTypes.js";
|
||||
import { Container } from "../../../../components/basic.js";
|
||||
import { Button } from "../../../../components/buttons.js";
|
||||
import { Link } from "../../../../components/text.js";
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function Providers(props: {
|
||||
preferredProvider?: FiatProvider;
|
||||
onSelect: (provider: FiatProvider) => void;
|
||||
}) {
|
||||
return (
|
||||
<Container
|
||||
expand
|
||||
flex="column"
|
||||
gap="sm"
|
||||
style={{
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
{FiatProviders.map((provider) => {
|
||||
return (
|
||||
<Container
|
||||
key={provider}
|
||||
flex="row"
|
||||
expand
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={() => props.onSelect(provider)}
|
||||
variant={"link"}
|
||||
>
|
||||
<Link
|
||||
color={
|
||||
props.preferredProvider === provider
|
||||
? "primaryText"
|
||||
: "secondaryText"
|
||||
}
|
||||
size="sm"
|
||||
hoverColor="primaryText"
|
||||
>
|
||||
{provider.charAt(0).toUpperCase() +
|
||||
provider.slice(1).toLowerCase()}
|
||||
</Link>
|
||||
</Button>
|
||||
</Container>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,7 @@ export type SelectedScreen =
|
||||
| {
|
||||
id: "swap-flow";
|
||||
quote: BuyWithCryptoQuote;
|
||||
approvalAmount?: bigint;
|
||||
}
|
||||
| {
|
||||
id: "fiat-flow";
|
||||
|
||||
+12
-2
@@ -19,6 +19,7 @@ import { Text } from "../../../../components/text.js";
|
||||
import { TokenSymbol } from "../../../../components/token/TokenSymbol.js";
|
||||
import type { ERC20OrNativeToken } from "../../nativeToken.js";
|
||||
import { getBuyTokenAmountFontSize } from "../utils.js";
|
||||
import { FiatValue } from "./FiatValue.js";
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -35,7 +36,6 @@ export function BuyTokenInput(props: {
|
||||
freezeChainAndToken?: boolean;
|
||||
}) {
|
||||
const { name } = useChainName(props.chain);
|
||||
|
||||
const getWidth = () => {
|
||||
let chars = props.value.replace(".", "").length;
|
||||
const hasDot = props.value.includes(".");
|
||||
@@ -122,9 +122,19 @@ export function BuyTokenInput(props: {
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
<Container flex="row" center="both">
|
||||
<FiatValue
|
||||
tokenAmount={props.value}
|
||||
token={props.token}
|
||||
chain={props.chain}
|
||||
client={props.client}
|
||||
size="md"
|
||||
/>
|
||||
</Container>
|
||||
|
||||
{!props.hideTokenSelector && (
|
||||
<>
|
||||
<Spacer y="sm" />
|
||||
<Spacer y="md" />
|
||||
|
||||
{/* Token / Chain selector */}
|
||||
<Container flex="row" center="x">
|
||||
|
||||
+21
-6
@@ -3,6 +3,8 @@ import { useState } from "react";
|
||||
import { trackPayEvent } from "../../../../../../../analytics/track/pay.js";
|
||||
import type { Chain } from "../../../../../../../chains/types.js";
|
||||
import type { ThirdwebClient } from "../../../../../../../client/client.js";
|
||||
import { getContract } from "../../../../../../../contract/contract.js";
|
||||
import { approve } from "../../../../../../../extensions/erc20/write/approve.js";
|
||||
import type { BuyWithCryptoQuote } from "../../../../../../../pay/buyWithCrypto/getQuote.js";
|
||||
import { sendTransaction } from "../../../../../../../transaction/actions/send-transaction.js";
|
||||
import { waitForReceipt } from "../../../../../../../transaction/actions/wait-for-tx-receipt.js";
|
||||
@@ -51,9 +53,13 @@ export function SwapConfirmationScreen(props: {
|
||||
fromTokenSymbol: string;
|
||||
isFiatFlow: boolean;
|
||||
payer: PayerInfo;
|
||||
preApprovedAmount?: bigint;
|
||||
}) {
|
||||
const isApprovalRequired = props.quote.approval !== undefined;
|
||||
const initialStep = isApprovalRequired ? "approval" : "swap";
|
||||
const needsApproval =
|
||||
props.quote.approvalData &&
|
||||
props.preApprovedAmount !== undefined &&
|
||||
props.preApprovedAmount < BigInt(props.quote.approvalData.amountWei);
|
||||
const initialStep = needsApproval ? "approval" : "swap";
|
||||
|
||||
const [step, setStep] = useState<"approval" | "swap">(initialStep);
|
||||
const [status, setStatus] = useState<
|
||||
@@ -136,7 +142,7 @@ export function SwapConfirmationScreen(props: {
|
||||
<Spacer y="xl" />
|
||||
|
||||
{/* Show 2 steps - Approve and confirm */}
|
||||
{isApprovalRequired && (
|
||||
{needsApproval && (
|
||||
<>
|
||||
<Spacer y="sm" />
|
||||
<Container
|
||||
@@ -187,7 +193,7 @@ export function SwapConfirmationScreen(props: {
|
||||
fullWidth
|
||||
disabled={status === "pending"}
|
||||
onClick={async () => {
|
||||
if (step === "approval" && props.quote.approval) {
|
||||
if (step === "approval" && props.quote.approvalData) {
|
||||
try {
|
||||
setStatus("pending");
|
||||
|
||||
@@ -204,13 +210,22 @@ export function SwapConfirmationScreen(props: {
|
||||
dstChainId: props.quote.swapDetails.toToken.chainId,
|
||||
});
|
||||
|
||||
const transaction = approve({
|
||||
contract: getContract({
|
||||
client: props.client,
|
||||
address: props.quote.swapDetails.fromToken.tokenAddress,
|
||||
chain: props.fromChain,
|
||||
}),
|
||||
spender: props.quote.approvalData.spenderAddress,
|
||||
amountWei: BigInt(props.quote.approvalData.amountWei),
|
||||
});
|
||||
|
||||
const tx = await sendTransaction({
|
||||
account: props.payer.account,
|
||||
transaction: props.quote.approval,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await waitForReceipt({ ...tx, maxBlocksWaitTime: 50 });
|
||||
// props.onQuoteFinalized(props.quote);
|
||||
|
||||
trackPayEvent({
|
||||
event: "swap_approval_success",
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { Chain } from "../../../../../../../chains/types.js";
|
||||
import type { ThirdwebClient } from "../../../../../../../client/client.js";
|
||||
import { convertCryptoToFiat } from "../../../../../../../pay/convert/cryptoToFiat.js";
|
||||
import { formatNumber } from "../../../../../../../utils/formatNumber.js";
|
||||
import { fontSize } from "../../../../../../core/design-system/index.js";
|
||||
import { Skeleton } from "../../../../components/Skeleton.js";
|
||||
import { Text } from "../../../../components/text.js";
|
||||
import type { TextProps } from "../../../../components/text.js";
|
||||
import { useDebouncedValue } from "../../../../hooks/useDebouncedValue.js";
|
||||
import type { ERC20OrNativeToken } from "../../nativeToken.js";
|
||||
import { getTokenAddress } from "../../nativeToken.js";
|
||||
|
||||
export function FiatValue(
|
||||
props: {
|
||||
tokenAmount: string;
|
||||
token: ERC20OrNativeToken;
|
||||
chain: Chain;
|
||||
client: ThirdwebClient;
|
||||
} & TextProps,
|
||||
) {
|
||||
const deferredTokenAmount = useDebouncedValue(props.tokenAmount, 500);
|
||||
const cryptoToFiatQuery = useQuery({
|
||||
queryKey: [
|
||||
"cryptoToFiat",
|
||||
props.chain.id,
|
||||
getTokenAddress(props.token),
|
||||
deferredTokenAmount,
|
||||
],
|
||||
queryFn: () =>
|
||||
convertCryptoToFiat({
|
||||
client: props.client,
|
||||
chain: props.chain,
|
||||
fromTokenAddress: getTokenAddress(props.token),
|
||||
fromAmount: Number(deferredTokenAmount),
|
||||
to: "USD",
|
||||
}),
|
||||
});
|
||||
|
||||
if (cryptoToFiatQuery.isLoading) {
|
||||
return <Skeleton width={"50px"} height={fontSize.lg} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Text {...props}>
|
||||
{cryptoToFiatQuery.data?.result
|
||||
? `$${formatNumber(cryptoToFiatQuery.data.result, 2)}`
|
||||
: "$0.00"}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ type SwapFlowProps = {
|
||||
transactionMode: boolean;
|
||||
isEmbed: boolean;
|
||||
onSuccess: ((status: BuyWithCryptoStatus) => void) | undefined;
|
||||
approvalAmount?: bigint;
|
||||
};
|
||||
|
||||
export function SwapFlow(props: SwapFlowProps) {
|
||||
@@ -109,6 +110,7 @@ export function SwapFlow(props: SwapFlowProps) {
|
||||
quote={quote}
|
||||
isFiatFlow={props.isFiatFlow}
|
||||
payer={props.payer}
|
||||
preApprovedAmount={props.approvalAmount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+353
@@ -0,0 +1,353 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { 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 { getContract } from "../../../../../../../contract/contract.js";
|
||||
import { allowance } from "../../../../../../../extensions/erc20/__generated__/IERC20/read/allowance.js";
|
||||
import type { GetBuyWithCryptoQuoteParams } from "../../../../../../../pay/buyWithCrypto/getQuote.js";
|
||||
import { formatNumber } from "../../../../../../../utils/formatNumber.js";
|
||||
import type { Account } from "../../../../../../../wallets/interfaces/wallet.js";
|
||||
import type { PayUIOptions } from "../../../../../../core/hooks/connection/ConnectButtonProps.js";
|
||||
import { useWalletBalance } from "../../../../../../core/hooks/others/useWalletBalance.js";
|
||||
import { useBuyWithCryptoQuote } from "../../../../../../core/hooks/pay/useBuyWithCryptoQuote.js";
|
||||
import {
|
||||
defaultMessage,
|
||||
getErrorMessage,
|
||||
} from "../../../../../utils/errors.js";
|
||||
import type { PayEmbedConnectOptions } from "../../../../PayEmbed.js";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerOverlay,
|
||||
useDrawer,
|
||||
} from "../../../../components/Drawer.js";
|
||||
import { Spacer } from "../../../../components/Spacer.js";
|
||||
import { Spinner } from "../../../../components/Spinner.js";
|
||||
import { SwitchNetworkButton } from "../../../../components/SwitchNetwork.js";
|
||||
import { Container } from "../../../../components/basic.js";
|
||||
import { Button } from "../../../../components/buttons.js";
|
||||
import { Text } from "../../../../components/text.js";
|
||||
import { TokenSymbol } from "../../../../components/token/TokenSymbol.js";
|
||||
import type { ConnectLocale } from "../../../locale/types.js";
|
||||
import { type ERC20OrNativeToken, isNativeToken } from "../../nativeToken.js";
|
||||
import { EstimatedTimeAndFees } from "../EstimatedTimeAndFees.js";
|
||||
import type { SelectedScreen } from "../main/types.js";
|
||||
import type { PayerInfo } from "../types.js";
|
||||
import { SwapFees } from "./Fees.js";
|
||||
import { PayWithCryptoQuoteInfo } from "./PayWithCrypto.js";
|
||||
|
||||
export function SwapScreenContent(props: {
|
||||
setScreen: (screen: SelectedScreen) => void;
|
||||
tokenAmount: string;
|
||||
toToken: ERC20OrNativeToken;
|
||||
toChain: Chain;
|
||||
fromChain: Chain;
|
||||
fromToken: ERC20OrNativeToken;
|
||||
showFromTokenSelector: () => void;
|
||||
payer: PayerInfo;
|
||||
client: ThirdwebClient;
|
||||
payOptions: PayUIOptions;
|
||||
isEmbed: boolean;
|
||||
onDone: () => void;
|
||||
connectOptions: PayEmbedConnectOptions | undefined;
|
||||
connectLocale: ConnectLocale;
|
||||
setPayer: (payer: PayerInfo) => void;
|
||||
activeAccount: Account;
|
||||
setTokenAmount: (amount: string) => void;
|
||||
setHasEditedAmount: (hasEdited: boolean) => void;
|
||||
disableTokenSelection: boolean;
|
||||
}) {
|
||||
const {
|
||||
setScreen,
|
||||
payer,
|
||||
client,
|
||||
toChain,
|
||||
tokenAmount,
|
||||
toToken,
|
||||
fromChain,
|
||||
fromToken,
|
||||
payOptions,
|
||||
disableTokenSelection,
|
||||
} = props;
|
||||
|
||||
const defaultRecipientAddress = (
|
||||
props.payOptions as Extract<PayUIOptions, { mode: "direct_payment" }>
|
||||
)?.paymentInfo?.sellerAddress;
|
||||
const receiverAddress =
|
||||
defaultRecipientAddress || props.activeAccount.address;
|
||||
const { drawerRef, drawerOverlayRef, isOpen, setIsOpen } = useDrawer();
|
||||
const [drawerScreen, setDrawerScreen] = useState<
|
||||
"fees" | "receiver" | "payer"
|
||||
>("fees");
|
||||
|
||||
const fromTokenBalanceQuery = useWalletBalance({
|
||||
address: payer.account.address,
|
||||
chain: fromChain,
|
||||
tokenAddress: isNativeToken(fromToken) ? undefined : fromToken.address,
|
||||
client,
|
||||
});
|
||||
|
||||
const fromTokenId = isNativeToken(fromToken)
|
||||
? NATIVE_TOKEN_ADDRESS
|
||||
: fromToken.address.toLowerCase();
|
||||
const toTokenId = isNativeToken(toToken)
|
||||
? NATIVE_TOKEN_ADDRESS
|
||||
: toToken.address.toLowerCase();
|
||||
const swapRequired =
|
||||
!!tokenAmount &&
|
||||
!(fromChain.id === toChain.id && fromTokenId === toTokenId);
|
||||
const quoteParams: GetBuyWithCryptoQuoteParams | undefined = swapRequired
|
||||
? {
|
||||
// wallets
|
||||
fromAddress: payer.account.address,
|
||||
toAddress: receiverAddress,
|
||||
// from
|
||||
fromChainId: fromChain.id,
|
||||
fromTokenAddress: isNativeToken(fromToken)
|
||||
? NATIVE_TOKEN_ADDRESS
|
||||
: fromToken.address,
|
||||
// to
|
||||
toChainId: toChain.id,
|
||||
toTokenAddress: isNativeToken(toToken)
|
||||
? NATIVE_TOKEN_ADDRESS
|
||||
: toToken.address,
|
||||
toAmount: tokenAmount,
|
||||
client,
|
||||
purchaseData: payOptions.purchaseData,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const quoteQuery = useBuyWithCryptoQuote(quoteParams, {
|
||||
// refetch every 30 seconds
|
||||
staleTime: 30 * 1000,
|
||||
refetchInterval: 30 * 1000,
|
||||
gcTime: 30 * 1000,
|
||||
});
|
||||
|
||||
const allowanceQuery = useQuery({
|
||||
queryKey: [
|
||||
"allowance",
|
||||
payer.account.address,
|
||||
quoteQuery.data?.approvalData,
|
||||
],
|
||||
queryFn: () => {
|
||||
if (!quoteQuery.data?.approvalData) {
|
||||
return null;
|
||||
}
|
||||
return allowance({
|
||||
contract: getContract({
|
||||
client: props.client,
|
||||
address: quoteQuery.data.swapDetails.fromToken.tokenAddress,
|
||||
chain: getCachedChain(quoteQuery.data.swapDetails.fromToken.chainId),
|
||||
}),
|
||||
spender: quoteQuery.data.approvalData.spenderAddress,
|
||||
owner: props.payer.account.address,
|
||||
});
|
||||
},
|
||||
enabled: !!quoteQuery.data?.approvalData,
|
||||
refetchOnMount: true,
|
||||
});
|
||||
|
||||
const sourceTokenAmount = swapRequired
|
||||
? quoteQuery.data?.swapDetails.fromAmount
|
||||
: tokenAmount;
|
||||
|
||||
const isNotEnoughBalance =
|
||||
!!sourceTokenAmount &&
|
||||
!!fromTokenBalanceQuery.data &&
|
||||
Number(fromTokenBalanceQuery.data.displayValue) < Number(sourceTokenAmount);
|
||||
|
||||
const disableContinue =
|
||||
(swapRequired && !quoteQuery.data) ||
|
||||
isNotEnoughBalance ||
|
||||
allowanceQuery.isLoading;
|
||||
const switchChainRequired =
|
||||
props.payer.wallet.getChain()?.id !== fromChain.id;
|
||||
|
||||
const errorMsg =
|
||||
!quoteQuery.isLoading && quoteQuery.error
|
||||
? getErrorMessage(quoteQuery.error)
|
||||
: undefined;
|
||||
|
||||
function showSwapFlow() {
|
||||
if (
|
||||
(props.payOptions.mode === "direct_payment" ||
|
||||
props.payOptions.mode === "fund_wallet") &&
|
||||
!isNotEnoughBalance &&
|
||||
!swapRequired
|
||||
) {
|
||||
// same currency, just direct transfer
|
||||
setScreen({
|
||||
id: "transfer-flow",
|
||||
});
|
||||
} else if (
|
||||
props.payOptions.mode === "transaction" &&
|
||||
!isNotEnoughBalance &&
|
||||
!swapRequired
|
||||
) {
|
||||
if (payer.account.address !== receiverAddress) {
|
||||
// needs transfer from another wallet before executing the transaction
|
||||
setScreen({
|
||||
id: "transfer-flow",
|
||||
});
|
||||
} else {
|
||||
// has enough balance to just do the transaction directly
|
||||
props.onDone();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!quoteQuery.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
setScreen({
|
||||
id: "swap-flow",
|
||||
quote: quoteQuery.data,
|
||||
approvalAmount: allowanceQuery.data ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function showFees() {
|
||||
if (!quoteQuery.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsOpen(true);
|
||||
setDrawerScreen("fees");
|
||||
}
|
||||
|
||||
return (
|
||||
<Container flex="column" gap="md" animate="fadein">
|
||||
{isOpen && (
|
||||
<>
|
||||
<DrawerOverlay ref={drawerOverlayRef} />
|
||||
<Drawer ref={drawerRef} close={() => setIsOpen(false)}>
|
||||
{drawerScreen === "fees" && quoteQuery.data && (
|
||||
<div>
|
||||
<Text size="lg" color="primaryText">
|
||||
Fees
|
||||
</Text>
|
||||
<Spacer y="lg" />
|
||||
<SwapFees quote={quoteQuery.data} />
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Quote info */}
|
||||
<div>
|
||||
<PayWithCryptoQuoteInfo
|
||||
value={sourceTokenAmount || ""}
|
||||
chain={fromChain}
|
||||
token={fromToken}
|
||||
isLoading={quoteQuery.isLoading && !sourceTokenAmount}
|
||||
client={client}
|
||||
freezeChainAndTokenSelection={disableTokenSelection}
|
||||
payerAccount={props.payer.account}
|
||||
swapRequired={swapRequired}
|
||||
/>
|
||||
{swapRequired && (
|
||||
<EstimatedTimeAndFees
|
||||
quoteIsLoading={quoteQuery.isLoading}
|
||||
estimatedSeconds={
|
||||
quoteQuery.data?.swapDetails.estimated.durationSeconds
|
||||
}
|
||||
onViewFees={showFees}
|
||||
/>
|
||||
)}
|
||||
<Spacer y="md" />
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{errorMsg && (
|
||||
<div>
|
||||
{errorMsg.data?.minimumAmountEth ? (
|
||||
<Text color="danger" size="sm" center multiline>
|
||||
Minimum amount is{" "}
|
||||
{formatNumber(Number(errorMsg.data.minimumAmountEth), 6)}{" "}
|
||||
<TokenSymbol
|
||||
token={toToken}
|
||||
chain={toChain}
|
||||
size="sm"
|
||||
inline
|
||||
color="danger"
|
||||
/>
|
||||
</Text>
|
||||
) : (
|
||||
<Text color="danger" size="sm" center multiline>
|
||||
{errorMsg.message || defaultMessage}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!errorMsg && isNotEnoughBalance && (
|
||||
<div>
|
||||
<Text color="danger" size="sm" center multiline>
|
||||
Not enough funds.
|
||||
</Text>
|
||||
<Text color="danger" size="sm" center multiline>
|
||||
Try a different wallet or token.
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Button */}
|
||||
{errorMsg?.data?.minimumAmountEth ? (
|
||||
<Button
|
||||
variant="accent"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
props.setTokenAmount(
|
||||
formatNumber(
|
||||
Number(errorMsg.data?.minimumAmountEth),
|
||||
6,
|
||||
).toString(),
|
||||
);
|
||||
props.setHasEditedAmount(true);
|
||||
}}
|
||||
>
|
||||
Set Minimum
|
||||
</Button>
|
||||
) : switchChainRequired &&
|
||||
!quoteQuery.isLoading &&
|
||||
!allowanceQuery.isLoading &&
|
||||
!isNotEnoughBalance &&
|
||||
!quoteQuery.error ? (
|
||||
<SwitchNetworkButton
|
||||
variant="accent"
|
||||
fullWidth
|
||||
switchChain={async () => {
|
||||
await props.payer.wallet.switchChain(fromChain);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
variant={disableContinue ? "outline" : "accent"}
|
||||
fullWidth
|
||||
data-disabled={disableContinue}
|
||||
disabled={disableContinue}
|
||||
onClick={async () => {
|
||||
if (!disableContinue) {
|
||||
showSwapFlow();
|
||||
}
|
||||
}}
|
||||
gap="xs"
|
||||
>
|
||||
{quoteQuery.isLoading ? (
|
||||
<>
|
||||
Getting price quote
|
||||
<Spinner size="sm" color="accentText" />
|
||||
</>
|
||||
) : (
|
||||
"Continue"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
+43
-6
@@ -1,14 +1,18 @@
|
||||
import { CheckCircledIcon } from "@radix-ui/react-icons";
|
||||
import { 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 { getContract } from "../../../../../../../contract/contract.js";
|
||||
import { allowance } from "../../../../../../../extensions/erc20/__generated__/IERC20/read/allowance.js";
|
||||
import { approve } from "../../../../../../../extensions/erc20/write/approve.js";
|
||||
import { transfer } from "../../../../../../../extensions/erc20/write/transfer.js";
|
||||
import { getBuyWithCryptoTransfer } from "../../../../../../../pay/buyWithCrypto/getTransfer.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 type { Address } from "../../../../../../../utils/address.js";
|
||||
import { toWei } from "../../../../../../../utils/units.js";
|
||||
import { iconSize } from "../../../../../../core/design-system/index.js";
|
||||
import type { PayUIOptions } from "../../../../../../core/hooks/connection/ConnectButtonProps.js";
|
||||
@@ -248,13 +252,46 @@ export function TransferConfirmationScreen(
|
||||
purchaseData: payOptions?.purchaseData,
|
||||
});
|
||||
|
||||
if (transferResponse.approval) {
|
||||
setStep("approve");
|
||||
// approve the transfer
|
||||
await sendAndConfirmTransaction({
|
||||
account: props.payer.account,
|
||||
transaction: transferResponse.approval,
|
||||
if (transferResponse.approvalData) {
|
||||
// check allowance
|
||||
const prevAllowance = await allowance({
|
||||
contract: getContract({
|
||||
client: client,
|
||||
address: transferResponse.approvalData.tokenAddress,
|
||||
chain: getCachedChain(
|
||||
transferResponse.approvalData.chainId,
|
||||
),
|
||||
}),
|
||||
spender: transferResponse.approvalData
|
||||
.spenderAddress as Address,
|
||||
owner: payer.account.address,
|
||||
});
|
||||
|
||||
if (
|
||||
prevAllowance <
|
||||
BigInt(transferResponse.approvalData.amountWei)
|
||||
) {
|
||||
setStep("approve");
|
||||
const transaction = approve({
|
||||
contract: getContract({
|
||||
client: client,
|
||||
address: transferResponse.approvalData.tokenAddress,
|
||||
chain: getCachedChain(
|
||||
transferResponse.approvalData.chainId,
|
||||
),
|
||||
}),
|
||||
spender: transferResponse.approvalData
|
||||
.spenderAddress as Address,
|
||||
amountWei: BigInt(
|
||||
transferResponse.approvalData.amountWei,
|
||||
),
|
||||
});
|
||||
// approve the transfer
|
||||
await sendAndConfirmTransaction({
|
||||
account: props.payer.account,
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setStep("transfer");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user