import { type CSSProperties, useMemo, useState } from "react"; import { RestApiClient } from "twenty-client-sdk/rest"; import { defineSettingsFrontComponent } from "twenty-sdk/define"; import { enqueueSnackbar } from "twenty-sdk/front-component"; import { SETTINGS_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from "src/constants/universal-identifiers"; type TemplatesResponse = { templates?: Array<{ id: string }>; }; type SetupWebhookResponse = { success: boolean; message: string; }; const styles: Record = { container: { color: "var(--t-font-color-primary)", display: "flex", flexDirection: "column", fontFamily: "var(--t-font-family)", gap: "28px", maxWidth: "720px", padding: "24px", }, section: { display: "flex", flexDirection: "column", gap: "14px" }, heading: { fontSize: "16px", margin: 0 }, description: { color: "var(--t-font-color-secondary)", fontSize: "13px", lineHeight: 1.5, margin: 0, }, actions: { display: "flex", gap: "8px" }, button: { background: "var(--t-background-secondary)", border: "1px solid var(--t-border-color-medium)", borderRadius: "var(--t-border-radius-sm)", color: "var(--t-font-color-secondary)", cursor: "pointer", fontFamily: "var(--t-font-family)", fontSize: "13px", padding: "8px 12px", }, code: { background: "var(--t-background-secondary)", border: "1px solid var(--t-border-color-light)", borderRadius: "var(--t-border-radius-sm)", fontFamily: "monospace", fontSize: "12px", overflowWrap: "anywhere", padding: "10px", }, }; const Settings = () => { const [testing, setTesting] = useState(false); const [repairing, setRepairing] = useState(false); const webhookUrl = useMemo( () => new RestApiClient().resolveUrl("/s/webhook/sajn"), [], ); const testConnection = async () => { setTesting(true); try { const result = await new RestApiClient().get( "/s/sajn/templates", ); await enqueueSnackbar({ message: `Connected to sajn. ${result.templates?.length ?? 0} template(s) found.`, variant: "success", }); } catch (error) { await enqueueSnackbar({ message: error instanceof Error ? error.message : "Could not connect to sajn.", variant: "error", }); } finally { setTesting(false); } }; const repairWebhook = async () => { setRepairing(true); try { const result = await new RestApiClient().post( "/s/sajn/setup-webhook", {}, ); await enqueueSnackbar({ message: result.message, variant: result.success ? "success" : "error", }); } catch (error) { await enqueueSnackbar({ message: error instanceof Error ? error.message : "Could not configure the sajn webhook.", variant: "error", }); } finally { setRepairing(false); } }; return (

Connect sajn

Add a sajn connection in the Connections section. You will sign in to sajn and approve access; no API key is required. For shared CRM workflows, choose a workspace-shared connection.

Status synchronization

Twenty creates or updates the sajn webhook automatically after OAuth connection. The secret is generated internally and is never shown to workspace users.

{webhookUrl}
); }; export default defineSettingsFrontComponent({ universalIdentifier: SETTINGS_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, name: "sajn-settings", description: "Test the sajn OAuth connection and repair status synchronization.", component: Settings, });