Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
"react-error-boundary": "6.0.0",
"react-hook-form": "7.55.0",
"react-markdown": "10.1.0",
"react-qrcode-logo": "^3.0.0",
"react-table": "^7.8.0",
"recharts": "2.15.3",
"remark-gfm": "4.0.1",
Expand Down Expand Up @@ -130,4 +129,4 @@
"update-checkly": "npx checkly deploy"
},
"version": "3.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { useState } from "react";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { Button } from "@/components/ui/button";
import { storybookThirdwebClient } from "@/storybook/utils";
import { FundWalletModal } from "./index";

const meta: Meta<typeof FundWalletModal> = {
title: "Blocks/FundWalletModal",
component: Variant,
decorators: [
(Story) => (
<ThirdwebProvider>
<div className="p-10">
<ConnectButton client={storybookThirdwebClient} />
<div className="h-4" />
<Story />
</div>
</ThirdwebProvider>
),
],
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Test: Story = {
args: {},
};
function Variant() {
const [isOpen, setIsOpen] = useState(false);

return (
<div>
<Button type="button" onClick={() => setIsOpen(true)}>
Open
</Button>

<FundWalletModal
open={isOpen}
onOpenChange={setIsOpen}
title="This is a title"
description="This is a description"
recipientAddress="0x83Dd93fA5D8343094f850f90B3fb90088C1bB425"
client={storybookThirdwebClient}
/>
</div>
);
}
280 changes: 280 additions & 0 deletions apps/dashboard/src/@/components/blocks/fund-wallets-modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
import { useTheme } from "next-themes";
import { useState } from "react";
import { useForm } from "react-hook-form";
import type { ThirdwebClient } from "thirdweb";
import { defineChain } from "thirdweb/chains";
import { CheckoutWidget, useActiveWalletChain } from "thirdweb/react";
import { z } from "zod";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { TokenSelector } from "@/components/blocks/TokenSelector";
import { Button } from "@/components/ui/button";
import { DecimalInput } from "@/components/ui/decimal-input";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { getSDKTheme } from "@/utils/sdk-component-theme";
import { WalletAddress } from "../wallet-address";

const formSchema = z.object({
chainId: z.number({
required_error: "Chain is required",
}),
token: z.object(
{
chainId: z.number(),
address: z.string(),
symbol: z.string(),
name: z.string(),
decimals: z.number(),
},
{
required_error: "Token is required",
},
),
amount: z
.string()
.min(1, "Amount is required")
.refine((value) => {
const num = Number(value);
return !Number.isNaN(num) && num > 0;
}, "Amount must be greater than 0"),
});

type FormData = z.infer<typeof formSchema>;

type FundWalletModalProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description: string;
recipientAddress: string;
client: ThirdwebClient;
defaultChainId?: number;
checkoutWidgetTitle?: string;
};

export function FundWalletModal(props: FundWalletModalProps) {
return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
<DialogContent className="p-0 gap-0 !w-full !max-w-md">
<FundWalletModalContent
open={props.open}
onOpenChange={props.onOpenChange}
title={props.title}
description={props.description}
recipientAddress={props.recipientAddress}
client={props.client}
defaultChainId={props.defaultChainId}
checkoutWidgetTitle={props.checkoutWidgetTitle}
/>
</DialogContent>
</Dialog>
);
}

