Skip to content

Commit ce1468f

Browse files
UI polish
1 parent c45e9cb commit ce1468f

File tree

6 files changed

+262
-90
lines changed

6 files changed

+262
-90
lines changed

apps/dashboard/.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,7 @@ ANALYTICS_SERVICE_URL=""
104104
NEXT_PUBLIC_NEBULA_URL=""
105105

106106
# required for billing parts of the dashboard (team -> settings -> billing / invoices)
107-
STRIPE_SECRET_KEY=""
107+
STRIPE_SECRET_KEY=""
108+
109+
# required for server wallet management
110+
NEXT_PUBLIC_THIRDWEB_VAULT_URL=""

apps/dashboard/src/app/team/[team_slug]/[project_slug]/transactions/server-wallets/components/create-server-wallet.tsx

Lines changed: 151 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
"use client";
22

3+
import { CopyTextButton } from "@/components/ui/CopyTextButton";
4+
import { Spinner } from "@/components/ui/Spinner/Spinner";
5+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
36
import { Button } from "@/components/ui/button";
7+
import { CheckboxWithLabel } from "@/components/ui/checkbox";
8+
import { Checkbox } from "@/components/ui/checkbox";
9+
import {
10+
Dialog,
11+
DialogContent,
12+
DialogHeader,
13+
DialogTitle,
14+
} from "@/components/ui/dialog";
415
import { THIRDWEB_VAULT_URL } from "@/constants/env";
516
import { useDashboardRouter } from "@/lib/DashboardRouter";
17+
import { cn } from "@/lib/utils";
618
import { updateProjectClient } from "@3rdweb-sdk/react/hooks/useApi";
719
import { useMutation } from "@tanstack/react-query";
820
import {
@@ -12,6 +24,7 @@ import {
1224
createVaultClient,
1325
} from "@thirdweb-dev/vault-sdk";
1426
import { Loader2 } from "lucide-react";
27+
import { useState } from "react";
1528
import { toast } from "sonner";
1629

1730
export default function CreateServerWallet(props: {
@@ -20,9 +33,13 @@ export default function CreateServerWallet(props: {
2033
managementAccessToken: string | undefined;
2134
}) {
2235
const router = useDashboardRouter();
36+
const [modalOpen, setModalOpen] = useState(false);
37+
const [keysConfirmed, setKeysConfirmed] = useState(false);
2338

2439
const initialiseProjectWithVaultMutation = useMutation({
2540
mutationFn: async () => {
41+
setModalOpen(true);
42+
2643
const vaultClient = await createVaultClient({
2744
baseUrl: THIRDWEB_VAULT_URL,
2845
});
@@ -257,11 +274,8 @@ export default function CreateServerWallet(props: {
257274
throw new Error("Failed to create access token");
258275
}
259276

260-
console.log(JSON.stringify(userAccessTokenRes.data, null, 2));
261-
console.log(JSON.stringify(serviceAccount.data, null, 2));
262-
console.log(JSON.stringify(managementAccessTokenRes.data, null, 2));
263-
264-
const apiServerResult = await updateProjectClient(
277+
// store the management access token in the project
278+
await updateProjectClient(
265279
{
266280
projectId: props.projectId,
267281
teamId: props.teamId,
@@ -278,13 +292,6 @@ export default function CreateServerWallet(props: {
278292
},
279293
);
280294

281-
console.log(apiServerResult);
282-
283-
// todo: show modal with credentials here
284-
// This should display:
285-
// - Service Account Admin Key
286-
// - Service Account Rotation Code
287-
// - "Project Level" Access Token: this allows all EOA operations for this service account, scoped to type, teamId and projectId by metadata
288295
return {
289296
serviceAccount: serviceAccount.data,
290297
userAccessToken: userAccessTokenRes.data,
@@ -293,6 +300,7 @@ export default function CreateServerWallet(props: {
293300
},
294301
onError: (error) => {
295302
toast.error(error.message);
303+
setModalOpen(false);
296304
},
297305
});
298306

@@ -337,62 +345,144 @@ export default function CreateServerWallet(props: {
337345
},
338346
});
339347

348+
const handleCreateServerWallet = async () => {
349+
if (!props.managementAccessToken) {
350+
const initResult = await initialiseProjectWithVaultMutation.mutateAsync();
351+
await createEoaMutation.mutateAsync({
352+
managementAccessToken: initResult.managementAccessToken.accessToken,
353+
});
354+
} else {
355+
await createEoaMutation.mutateAsync({
356+
managementAccessToken: props.managementAccessToken,
357+
});
358+
}
359+
};
360+
361+
const handleCloseModal = () => {
362+
if (!keysConfirmed) {
363+
return;
364+
}
365+
setModalOpen(false);
366+
setKeysConfirmed(false);
367+
};
368+
369+
const isLoading =
370+
initialiseProjectWithVaultMutation.isPending || createEoaMutation.isPending;
371+
340372
return (
341373
<>
342374
<Button
343375
variant={"primary"}
344-
onClick={async () => {
345-
if (!props.managementAccessToken) {
346-
const initResult =
347-
await initialiseProjectWithVaultMutation.mutateAsync();
348-
await createEoaMutation.mutateAsync({
349-
managementAccessToken:
350-
initResult.managementAccessToken.accessToken,
351-
});
352-
} else {
353-
await createEoaMutation.mutateAsync({
354-
managementAccessToken: props.managementAccessToken,
355-
});
356-
}
357-
}}
358-
disabled={initialiseProjectWithVaultMutation.isPending}
376+
onClick={handleCreateServerWallet}
377+
disabled={isLoading}
378+
className="flex flex-row items-center gap-2"
359379
>
360-
{initialiseProjectWithVaultMutation.isPending && (
361-
<Loader2 className="animate-spin" />
362-
)}
380+
{isLoading && <Loader2 className="animate-spin" />}
363381
Create Server Wallet
364382
</Button>
365-
{initialiseProjectWithVaultMutation.data ? (
366-
<div>
367-
Success! <h1>Admin Key</h1>
368-
<p>
369-
{initialiseProjectWithVaultMutation.data.serviceAccount.adminKey}
370-
</p>
371-
<h1>Rotation Code</h1>
372-
<p>
373-
{
374-
initialiseProjectWithVaultMutation.data.serviceAccount
375-
.rotationCode
376-
}
377-
</p>
378-
<h1>Access Token</h1>
379-
<p>
380-
{
381-
initialiseProjectWithVaultMutation.data.userAccessToken
382-
.accessToken
383-
}
384-
</p>
385-
<h1>Management Access Token</h1>
386-
<p>
387-
{
388-
initialiseProjectWithVaultMutation.data.managementAccessToken
389-
.accessToken
390-
}
391-
</p>
392-
</div>
393-
) : (
394-
<></>
395-
)}
383+
384+
<Dialog open={modalOpen} onOpenChange={handleCloseModal} modal={true}>
385+
<DialogContent
386+
className="overflow-hidden p-0"
387+
dialogCloseClassName={cn(!keysConfirmed && "hidden")}
388+
>
389+
{initialiseProjectWithVaultMutation.isPending ? (
390+
<div className="flex flex-col items-center justify-center gap-4 p-10">
391+
<Spinner className="size-8" />
392+
<DialogTitle>Generating your wallet management keys</DialogTitle>
393+
</div>
394+
) : initialiseProjectWithVaultMutation.data ? (
395+
<div>
396+
<DialogHeader className="p-6">
397+
<DialogTitle>Wallet Management Keys</DialogTitle>
398+
</DialogHeader>
399+
400+
<div className="space-y-6 p-6 pt-0">
401+
<Alert variant="destructive">
402+
<AlertTitle>Secure your keys</AlertTitle>
403+
<AlertDescription>
404+
These keys cannot be recovered. Store them securely as they
405+
provide access to your wallet.
406+
</AlertDescription>
407+
</Alert>
408+
409+
<div className="space-y-4">
410+
<div>
411+
<h3 className="mb-2 font-medium text-sm">Admin Key</h3>
412+
<div className="rounded-lg border bg-card p-3">
413+
<CopyTextButton
414+
textToCopy={
415+
initialiseProjectWithVaultMutation.data.serviceAccount
416+
.adminKey
417+
}
418+
className="!h-auto w-full justify-between bg-background px-3 py-3 font-mono text-xs"
419+
textToShow={maskSecret(
420+
initialiseProjectWithVaultMutation.data.serviceAccount
421+
.adminKey,
422+
)}
423+
copyIconPosition="right"
424+
tooltip="Copy Admin Key"
425+
/>
426+
</div>
427+
</div>
428+
429+
<div>
430+
<h3 className="mb-2 font-medium text-sm">Rotation Code</h3>
431+
<div className="rounded-lg border bg-card p-3">
432+
<CopyTextButton
433+
textToCopy={
434+
initialiseProjectWithVaultMutation.data.serviceAccount
435+
.rotationCode
436+
}
437+
className="!h-auto w-full justify-between bg-background px-3 py-3 font-mono text-xs"
438+
textToShow={maskSecret(
439+
initialiseProjectWithVaultMutation.data.serviceAccount
440+
.rotationCode,
441+
)}
442+
copyIconPosition="right"
443+
tooltip="Copy Rotation Code"
444+
/>
445+
</div>
446+
</div>
447+
448+
<div>
449+
<h3 className="mb-2 font-medium text-sm">Access Token</h3>
450+
<div className="rounded-lg border bg-card p-3">
451+
<CopyTextButton
452+
textToCopy={
453+
initialiseProjectWithVaultMutation.data
454+
.userAccessToken.accessToken
455+
}
456+
className="!h-auto w-full justify-between bg-background px-3 py-3 font-mono text-xs"
457+
textToShow={maskSecret(
458+
initialiseProjectWithVaultMutation.data
459+
.userAccessToken.accessToken,
460+
)}
461+
copyIconPosition="right"
462+
tooltip="Copy Access Token"
463+
/>
464+
</div>
465+
</div>
466+
</div>
467+
</div>
468+
469+
<div className="flex justify-end gap-3 border-t bg-card p-6">
470+
<CheckboxWithLabel className="text-foreground">
471+
<Checkbox
472+
checked={keysConfirmed}
473+
onCheckedChange={(v) => setKeysConfirmed(!!v)}
474+
/>
475+
I confirm that I've securely stored these keys
476+
</CheckboxWithLabel>
477+
478+
<Button onClick={handleCloseModal} disabled={!keysConfirmed}>
479+
Close
480+
</Button>
481+
</div>
482+
</div>
483+
) : null}
484+
</DialogContent>
485+
</Dialog>
396486
</>
397487
);
398488
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Button } from "@/components/ui/button";
2+
import { CodeServer } from "../../../../../../../@/components/ui/code/code.server";
3+
4+
export function TryItOut() {
5+
return (
6+
<div className="flex flex-col gap-6 overflow-hidden rounded-lg border border-border bg-card p-6">
7+
<div className="flex flex-row items-center gap-4">
8+
<div className="flex flex-1 flex-col gap-4 rounded-lg rounded-b-none lg:flex-row lg:justify-between">
9+
<div>
10+
<h2 className="font-semibold text-xl tracking-tight">Usage</h2>
11+
<p className="text-muted-foreground text-sm">
12+
Simple http API to send transactions to the blockchain
13+
</p>
14+
</div>
15+
</div>
16+
<Button variant={"primary"}>View API reference</Button>
17+
</div>
18+
<div>
19+
<CodeServer
20+
lang="ts"
21+
code={typescriptCodeExample()}
22+
className="bg-background"
23+
/>
24+
</div>
25+
</div>
26+
);
27+
}
28+
const typescriptCodeExample = () => `\
29+
const response = fetch("https://wallet.thirdweb.com/v1/account/send-transaction", {
30+
method: "POST",
31+
headers: {
32+
"Content-Type": "application/json",
33+
"x-secret-key": <your-project-secret-key>,
34+
},
35+
body: JSON.stringify({
36+
"executionOptions": {
37+
"type": "AA",
38+
"signerAddress": <your-server-wallet-address>
39+
},
40+
"transactionParams": [
41+
{
42+
"to": "0xeb0effdfb4dc5b3d5d3ac6ce29f3ed213e95d675",
43+
"value": "0"
44+
}
45+
],
46+
"vaultAccessToken": <your-wallet-access-token>,
47+
"chainId": "84532"
48+
}),
49+
});`;

apps/dashboard/src/app/team/[team_slug]/[project_slug]/transactions/server-wallets/page.tsx

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
1+
import { getProject } from "@/api/projects";
12
import { THIRDWEB_VAULT_URL } from "@/constants/env";
23
import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
34
import type { Wallet } from "./wallet-table/types.js";
45
import { ServerWalletsTable } from "./wallet-table/wallet-table";
5-
import { getProject } from "@/api/projects";
6-
import CreateServerWallet from "./components/create-server-wallet";
7-
8-
type EngineCloudService = {
9-
name: "engineCloud";
10-
actions: [];
11-
maskedAdminKey: string;
12-
managementAccessToken: string;
13-
};
146

157
export default async function TransactionsServerWalletsPage(props: {
168
params: Promise<{ team_slug: string; project_slug: string }>;
@@ -25,7 +17,7 @@ export default async function TransactionsServerWalletsPage(props: {
2517

2618
const projectEngineCloudService = project?.services.find(
2719
(service) => service.name === "engineCloud",
28-
) as EngineCloudService | undefined;
20+
);
2921

3022
const managementAccessToken =
3123
projectEngineCloudService?.managementAccessToken;
@@ -48,16 +40,15 @@ export default async function TransactionsServerWalletsPage(props: {
4840

4941
return (
5042
<>
51-
<CreateServerWallet
52-
projectId={project.id}
53-
teamId={project.teamId}
54-
managementAccessToken={managementAccessToken}
55-
/>
56-
5743
{eoas.error ? (
5844
<div>Error: {eoas.error.message}</div>
5945
) : (
60-
<ServerWalletsTable wallets={eoas.data.items as Wallet[]} />
46+
<ServerWalletsTable
47+
wallets={eoas.data.items as Wallet[]}
48+
projectId={project.id}
49+
teamId={project.teamId}
50+
managementAccessToken={managementAccessToken ?? undefined}
51+
/>
6152
)}
6253
</>
6354
);

0 commit comments

Comments
 (0)