Skip to content

Commit d353dfe

Browse files
committed
[MNY-62] Dashboard: Add CheckoutWidget in server wallets table, backend wallets table
1 parent 572368e commit d353dfe

File tree

13 files changed

+745
-338
lines changed

13 files changed

+745
-338
lines changed

apps/dashboard/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@
5757
"react-error-boundary": "6.0.0",
5858
"react-hook-form": "7.55.0",
5959
"react-markdown": "10.1.0",
60-
"react-qrcode-logo": "^3.0.0",
6160
"react-table": "^7.8.0",
6261
"recharts": "2.15.3",
6362
"remark-gfm": "4.0.1",
@@ -130,4 +129,4 @@
130129
"update-checkly": "npx checkly deploy"
131130
},
132131
"version": "3.0.0"
133-
}
132+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { Meta, StoryObj } from "@storybook/nextjs";
2+
import { useState } from "react";
3+
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
4+
import { Button } from "@/components/ui/button";
5+
import { storybookThirdwebClient } from "@/storybook/utils";
6+
import { FundWalletModal } from "./index";
7+
8+
const meta: Meta<typeof FundWalletModal> = {
9+
title: "Blocks/FundWalletModal",
10+
component: Variant,
11+
decorators: [
12+
(Story) => (
13+
<ThirdwebProvider>
14+
<div className="p-10">
15+
<ConnectButton client={storybookThirdwebClient} />
16+
<div className="h-4" />
17+
<Story />
18+
</div>
19+
</ThirdwebProvider>
20+
),
21+
],
22+
};
23+
24+
export default meta;
25+
type Story = StoryObj<typeof meta>;
26+
27+
export const Test: Story = {
28+
args: {},
29+
};
30+
function Variant() {
31+
const [isOpen, setIsOpen] = useState(false);
32+
33+
return (
34+
<div>
35+
<Button type="button" onClick={() => setIsOpen(true)}>
36+
Open
37+
</Button>
38+
39+
<FundWalletModal
40+
open={isOpen}
41+
onOpenChange={setIsOpen}
42+
title="This is a title"
43+
description="This is a description"
44+
recipientAddress="0x83Dd93fA5D8343094f850f90B3fb90088C1bB425"
45+
client={storybookThirdwebClient}
46+
/>
47+
</div>
48+
);
49+
}
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
"use client";
2+
3+
import { zodResolver } from "@hookform/resolvers/zod";
4+
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
5+
import { useTheme } from "next-themes";
6+
import { useState } from "react";
7+
import { useForm } from "react-hook-form";
8+
import type { ThirdwebClient } from "thirdweb";
9+
import { defineChain } from "thirdweb/chains";
10+
import { CheckoutWidget, useActiveWalletChain } from "thirdweb/react";
11+
import { z } from "zod";
12+
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
13+
import { TokenSelector } from "@/components/blocks/TokenSelector";
14+
import { Button } from "@/components/ui/button";
15+
import { DecimalInput } from "@/components/ui/decimal-input";
16+
import {
17+
Dialog,
18+
DialogContent,
19+
DialogDescription,
20+
DialogHeader,
21+
DialogTitle,
22+
} from "@/components/ui/dialog";
23+
import {
24+
Form,
25+
FormControl,
26+
FormField,
27+
FormItem,
28+
FormLabel,
29+
FormMessage,
30+
} from "@/components/ui/form";
31+
import { getSDKTheme } from "@/utils/sdk-component-theme";
32+
import { WalletAddress } from "../wallet-address";
33+
34+
const formSchema = z.object({
35+
chainId: z.number({
36+
required_error: "Chain is required",
37+
}),
38+
token: z.object(
39+
{
40+
chainId: z.number(),
41+
address: z.string(),
42+
symbol: z.string(),
43+
name: z.string(),
44+
decimals: z.number(),
45+
},
46+
{
47+
required_error: "Token is required",
48+
},
49+
),
50+
amount: z
51+
.string()
52+
.min(1, "Amount is required")
53+
.refine((value) => {
54+
const num = Number(value);
55+
return !Number.isNaN(num) && num > 0;
56+
}, "Amount must be greater than 0"),
57+
});
58+
59+
type FormData = z.infer<typeof formSchema>;
60+
61+
type FundWalletModalProps = {
62+
open: boolean;
63+
onOpenChange: (open: boolean) => void;
64+
title: string;
65+
description: string;
66+
recipientAddress: string;
67+
client: ThirdwebClient;
68+
defaultChainId?: number;
69+
};
70+
71+
export function FundWalletModal(props: FundWalletModalProps) {
72+
return (
73+
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
74+
<DialogContent className="p-0 gap-0 !w-full !max-w-md">
75+
<FundWalletModalContent
76+
open={props.open}
77+
onOpenChange={props.onOpenChange}
78+
title={props.title}
79+
description={props.description}
80+
recipientAddress={props.recipientAddress}
81+
client={props.client}
82+
defaultChainId={props.defaultChainId}
83+
/>
84+
</DialogContent>
85+
</Dialog>
86+
);
87+
}
88+
89+
function FundWalletModalContent(props: FundWalletModalProps) {
90+
const [step, setStep] = useState<"form" | "checkout">("form");
91+
const activeChain = useActiveWalletChain();
92+
const { theme } = useTheme();
93+
94+
const form = useForm<FormData>({
95+
defaultValues: {
96+
chainId: props.defaultChainId || activeChain?.id || 1,
97+
token: undefined,
98+
amount: "0.1",
99+
},
100+
mode: "onChange",
101+
resolver: zodResolver(formSchema),
102+
});
103+
104+
const selectedChainId = form.watch("chainId");
105+
106+
return (
107+
<div>
108+
{step === "form" ? (
109+
<Form {...form}>
110+
<form
111+
onSubmit={form.handleSubmit(() => {
112+
setStep("checkout");
113+
})}
114+
>
115+
<DialogHeader className="p-4 lg:p-6">
116+
<DialogTitle>{props.title}</DialogTitle>
117+
<DialogDescription>{props.description}</DialogDescription>
118+
</DialogHeader>
119+
120+
<div className="space-y-4 px-4 lg:px-6 pb-8">
121+
<div>
122+
<h3 className="text-sm font-medium"> Recipient</h3>
123+
<WalletAddress
124+
address={props.recipientAddress}
125+
client={props.client}
126+
className="py-1 h-auto"
127+
iconClassName="size-4"
128+
/>
129+
</div>
130+
131+
<FormField
132+
control={form.control}
133+
name="chainId"
134+
render={({ field }) => (
135+
<FormItem>
136+
<FormLabel>Chain</FormLabel>
137+
<FormControl>
138+
<SingleNetworkSelector
139+
className="bg-card"
140+
chainId={field.value}
141+
onChange={(token) => {
142+
field.onChange(token);
143+
form.resetField("token", {
144+
defaultValue: undefined,
145+
});
146+
}}
147+
client={props.client}
148+
placeholder="Select a chain"
149+
disableDeprecated
150+
disableTestnets
151+
disableChainId
152+
/>
153+
</FormControl>
154+
<FormMessage />
155+
</FormItem>
156+
)}
157+
/>
158+
159+
<FormField
160+
control={form.control}
161+
name="token"
162+
render={({ field }) => {
163+
return (
164+
<FormItem>
165+
<FormLabel>Token</FormLabel>
166+
<FormControl>
167+
<TokenSelector
168+
className="bg-card"
169+
disableAddress
170+
selectedToken={
171+
field.value
172+
? {
173+
chainId: field.value.chainId,
174+
address: field.value.address,
175+
}
176+
: undefined
177+
}
178+
onChange={field.onChange}
179+
chainId={selectedChainId}
180+
client={props.client}
181+
placeholder="Select a token"
182+
enabled={!!selectedChainId}
183+
addNativeTokenIfMissing={true}
184+
showCheck={true}
185+
/>
186+
</FormControl>
187+
<FormMessage />
188+
</FormItem>
189+
);
190+
}}
191+
/>
192+
193+
<FormField
194+
control={form.control}
195+
name="amount"
196+
render={({ field }) => (
197+
<FormItem>
198+
<FormLabel>Amount</FormLabel>
199+
<FormControl>
200+
<div className="relative">
201+
<DecimalInput
202+
className="bg-card !text-xl h-auto font-semibold"
203+
value={field.value}
204+
onChange={field.onChange}
205+
placeholder="0.1"
206+
/>
207+
{form.watch("token") && (
208+
<div className="absolute right-4 top-1/2 -translate-y-1/2 text-sm text-muted-foreground">
209+
{form.watch("token").symbol}
210+
</div>
211+
)}
212+
</div>
213+
</FormControl>
214+
<FormMessage />
215+
</FormItem>
216+
)}
217+
/>
218+
</div>
219+
220+
<div className="flex justify-end gap-3 p-4 lg:p-6 bg-card border-t rounded-b-lg">
221+
<Button
222+
variant="outline"
223+
onClick={() => props.onOpenChange(false)}
224+
>
225+
Cancel
226+
</Button>
227+
<Button type="submit" className="gap-2">
228+
Next
229+
<ArrowRightIcon className="w-4 h-4" />
230+
</Button>
231+
</div>
232+
</form>
233+
</Form>
234+
) : (
235+
<div>
236+
<DialogHeader className="sr-only">
237+
<DialogTitle>{props.title}</DialogTitle>
238+
<DialogDescription>{props.description}</DialogDescription>
239+
</DialogHeader>
240+
241+
<div>
242+
<CheckoutWidget
243+
client={props.client}
244+
// eslint-disable-next-line no-restricted-syntax
245+
chain={defineChain(form.getValues("chainId"))}
246+
tokenAddress={form.getValues("token").address as `0x${string}`}
247+
amount={form.getValues("amount")}
248+
seller={props.recipientAddress as `0x${string}`}
249+
showThirdwebBranding={false}
250+
className="!w-full !max-w-full !min-w-0 !rounded-b-none !border-none"
251+
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
252+
/>
253+
</div>
254+
255+
<div className="flex justify-end gap-3 p-4 lg:p-6 bg-card border-t rounded-b-lg">
256+
<Button
257+
variant="outline"
258+
onClick={() => setStep("form")}
259+
className="gap-2"
260+
>
261+
<ArrowLeftIcon className="size-4 text-muted-foreground" />
262+
Back
263+
</Button>
264+
265+
<Button
266+
className="gap-2"
267+
variant="outline"
268+
onClick={() => props.onOpenChange(false)}
269+
>
270+
Cancel
271+
</Button>
272+
</div>
273+
</div>
274+
)}
275+
</div>
276+
);
277+
}

