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" ;
36import { 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" ;
415import { THIRDWEB_VAULT_URL } from "@/constants/env" ;
516import { useDashboardRouter } from "@/lib/DashboardRouter" ;
17+ import { cn } from "@/lib/utils" ;
618import { updateProjectClient } from "@3rdweb-sdk/react/hooks/useApi" ;
719import { useMutation } from "@tanstack/react-query" ;
820import {
@@ -12,6 +24,7 @@ import {
1224 createVaultClient ,
1325} from "@thirdweb-dev/vault-sdk" ;
1426import { Loader2 } from "lucide-react" ;
27+ import { useState } from "react" ;
1528import { toast } from "sonner" ;
1629
1730export 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}
0 commit comments