11"use client" ;
22
3- import { SmartFormDialog } from "@/components/form-dialog" ;
3+ import { FormDialog , SmartFormDialog } from "@/components/form-dialog" ;
4+ import { InputField , SelectField } from "@/components/form-fields" ;
45import { SettingCard } from "@/components/settings" ;
56import { DeleteUserDialog , ImpersonateUserDialog } from "@/components/user-dialogs" ;
67import { useThemeWatcher } from '@/lib/theme' ;
@@ -10,9 +11,9 @@ import { useAsyncCallback } from "@stackframe/stack-shared/dist/hooks/use-async-
1011import { fromNow } from "@stackframe/stack-shared/dist/utils/dates" ;
1112import { throwErr } from '@stackframe/stack-shared/dist/utils/errors' ;
1213import { deindent } from "@stackframe/stack-shared/dist/utils/strings" ;
13- import { ActionCell , Avatar , AvatarFallback , AvatarImage , Button , DropdownMenu , DropdownMenuContent , DropdownMenuItem , DropdownMenuSeparator , DropdownMenuTrigger , Input , Separator , SimpleTooltip , Table , TableBody , TableCell , TableHead , TableHeader , TableRow , Typography , cn } from "@stackframe/stack-ui" ;
14+ import { Accordion , AccordionContent , AccordionItem , AccordionTrigger , ActionCell , Avatar , AvatarFallback , AvatarImage , Button , DropdownMenu , DropdownMenuContent , DropdownMenuItem , DropdownMenuSeparator , DropdownMenuTrigger , Input , Separator , SimpleTooltip , Table , TableBody , TableCell , TableHead , TableHeader , TableRow , Typography , cn } from "@stackframe/stack-ui" ;
1415import { AtSign , Calendar , Check , Hash , Mail , MoreHorizontal , Shield , SquareAsterisk , X } from "lucide-react" ;
15- import { useMemo , useRef , useState } from "react" ;
16+ import { useEffect , useMemo , useRef , useState } from "react" ;
1617import * as yup from "yup" ;
1718import { PageLayout } from "../../page-layout" ;
1819import { useAdminApp } from "../../use-admin-app" ;
@@ -167,6 +168,7 @@ type MetadataEditorProps = {
167168function MetadataEditor ( { title, initialValue, onUpdate, hint } : MetadataEditorProps ) {
168169 const formatJson = ( json : string ) => JSON . stringify ( JSON . parse ( json ) , null , 2 ) ;
169170 const [ hasChanged , setHasChanged ] = useState ( false ) ;
171+ const [ isMounted , setIsMounted ] = useState ( false ) ;
170172
171173 const { mounted, theme } = useThemeWatcher ( ) ;
172174
@@ -180,6 +182,14 @@ function MetadataEditor({ title, initialValue, onUpdate, hint }: MetadataEditorP
180182 }
181183 } , [ value ] ) ;
182184
185+ // Ensure proper mounting lifecycle
186+ useEffect ( ( ) => {
187+ setIsMounted ( true ) ;
188+ return ( ) => {
189+ setIsMounted ( false ) ;
190+ } ;
191+ } , [ ] ) ;
192+
183193 const handleSave = async ( ) => {
184194 if ( isJson ) {
185195 const formatted = formatJson ( value ) ;
@@ -189,14 +199,18 @@ function MetadataEditor({ title, initialValue, onUpdate, hint }: MetadataEditorP
189199 }
190200 } ;
191201
202+ // Only render Monaco when both mounted states are true
203+ const shouldRenderMonaco = mounted && isMounted ;
204+
192205 return < div className = "flex flex-col" >
193206 < h3 className = 'text-sm mb-4 font-semibold' >
194207 { title }
195208 < SimpleTooltip tooltip = { hint } type = "info" inline className = "ml-2 mb-[2px]" />
196209 </ h3 >
197- { mounted && (
210+ { shouldRenderMonaco ? (
198211 < div className = { cn ( "rounded-md overflow-hidden" , theme !== 'dark' && "border" ) } >
199212 < MonacoEditor
213+ key = { `monaco-${ theme } ` } // Force recreation on theme change
200214 height = "240px"
201215 defaultLanguage = "json"
202216 value = { value }
@@ -217,6 +231,10 @@ function MetadataEditor({ title, initialValue, onUpdate, hint }: MetadataEditorP
217231 } }
218232 />
219233 </ div >
234+ ) : (
235+ < div className = { cn ( "rounded-md overflow-hidden h-[240px] flex items-center justify-center" , theme !== 'dark' && "border" ) } >
236+ < div className = "text-sm text-muted-foreground" > Loading editor...</ div >
237+ </ div >
220238 ) }
221239 < div className = { cn ( 'self-end flex items-end gap-2 transition-all h-0 opacity-0 overflow-hidden' , hasChanged && 'h-[48px] opacity-100' ) } >
222240 < Button
@@ -404,9 +422,101 @@ function AddEmailDialog({ user, open, onOpenChange }: AddEmailDialogProps) {
404422 ) ;
405423}
406424
425+ type SendVerificationEmailDialogProps = {
426+ channel : ServerContactChannel ,
427+ open : boolean ,
428+ onOpenChange : ( open : boolean ) => void ,
429+ } ;
430+
431+ function SendVerificationEmailDialog ( { channel, open, onOpenChange } : SendVerificationEmailDialogProps ) {
432+ const stackAdminApp = useAdminApp ( ) ;
433+ const project = stackAdminApp . useProject ( ) ;
434+ const domains = project . config . domains ;
435+
436+ return (
437+ < FormDialog
438+ title = "Send Verification Email"
439+ description = { `Send a verification email to ${ channel . value } ? The email will contain a callback link to your domain.` }
440+ open = { open }
441+ onOpenChange = { onOpenChange }
442+ formSchema = { yup . object ( {
443+ selected : yup . string ( ) . defined ( ) ,
444+ localhostPort : yup . number ( ) . test ( "required-if-localhost" , "Required if localhost is selected" , ( value , context ) => {
445+ return context . parent . selected === "localhost" ? value !== undefined : true ;
446+ } ) ,
447+ handlerPath : yup . string ( ) . optional ( ) ,
448+ } ) }
449+ okButton = { {
450+ label : "Send" ,
451+ } }
452+ render = { ( { control, watch } ) => (
453+ < >
454+ < SelectField
455+ control = { control }
456+ name = "selected"
457+ label = "Domain"
458+ options = { [
459+ ...domains . map ( ( domain , index ) => ( { value : index . toString ( ) , label : domain . domain } ) ) ,
460+ ...( project . config . allowLocalhost ? [ { value : "localhost" , label : "localhost" } ] : [ ] )
461+ ] }
462+ />
463+ { watch ( "selected" ) === "localhost" && (
464+ < >
465+ < InputField
466+ control = { control }
467+ name = "localhostPort"
468+ label = "Localhost Port"
469+ placeholder = "3000"
470+ type = "number"
471+ />
472+ < Accordion type = "single" collapsible className = "w-full" >
473+ < AccordionItem value = "item-1" >
474+ < AccordionTrigger > Advanced</ AccordionTrigger >
475+ < AccordionContent className = "flex flex-col gap-8" >
476+ < div className = "flex flex-col gap-2" >
477+ < InputField
478+ label = "Handler path"
479+ name = "handlerPath"
480+ control = { control }
481+ placeholder = '/handler'
482+ />
483+ < Typography variant = "secondary" type = "footnote" >
484+ only modify this if you changed the default handler path in your app
485+ </ Typography >
486+ </ div >
487+ </ AccordionContent >
488+ </ AccordionItem >
489+ </ Accordion >
490+ </ >
491+ ) }
492+ </ >
493+ ) }
494+ onSubmit = { async ( values ) => {
495+ let baseUrl : string ;
496+ let handlerPath : string ;
497+ if ( values . selected === "localhost" ) {
498+ baseUrl = `http://localhost:${ values . localhostPort } ` ;
499+ handlerPath = values . handlerPath || '/handler' ;
500+ } else {
501+ const domain = domains [ parseInt ( values . selected ) ] ;
502+ baseUrl = domain . domain ;
503+ handlerPath = domain . handlerPath ;
504+ }
505+ const callbackUrl = new URL ( handlerPath + '/email-verification' , baseUrl ) . toString ( ) ;
506+ console . log ( callbackUrl ) ;
507+ await channel . sendVerificationEmail ( { callbackUrl } ) ;
508+ } }
509+ />
510+ ) ;
511+ }
512+
407513function ContactChannelsSection ( { user } : ContactChannelsSectionProps ) {
408514 const contactChannels = user . useContactChannels ( ) ;
409515 const [ isAddEmailDialogOpen , setIsAddEmailDialogOpen ] = useState ( false ) ;
516+ const [ sendVerificationEmailDialog , setSendVerificationEmailDialog ] = useState < {
517+ channel : ServerContactChannel ,
518+ isOpen : boolean ,
519+ } | null > ( null ) ;
410520
411521 const toggleUsedForAuth = async ( channel : ServerContactChannel ) => {
412522 await channel . update ( { usedForAuth : ! channel . usedForAuth } ) ;
@@ -441,6 +551,18 @@ function ContactChannelsSection({ user }: ContactChannelsSectionProps) {
441551 onOpenChange = { setIsAddEmailDialogOpen }
442552 />
443553
554+ { sendVerificationEmailDialog && (
555+ < SendVerificationEmailDialog
556+ channel = { sendVerificationEmailDialog . channel }
557+ open = { sendVerificationEmailDialog . isOpen }
558+ onOpenChange = { ( open ) => {
559+ if ( ! open ) {
560+ setSendVerificationEmailDialog ( null ) ;
561+ }
562+ } }
563+ />
564+ ) }
565+
444566 { contactChannels . length === 0 ? (
445567 < div className = "flex flex-col items-center gap-2 p-4 border rounded-md bg-muted/10" >
446568 < p className = 'text-sm text-gray-500 text-center' >
@@ -488,7 +610,10 @@ function ContactChannelsSection({ user }: ContactChannelsSectionProps) {
488610 ...( ! channel . isVerified ? [ {
489611 item : "Send verification email" ,
490612 onClick : async ( ) => {
491- await channel . sendVerificationEmail ( ) ;
613+ setSendVerificationEmailDialog ( {
614+ channel,
615+ isOpen : true ,
616+ } ) ;
492617 } ,
493618 } ] : [ ] ) ,
494619 {
0 commit comments