2026-02-26 16:30:12 -06:00
import { useEffect , useState } from "react" ;
2026-02-19 14:02:29 -06:00
import { useMutation , useQueryClient } from "@tanstack/react-query" ;
import { useCompany } from "../context/CompanyContext" ;
import { useBreadcrumbs } from "../context/BreadcrumbContext" ;
import { companiesApi } from "../api/companies" ;
2026-02-23 14:41:21 -06:00
import { accessApi } from "../api/access" ;
2026-02-19 14:02:29 -06:00
import { queryKeys } from "../lib/queryKeys" ;
import { Button } from "@/components/ui/button" ;
2026-03-05 13:12:07 -06:00
import { Settings , Check } from "lucide-react" ;
2026-02-26 16:30:12 -06:00
import { CompanyPatternIcon } from "../components/CompanyPatternIcon" ;
2026-03-06 08:42:28 -06:00
import {
Field ,
ToggleField ,
HintIcon
} from "../components/agent-config-primitives" ;
2026-02-19 14:02:29 -06:00
2026-03-05 12:52:39 -06:00
type AgentSnippetInput = {
2026-03-05 12:37:56 -06:00
onboardingTextUrl : string ;
connectionCandidates? : string [ ] | null ;
2026-03-05 13:05:04 -06:00
testResolutionUrl? : string | null ;
2026-03-05 12:37:56 -06:00
} ;
2026-02-19 14:02:29 -06:00
export function CompanySettings() {
2026-03-06 08:42:28 -06:00
const {
companies ,
selectedCompany ,
selectedCompanyId ,
setSelectedCompanyId
} = useCompany ( ) ;
2026-02-19 14:02:29 -06:00
const { setBreadcrumbs } = useBreadcrumbs ( ) ;
const queryClient = useQueryClient ( ) ;
2026-02-26 16:30:12 -06:00
// General settings local state
const [ companyName , setCompanyName ] = useState ( "" ) ;
const [ description , setDescription ] = useState ( "" ) ;
const [ brandColor , setBrandColor ] = useState ( "" ) ;
// Sync local state from selected company
useEffect ( ( ) = > {
if ( ! selectedCompany ) return ;
setCompanyName ( selectedCompany . name ) ;
setDescription ( selectedCompany . description ? ? "" ) ;
setBrandColor ( selectedCompany . brandColor ? ? "" ) ;
} , [ selectedCompany ] ) ;
2026-02-23 14:41:21 -06:00
const [ inviteError , setInviteError ] = useState < string | null > ( null ) ;
2026-03-05 12:37:56 -06:00
const [ inviteSnippet , setInviteSnippet ] = useState < string | null > ( null ) ;
const [ snippetCopied , setSnippetCopied ] = useState ( false ) ;
const [ snippetCopyDelightId , setSnippetCopyDelightId ] = useState ( 0 ) ;
2026-02-19 14:02:29 -06:00
2026-02-26 16:30:12 -06:00
const generalDirty =
! ! selectedCompany &&
( companyName !== selectedCompany . name ||
description !== ( selectedCompany . description ? ? "" ) ||
brandColor !== ( selectedCompany . brandColor ? ? "" ) ) ;
const generalMutation = useMutation ( {
2026-03-06 08:42:28 -06:00
mutationFn : ( data : {
name : string ;
description : string | null ;
brandColor : string | null ;
} ) = > companiesApi . update ( selectedCompanyId ! , data ) ,
2026-02-26 16:30:12 -06:00
onSuccess : ( ) = > {
queryClient . invalidateQueries ( { queryKey : queryKeys.companies.all } ) ;
2026-03-06 08:42:28 -06:00
}
2026-02-26 16:30:12 -06:00
} ) ;
2026-02-19 14:02:29 -06:00
const settingsMutation = useMutation ( {
mutationFn : ( requireApproval : boolean ) = >
companiesApi . update ( selectedCompanyId ! , {
2026-03-06 08:42:28 -06:00
requireBoardApprovalForNewAgents : requireApproval
2026-02-19 14:02:29 -06:00
} ) ,
onSuccess : ( ) = > {
queryClient . invalidateQueries ( { queryKey : queryKeys.companies.all } ) ;
2026-03-06 08:42:28 -06:00
}
2026-02-19 14:02:29 -06:00
} ) ;
2026-02-23 14:41:21 -06:00
const inviteMutation = useMutation ( {
mutationFn : ( ) = >
accessApi . createCompanyInvite ( selectedCompanyId ! , {
2026-03-05 12:10:01 -06:00
allowedJoinTypes : "agent" ,
2026-03-06 08:42:28 -06:00
expiresInHours : 72
2026-02-23 14:41:21 -06:00
} ) ,
2026-03-05 12:00:38 -06:00
onSuccess : async ( invite ) = > {
2026-02-23 14:41:21 -06:00
setInviteError ( null ) ;
const base = window . location . origin . replace ( /\/+$/ , "" ) ;
2026-03-06 08:42:28 -06:00
const onboardingTextLink =
invite . onboardingTextUrl ? ?
invite . onboardingTextPath ? ?
` /api/invites/ ${ invite . token } /onboarding.txt ` ;
2026-03-05 12:10:01 -06:00
const absoluteUrl = onboardingTextLink . startsWith ( "http" )
? onboardingTextLink
: ` ${ base } ${ onboardingTextLink } ` ;
2026-03-05 12:37:56 -06:00
setSnippetCopied ( false ) ;
setSnippetCopyDelightId ( 0 ) ;
2026-03-06 10:00:34 -06:00
let snippet : string ;
2026-03-05 12:37:56 -06:00
try {
const manifest = await accessApi . getInviteOnboarding ( invite . token ) ;
2026-03-06 10:00:34 -06:00
snippet = buildAgentSnippet ( {
onboardingTextUrl : absoluteUrl ,
connectionCandidates :
manifest . onboarding . connectivity ? . connectionCandidates ? ? null ,
testResolutionUrl :
manifest . onboarding . connectivity ? . testResolutionEndpoint ? . url ? ?
null
} ) ;
2026-03-05 12:37:56 -06:00
} catch {
2026-03-06 10:00:34 -06:00
snippet = buildAgentSnippet ( {
onboardingTextUrl : absoluteUrl ,
connectionCandidates : null ,
testResolutionUrl : null
} ) ;
}
setInviteSnippet ( snippet ) ;
try {
await navigator . clipboard . writeText ( snippet ) ;
setSnippetCopied ( true ) ;
setSnippetCopyDelightId ( ( prev ) = > prev + 1 ) ;
setTimeout ( ( ) = > setSnippetCopied ( false ) , 2000 ) ;
} catch {
/* clipboard may not be available */
2026-03-05 12:37:56 -06:00
}
2026-03-06 08:42:28 -06:00
queryClient . invalidateQueries ( {
queryKey : queryKeys.sidebarBadges ( selectedCompanyId ! )
} ) ;
2026-02-23 14:41:21 -06:00
} ,
onError : ( err ) = > {
2026-03-06 08:42:28 -06:00
setInviteError (
err instanceof Error ? err . message : "Failed to create invite"
) ;
}
2026-02-23 14:41:21 -06:00
} ) ;
2026-03-05 12:10:01 -06:00
useEffect ( ( ) = > {
setInviteError ( null ) ;
2026-03-05 12:37:56 -06:00
setInviteSnippet ( null ) ;
setSnippetCopied ( false ) ;
setSnippetCopyDelightId ( 0 ) ;
2026-03-05 12:10:01 -06:00
} , [ selectedCompanyId ] ) ;
2026-03-02 10:31:54 -06:00
const archiveMutation = useMutation ( {
mutationFn : ( {
companyId ,
2026-03-06 08:42:28 -06:00
nextCompanyId
2026-03-02 10:31:54 -06:00
} : {
companyId : string ;
nextCompanyId : string | null ;
} ) = > companiesApi . archive ( companyId ) . then ( ( ) = > ( { nextCompanyId } ) ) ,
onSuccess : async ( { nextCompanyId } ) = > {
if ( nextCompanyId ) {
setSelectedCompanyId ( nextCompanyId ) ;
}
2026-03-06 08:42:28 -06:00
await queryClient . invalidateQueries ( {
queryKey : queryKeys.companies.all
} ) ;
await queryClient . invalidateQueries ( {
queryKey : queryKeys.companies.stats
} ) ;
}
2026-03-02 10:31:54 -06:00
} ) ;
2026-02-23 14:41:21 -06:00
2026-02-19 14:02:29 -06:00
useEffect ( ( ) = > {
setBreadcrumbs ( [
{ label : selectedCompany?.name ? ? "Company" , href : "/dashboard" } ,
2026-03-06 08:42:28 -06:00
{ label : "Settings" }
2026-02-19 14:02:29 -06:00
] ) ;
} , [ setBreadcrumbs , selectedCompany ? . name ] ) ;
if ( ! selectedCompany ) {
return (
< div className = "text-sm text-muted-foreground" >
No company selected . Select a company from the switcher above .
< / div >
) ;
}
2026-02-26 16:30:12 -06:00
function handleSaveGeneral() {
generalMutation . mutate ( {
name : companyName.trim ( ) ,
description : description.trim ( ) || null ,
2026-03-06 08:42:28 -06:00
brandColor : brandColor || null
2026-02-26 16:30:12 -06:00
} ) ;
}
2026-02-19 14:02:29 -06:00
return (
< div className = "max-w-2xl space-y-6" >
< div className = "flex items-center gap-2" >
< Settings className = "h-5 w-5 text-muted-foreground" / >
< h1 className = "text-lg font-semibold" > Company Settings < / h1 >
< / div >
2026-02-26 16:30:12 -06:00
{ /* General */ }
2026-02-19 14:02:29 -06:00
< div className = "space-y-4" >
< div className = "text-xs font-medium text-muted-foreground uppercase tracking-wide" >
2026-02-26 16:30:12 -06:00
General
< / div >
< div className = "space-y-3 rounded-md border border-border px-4 py-4" >
< Field label = "Company name" hint = "The display name for your company." >
< input
className = "w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
type = "text"
value = { companyName }
onChange = { ( e ) = > setCompanyName ( e . target . value ) }
/ >
< / Field >
2026-03-06 08:42:28 -06:00
< Field
label = "Description"
hint = "Optional description shown in the company profile."
>
2026-02-26 16:30:12 -06:00
< input
className = "w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
type = "text"
value = { description }
placeholder = "Optional company description"
onChange = { ( e ) = > setDescription ( e . target . value ) }
/ >
< / Field >
2026-02-19 14:02:29 -06:00
< / div >
2026-02-26 16:30:12 -06:00
< / div >
{ /* Appearance */ }
< div className = "space-y-4" >
< div className = "text-xs font-medium text-muted-foreground uppercase tracking-wide" >
Appearance
< / div >
< div className = "space-y-3 rounded-md border border-border px-4 py-4" >
< div className = "flex items-start gap-4" >
< div className = "shrink-0" >
< CompanyPatternIcon
companyName = { companyName || selectedCompany . name }
brandColor = { brandColor || null }
className = "rounded-[14px]"
/ >
2026-02-19 14:02:29 -06:00
< / div >
2026-02-26 16:30:12 -06:00
< div className = "flex-1 space-y-2" >
2026-03-06 08:42:28 -06:00
< Field
label = "Brand color"
hint = "Sets the hue for the company icon. Leave empty for auto-generated color."
>
2026-02-26 16:30:12 -06:00
< div className = "flex items-center gap-2" >
< input
type = "color"
value = { brandColor || "#6366f1" }
onChange = { ( e ) = > setBrandColor ( e . target . value ) }
className = "h-8 w-8 cursor-pointer rounded border border-border bg-transparent p-0"
/ >
< input
type = "text"
value = { brandColor }
onChange = { ( e ) = > {
const v = e . target . value ;
if ( v === "" || /^#[0-9a-fA-F]{0,6}$/ . test ( v ) ) {
setBrandColor ( v ) ;
}
} }
placeholder = "Auto"
className = "w-28 rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm font-mono outline-none"
/ >
{ brandColor && (
< Button
size = "sm"
variant = "ghost"
onClick = { ( ) = > setBrandColor ( "" ) }
className = "text-xs text-muted-foreground"
>
Clear
< / Button >
) }
< / div >
< / Field >
2026-02-19 14:02:29 -06:00
< / div >
< / div >
2026-02-26 16:30:12 -06:00
< / div >
< / div >
{ /* Save button for General + Appearance */ }
{ generalDirty && (
< div className = "flex items-center gap-2" >
2026-02-19 14:02:29 -06:00
< Button
size = "sm"
2026-02-26 16:30:12 -06:00
onClick = { handleSaveGeneral }
disabled = { generalMutation . isPending || ! companyName . trim ( ) }
2026-02-19 14:02:29 -06:00
>
2026-02-26 16:30:12 -06:00
{ generalMutation . isPending ? "Saving..." : "Save changes" }
2026-02-19 14:02:29 -06:00
< / Button >
2026-02-26 16:30:12 -06:00
{ generalMutation . isSuccess && (
< span className = "text-xs text-muted-foreground" > Saved < / span >
) }
{ generalMutation . isError && (
< span className = "text-xs text-destructive" >
{ generalMutation . error instanceof Error
? generalMutation . error . message
: "Failed to save" }
< / span >
) }
< / div >
) }
{ /* Hiring */ }
< div className = "space-y-4" >
< div className = "text-xs font-medium text-muted-foreground uppercase tracking-wide" >
Hiring
< / div >
< div className = "rounded-md border border-border px-4 py-3" >
< ToggleField
label = "Require board approval for new hires"
hint = "New agent hires stay pending until approved by board."
checked = { ! ! selectedCompany . requireBoardApprovalForNewAgents }
onChange = { ( v ) = > settingsMutation . mutate ( v ) }
/ >
2026-02-19 14:02:29 -06:00
< / div >
< / div >
2026-02-23 14:41:21 -06:00
2026-02-26 16:30:12 -06:00
{ /* Invites */ }
2026-02-23 14:41:21 -06:00
< div className = "space-y-4" >
< div className = "text-xs font-medium text-muted-foreground uppercase tracking-wide" >
Invites
< / div >
< div className = "space-y-3 rounded-md border border-border px-4 py-4" >
2026-02-26 16:30:12 -06:00
< div className = "flex items-center gap-1.5" >
2026-03-05 12:10:01 -06:00
< span className = "text-xs text-muted-foreground" >
2026-03-05 13:12:07 -06:00
Generate an agent snippet for join flows .
2026-03-05 12:10:01 -06:00
< / span >
2026-03-05 13:12:07 -06:00
< HintIcon text = "Creates an agent-only invite (72h) and renders a copy-ready snippet." / >
2026-02-23 14:41:21 -06:00
< / div >
2026-03-05 12:10:01 -06:00
< div className = "flex flex-wrap items-center gap-2" >
2026-03-06 08:42:28 -06:00
< Button
size = "sm"
onClick = { ( ) = > inviteMutation . mutate ( ) }
disabled = { inviteMutation . isPending }
>
{ inviteMutation . isPending
? "Generating..."
: "Generate agent snippet" }
2026-03-05 12:10:01 -06:00
< / Button >
< / div >
2026-03-06 08:42:28 -06:00
{ inviteError && (
< p className = "text-sm text-destructive" > { inviteError } < / p >
) }
2026-03-05 12:37:56 -06:00
{ inviteSnippet && (
< div className = "rounded-md border border-border bg-muted/30 p-2" >
< div className = "flex items-center justify-between gap-2" >
2026-03-06 08:42:28 -06:00
< div className = "text-xs text-muted-foreground" >
Agent Snippet
< / div >
2026-03-05 12:37:56 -06:00
{ snippetCopied && (
2026-03-06 08:42:28 -06:00
< span
key = { snippetCopyDelightId }
className = "flex items-center gap-1 text-xs text-green-600 animate-pulse"
>
2026-03-05 12:37:56 -06:00
< Check className = "h-3 w-3" / >
Copied
< / span >
) }
< / div >
< div className = "mt-1 space-y-1.5" >
< textarea
2026-03-05 12:52:39 -06:00
className = "h-[28rem] w-full rounded-md border border-border bg-background px-2 py-1.5 font-mono text-xs outline-none"
2026-03-05 12:37:56 -06:00
value = { inviteSnippet }
readOnly
/ >
< div className = "flex justify-end" >
< Button
size = "sm"
variant = "ghost"
onClick = { async ( ) = > {
try {
await navigator . clipboard . writeText ( inviteSnippet ) ;
setSnippetCopied ( true ) ;
setSnippetCopyDelightId ( ( prev ) = > prev + 1 ) ;
setTimeout ( ( ) = > setSnippetCopied ( false ) , 2000 ) ;
2026-03-06 08:42:28 -06:00
} catch {
/* clipboard may not be available */
}
2026-03-05 12:37:56 -06:00
} }
>
{ snippetCopied ? "Copied snippet" : "Copy snippet" }
< / Button >
< / div >
< / div >
< / div >
) }
2026-02-23 14:41:21 -06:00
< / div >
< / div >
2026-03-02 10:31:54 -06:00
2026-03-05 13:12:07 -06:00
{ /* Danger Zone */ }
2026-03-02 10:31:54 -06:00
< div className = "space-y-4" >
2026-03-05 13:12:07 -06:00
< div className = "text-xs font-medium text-destructive uppercase tracking-wide" >
Danger Zone
2026-03-02 10:31:54 -06:00
< / div >
2026-03-05 13:12:07 -06:00
< div className = "space-y-3 rounded-md border border-destructive/40 bg-destructive/5 px-4 py-4" >
2026-03-02 10:31:54 -06:00
< p className = "text-sm text-muted-foreground" >
2026-03-06 08:42:28 -06:00
Archive this company to hide it from the sidebar . This persists in
the database .
2026-03-02 10:31:54 -06:00
< / p >
< div className = "flex items-center gap-2" >
< Button
size = "sm"
2026-03-05 13:12:07 -06:00
variant = "destructive"
2026-03-06 08:42:28 -06:00
disabled = {
archiveMutation . isPending ||
selectedCompany . status === "archived"
}
2026-03-02 10:31:54 -06:00
onClick = { ( ) = > {
if ( ! selectedCompanyId ) return ;
const confirmed = window . confirm (
2026-03-06 08:42:28 -06:00
` Archive company " ${ selectedCompany . name } "? It will be hidden from the sidebar. `
2026-03-02 10:31:54 -06:00
) ;
if ( ! confirmed ) return ;
2026-03-06 08:42:28 -06:00
const nextCompanyId =
companies . find (
( company ) = >
company . id !== selectedCompanyId &&
company . status !== "archived"
) ? . id ? ? null ;
archiveMutation . mutate ( {
companyId : selectedCompanyId ,
nextCompanyId
} ) ;
2026-03-02 10:31:54 -06:00
} }
>
{ archiveMutation . isPending
? "Archiving..."
: selectedCompany . status === "archived"
2026-03-06 08:42:28 -06:00
? "Already archived"
: "Archive company" }
2026-03-02 10:31:54 -06:00
< / Button >
{ archiveMutation . isError && (
< span className = "text-xs text-destructive" >
{ archiveMutation . error instanceof Error
? archiveMutation . error . message
: "Failed to archive company" }
< / span >
) }
< / div >
< / div >
< / div >
2026-02-19 14:02:29 -06:00
< / div >
) ;
}
2026-03-05 12:37:56 -06:00
2026-03-05 12:52:39 -06:00
function buildAgentSnippet ( input : AgentSnippetInput ) {
const candidateUrls = buildCandidateOnboardingUrls ( input ) ;
2026-03-05 16:13:43 -06:00
const resolutionTestUrl = buildResolutionTestUrl ( input ) ;
2026-03-05 12:37:56 -06:00
2026-03-06 08:42:28 -06:00
const candidateList =
candidateUrls . length > 0
? candidateUrls . map ( ( u ) = > ` - ${ u } ` ) . join ( "\n" )
: "- (No candidate URLs available yet.)" ;
2026-03-05 12:52:39 -06:00
2026-03-06 08:42:28 -06:00
const connectivityBlock =
candidateUrls . length === 0
? ` No candidate URLs are available. Ask your user to configure a reachable hostname in Paperclip, then retry.
2026-03-05 16:13:43 -06:00
Suggested steps :
- choose a hostname that resolves to the Paperclip host from your runtime
- run : pnpm paperclipai allowed - hostname < host >
- restart Paperclip
- verify with : curl - fsS http : //<host>:3100/api/health
- regenerate this invite snippet `
2026-03-06 08:42:28 -06:00
: ` If none are reachable, ask your user to add a reachable hostname in Paperclip, restart, and retry.
2026-03-05 16:13:43 -06:00
Suggested command :
- pnpm paperclipai allowed - hostname < host >
Then verify with : curl - fsS < base - url > / api / health ` ;
2026-03-05 12:52:39 -06:00
2026-03-05 16:13:43 -06:00
const resolutionLine = resolutionTestUrl
2026-03-06 08:42:28 -06:00
? ` \ nIf you need to test callback reachability, call: http://<whatever hostname worked above>?url=<urlencoded-webhook-url> `
2026-03-05 16:13:43 -06:00
: "" ;
return ` You're invited to join a Paperclip organization.
The URLs you should try are :
$ { candidateList }
Connectivity guidance :
If you are running on a different machine than Paperclip , Paperclip must be reachable at one of the hostnames used above .
Verify the hostname works from your runtime with : GET < base - url > / api / health
$ { connectivityBlock }
For your "agentName" , just use your own agent name ( OpenClaw or whatever your name is ) and send it directly .
You also need to send a webhook URL that Paperclip can access . Check your config and include that URL . $ { resolutionLine }
-- -
2026-03-06 08:42:28 -06:00
In order for Paperclip to work smoothly , you need to enable \ ` gateway.http.endpoints.responses.enabled=true \` in your OpenClaw config.
2026-03-05 13:05:04 -06:00
2026-03-05 16:13:43 -06:00
Please enable that setting and notify your user that we did so .
2026-03-06 08:42:28 -06:00
Before you do anything , please respond to your user that you understand the instructions and you 're going to work on them. Then do the step above in another session called "paperclip-onboarding" and then tell your user when you' re done . Update your user in intermediate steps along the way so they know what ' s going on .
Then after you ' ve connected to Paperclip ( exchanged keys etc . ) you MUST review and follow the onboarding instructions in onboarding . txt they give you .
2026-03-05 16:13:43 -06:00
` ;
2026-03-05 12:52:39 -06:00
}
function buildCandidateOnboardingUrls ( input : AgentSnippetInput ) : string [ ] {
2026-03-05 12:37:56 -06:00
const candidates = ( input . connectionCandidates ? ? [ ] )
. map ( ( candidate ) = > candidate . trim ( ) )
. filter ( Boolean ) ;
2026-03-05 12:52:39 -06:00
const urls = new Set < string > ( ) ;
let onboardingUrl : URL | null = null ;
try {
onboardingUrl = new URL ( input . onboardingTextUrl ) ;
urls . add ( onboardingUrl . toString ( ) ) ;
} catch {
const trimmed = input . onboardingTextUrl . trim ( ) ;
if ( trimmed ) {
urls . add ( trimmed ) ;
}
}
2026-03-05 12:37:56 -06:00
2026-03-05 12:52:39 -06:00
if ( ! onboardingUrl ) {
2026-03-05 12:37:56 -06:00
for ( const candidate of candidates ) {
2026-03-05 12:52:39 -06:00
urls . add ( candidate ) ;
2026-03-05 12:37:56 -06:00
}
2026-03-05 12:52:39 -06:00
return Array . from ( urls ) ;
2026-03-05 12:37:56 -06:00
}
2026-03-05 12:52:39 -06:00
const onboardingPath = ` ${ onboardingUrl . pathname } ${ onboardingUrl . search } ` ;
for ( const candidate of candidates ) {
try {
const base = new URL ( candidate ) ;
urls . add ( ` ${ base . origin } ${ onboardingPath } ` ) ;
} catch {
urls . add ( candidate ) ;
}
}
2026-03-05 12:37:56 -06:00
2026-03-05 12:52:39 -06:00
return Array . from ( urls ) ;
2026-03-05 12:37:56 -06:00
}
2026-03-05 13:05:04 -06:00
function buildResolutionTestUrl ( input : AgentSnippetInput ) : string | null {
const explicit = input . testResolutionUrl ? . trim ( ) ;
if ( explicit ) return explicit ;
try {
const onboardingUrl = new URL ( input . onboardingTextUrl ) ;
2026-03-06 08:42:28 -06:00
const testPath = onboardingUrl . pathname . replace (
/\/onboarding\.txt$/ ,
"/test-resolution"
) ;
2026-03-05 13:05:04 -06:00
return ` ${ onboardingUrl . origin } ${ testPath } ` ;
} catch {
return null ;
}
}