function FundWalletModalContent(props: FundWalletModalProps) {
const [step, setStep] = useState<"form" | "checkout">("form");
const activeChain = useActiveWalletChain();
const { theme } = useTheme();

const form = useForm<FormData>({
defaultValues: {
chainId: props.defaultChainId || activeChain?.id || 1,
token: undefined,
amount: "0.1",
},
mode: "onChange",
resolver: zodResolver(formSchema),
});

const selectedChainId = form.watch("chainId");

return (
<div>
{step === "form" ? (
<Form {...form}>
<form
onSubmit={form.handleSubmit(() => {
setStep("checkout");
})}
>
<DialogHeader className="p-4 lg:p-6">
<DialogTitle>{props.title}</DialogTitle>
<DialogDescription>{props.description}</DialogDescription>
</DialogHeader>

<div className="space-y-4 px-4 lg:px-6 pb-8">
<div>
<h3 className="text-sm font-medium"> Recipient</h3>
<WalletAddress
address={props.recipientAddress}
client={props.client}
className="py-1 h-auto"
iconClassName="size-4"
/>
</div>

<FormField
control={form.control}
name="chainId"
render={({ field }) => (
<FormItem>
<FormLabel>Chain</FormLabel>
<FormControl>
<SingleNetworkSelector
className="bg-card"
chainId={field.value}
onChange={(token) => {
field.onChange(token);
form.resetField("token", {
defaultValue: undefined,
});
}}
client={props.client}
placeholder="Select a chain"
disableDeprecated
disableTestnets
disableChainId
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="token"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Token</FormLabel>
<FormControl>
<TokenSelector
className="bg-card"
disableAddress
selectedToken={
field.value
? {
chainId: field.value.chainId,
address: field.value.address,
}
: undefined
}
onChange={field.onChange}
chainId={selectedChainId}
client={props.client}
placeholder="Select a token"
enabled={!!selectedChainId}
addNativeTokenIfMissing={true}
showCheck={true}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>

<FormField
control={form.control}
name="amount"
render={({ field }) => (
<FormItem>
<FormLabel>Amount</FormLabel>
<FormControl>
<div className="relative">
<DecimalInput
className="bg-card !text-xl h-auto font-semibold"
value={field.value}
onChange={field.onChange}
placeholder="0.1"
/>
{form.watch("token") && (
<div className="absolute right-4 top-1/2 -translate-y-1/2 text-sm text-muted-foreground">
{form.watch("token").symbol}
</div>
)}
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>

<div className="flex justify-end gap-3 p-4 lg:p-6 bg-card border-t rounded-b-lg">
<Button
variant="outline"
onClick={() => props.onOpenChange(false)}
>
Cancel
</Button>
<Button type="submit" className="gap-2">
Next
<ArrowRightIcon className="w-4 h-4" />
</Button>
</div>
</form>
</Form>
) : (
<div>
<DialogHeader className="sr-only">
<DialogTitle>{props.title}</DialogTitle>
<DialogDescription>{props.description}</DialogDescription>
</DialogHeader>

<div>
<CheckoutWidget
client={props.client}
// eslint-disable-next-line no-restricted-syntax
chain={defineChain(form.getValues("chainId"))}
tokenAddress={form.getValues("token").address as `0x${string}`}
amount={form.getValues("amount")}
seller={props.recipientAddress as `0x${string}`}
showThirdwebBranding={false}
className="!w-full !max-w-full !min-w-0 !rounded-b-none !border-none"
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
name={props.checkoutWidgetTitle}
/>
</div>

<div className="flex justify-end gap-3 p-4 lg:p-6 bg-card border-t rounded-b-lg">
<Button
variant="outline"
onClick={() => setStep("form")}
className="gap-2"
>
<ArrowLeftIcon className="size-4 text-muted-foreground" />
Back
</Button>

<Button
className="gap-2"
variant="outline"
onClick={() => props.onOpenChange(false)}
>
Cancel
</Button>
</div>
</div>
)}
</div>
);
}
4 changes: 2 additions & 2 deletions apps/dashboard/src/@/components/blocks/wallet-address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,11 @@ function WalletAvatar(props: {

return (
<div
className={cn("size-6 overflow-hidden rounded-full", props.iconClassName)}
className={cn("size-5 overflow-hidden rounded-full", props.iconClassName)}
>
{resolvedAvatarSrc ? (
<Img
className={cn("size-6 object-cover", props.iconClassName)}
className={cn("size-5 object-cover", props.iconClassName)}
src={resolvedAvatarSrc}
/>
) : (
Expand Down
7 changes: 6 additions & 1 deletion apps/dashboard/src/@/components/ui/switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@ import { cn } from "@/lib/utils";

const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> & {
size?: "sm";
}
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-foreground data-[state=unchecked]:bg-input",
className,
props.size === "sm" && "h-4.5 w-9",
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
props.size === "sm" &&
"h-4 w-4 translate-x-4 data-[state=checked]:translate-x-4",
)}
/>
</SwitchPrimitives.Root>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { TabButtons } from "@/components/ui/tabs";
type TabKey = "ts" | "react" | "react-native" | "dotnet" | "unity" | "unreal";

const tabNames: Record<TabKey, string> = {
dotnet: ".NET",
ts: "TypeScript",
react: "React",
"react-native": "React Native",
ts: "TypeScript",
dotnet: ".NET",
unity: "Unity",
unreal: "Unreal Engine",
};
Expand Down
Loading
Loading