apps/dashboard/src/@/components/blocks/wallet-address.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,11 @@ function WalletAvatar(props: {
192192

193193
return (
194194
<div
195-
className={cn("size-6 overflow-hidden rounded-full", props.iconClassName)}
195+
className={cn("size-5 overflow-hidden rounded-full", props.iconClassName)}
196196
>
197197
{resolvedAvatarSrc ? (
198198
<Img
199-
className={cn("size-6 object-cover", props.iconClassName)}
199+
className={cn("size-5 object-cover", props.iconClassName)}
200200
src={resolvedAvatarSrc}
201201
/>
202202
) : (

apps/dashboard/src/@/components/ui/switch.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,24 @@ import { cn } from "@/lib/utils";
77

88
const Switch = React.forwardRef<
99
React.ElementRef<typeof SwitchPrimitives.Root>,
10-
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
10+
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> & {
11+
size?: "sm";
12+
}
1113
>(({ className, ...props }, ref) => (
1214
<SwitchPrimitives.Root
1315
className={cn(
1416
"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",
1517
className,
18+
props.size === "sm" && "h-4.5 w-9",
1619
)}
1720
{...props}
1821
ref={ref}
1922
>
2023
<SwitchPrimitives.Thumb
2124
className={cn(
2225
"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",
26+
props.size === "sm" &&
27+
"h-4 w-4 translate-x-4 data-[state=checked]:translate-x-4",
2328
)}
2429
/>
2530
</SwitchPrimitives.Root>

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/IntegrateAPIKeyCodeTabs.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { TabButtons } from "@/components/ui/tabs";
66
type TabKey = "ts" | "react" | "react-native" | "dotnet" | "unity" | "unreal";
77

88
const tabNames: Record<TabKey, string> = {
9-
dotnet: ".NET",
9+
ts: "TypeScript",
1010
react: "React",
1111
"react-native": "React Native",
12-
ts: "TypeScript",
12+
dotnet: ".NET",
1313
unity: "Unity",
1414
unreal: "Unreal Engine",
1515
};

0 commit comments

Comments
 (0)