feat: add vonage telephony (#35)

* refactor: telephony integration

* feat: add vonage telephony
This commit is contained in:
Sabiha Khan 2025-10-27 15:29:57 +05:30 committed by Sabiha Khan
parent 6503d806c5
commit 4cfdc3d420
39 changed files with 3382 additions and 335 deletions

View file

@ -25,10 +25,18 @@ import {
} from "@/components/ui/select";
import { useAuth } from "@/lib/auth";
// TODO: Make UI provider-agnostic
interface TelephonyConfigForm {
provider: string;
account_sid: string;
auth_token: string;
// Twilio fields
account_sid?: string;
auth_token?: string;
// Vonage fields
application_id?: string;
private_key?: string;
api_key?: string;
api_secret?: string;
// Common field
from_number: string;
}
@ -70,13 +78,26 @@ export default function ConfigureTelephonyPage() {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.error && response.data?.twilio) {
setHasExistingConfig(true);
// Masked values like "****************def0" from backend
setValue("account_sid", response.data.twilio.account_sid);
setValue("auth_token", response.data.twilio.auth_token);
if (response.data.twilio.from_numbers?.length > 0) {
setValue("from_number", response.data.twilio.from_numbers[0]);
if (!response.error) {
// Simple single provider config
if (response.data?.twilio) {
setHasExistingConfig(true);
setValue("provider", "twilio");
setValue("account_sid", response.data.twilio.account_sid);
setValue("auth_token", response.data.twilio.auth_token);
if (response.data.twilio.from_numbers?.length > 0) {
setValue("from_number", response.data.twilio.from_numbers[0]);
}
} else if (response.data?.vonage) {
setHasExistingConfig(true);
setValue("provider", "vonage");
setValue("application_id", response.data.vonage.application_id);
setValue("private_key", response.data.vonage.private_key);
setValue("api_key", response.data.vonage.api_key || "");
setValue("api_secret", response.data.vonage.api_secret || "");
if (response.data.vonage.from_numbers?.length > 0) {
setValue("from_number", response.data.vonage.from_numbers[0]);
}
}
}
} catch (error) {
@ -92,14 +113,26 @@ export default function ConfigureTelephonyPage() {
try {
const accessToken = await getAccessToken();
// Build the request body based on provider
let requestBody: any = {
provider: data.provider,
from_numbers: [data.from_number],
};
if (data.provider === "twilio") {
requestBody.account_sid = data.account_sid;
requestBody.auth_token = data.auth_token;
} else if (data.provider === "vonage") {
requestBody.application_id = data.application_id;
requestBody.private_key = data.private_key;
requestBody.api_key = data.api_key;
requestBody.api_secret = data.api_secret;
}
const response = await saveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPost({
headers: { Authorization: `Bearer ${accessToken}` },
body: {
provider: data.provider,
account_sid: data.account_sid,
auth_token: data.auth_token,
from_numbers: [data.from_number],
},
body: requestBody,
});
if (response.error) {
@ -177,8 +210,14 @@ export default function ConfigureTelephonyPage() {
</SelectTrigger>
<SelectContent>
<SelectItem value="twilio">Twilio</SelectItem>
<SelectItem value="vonage">Vonage</SelectItem>
</SelectContent>
</Select>
{hasExistingConfig && (
<p className="text-sm text-amber-600">
Switching providers will require entering new credentials
</p>
)}
</div>
{/* Twilio-specific fields */}
@ -249,6 +288,87 @@ export default function ConfigureTelephonyPage() {
</>
)}
{/* Vonage-specific fields */}
{selectedProvider === "vonage" && (
<>
<div className="space-y-2">
<Label htmlFor="application_id">Application ID</Label>
<Input
id="application_id"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
{...register("application_id", {
required: selectedProvider === "vonage" ? "Application ID is required" : false,
})}
/>
{errors.application_id && (
<p className="text-sm text-red-500">
{errors.application_id.message}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="private_key">Private Key</Label>
<textarea
id="private_key"
className="w-full min-h-[100px] px-3 py-2 text-sm border rounded-md"
placeholder="-----BEGIN PRIVATE KEY-----&#10;...&#10;-----END PRIVATE KEY-----"
{...register("private_key", {
required: selectedProvider === "vonage" && !hasExistingConfig
? "Private key is required"
: false,
})}
/>
{errors.private_key && (
<p className="text-sm text-red-500">
{errors.private_key.message}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="api_key">API Key (Optional)</Label>
<Input
id="api_key"
placeholder="Optional - for some operations"
{...register("api_key")}
/>
</div>
<div className="space-y-2">
<Label htmlFor="api_secret">API Secret (Optional)</Label>
<Input
id="api_secret"
type="password"
placeholder="Optional - for webhook verification"
{...register("api_secret")}
/>
</div>
<div className="space-y-2">
<Label htmlFor="from_number">From Phone Number</Label>
<Input
id="from_number"
autoComplete="tel"
placeholder="14155551234 (no + prefix for Vonage)"
{...register("from_number", {
required: "Phone number is required",
pattern: {
value: /^[1-9]\d{1,14}$/,
message:
"Enter a valid phone number without + prefix (e.g., 14155551234)",
},
})}
/>
{errors.from_number && (
<p className="text-sm text-red-500">
{errors.from_number.message}
</p>
)}
</div>
</>
)}
<div className="pt-4">
<Button
type="submit"

View file

@ -6,7 +6,7 @@ import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { PhoneInput } from 'react-international-phone';
import { getTelephonyConfigurationApiV1OrganizationsTelephonyConfigGet, initiateCallApiV1TwilioInitiateCallPost } from '@/client/sdk.gen';
import { getTelephonyConfigurationApiV1OrganizationsTelephonyConfigGet, initiateCallApiV1TelephonyInitiateCallPost } from '@/client/sdk.gen';
import { WorkflowError } from '@/client/types.gen';
import { FlowEdge, FlowNode } from "@/components/flow/types";
import { OnboardingTooltip } from '@/components/onboarding/OnboardingTooltip';
@ -117,7 +117,8 @@ const WorkflowHeader = ({ isDirty, workflowName, rfInstance, onRun, workflowId,
});
// If no configuration exists, show configure dialog
if (configResponse.error || !configResponse.data?.twilio) {
// Check if any telephony provider is configured (Twilio or Vonage)
if (configResponse.error || (!configResponse.data?.twilio && !configResponse.data?.vonage)) {
setConfigureDialogOpen(true);
return;
}
@ -151,8 +152,11 @@ const WorkflowHeader = ({ isDirty, workflowName, rfInstance, onRun, workflowId,
}
// Configuration exists, proceed with call initiation
const response = await initiateCallApiV1TwilioInitiateCallPost({
body: { workflow_id: workflowId },
const response = await initiateCallApiV1TelephonyInitiateCallPost({
body: {
workflow_id: workflowId,
phone_number: phoneNumber
},
headers: { 'Authorization': `Bearer ${accessToken}` },
});

View file

@ -1,9 +1,8 @@
// This file is auto-generated by @hey-api/openapi-ts
import { type ClientOptions as DefaultClientOptions, type Config, createClient, createConfig } from '@hey-api/client-fetch';
import { createClientConfig } from '../lib/apiClient';
import type { ClientOptions } from './types.gen';
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch';
import { createClientConfig } from '../lib/apiClient';
/**
* The `createClientConfig()` function will be called on client initialization
@ -17,4 +16,4 @@ export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> =
export const client = createClient(createClientConfig(createConfig<ClientOptions>({
baseUrl: 'http://127.0.0.1:8000'
})));
})));

View file

@ -1,3 +1,3 @@
// This file is auto-generated by @hey-api/openapi-ts
export * from './sdk.gen';
export * from './types.gen';
export * from './sdk.gen';

File diff suppressed because one or more lines are too long

View file

@ -261,11 +261,6 @@ export type ImpersonateResponse = {
access_token: string;
};
export type InitiateCallRequest = {
workflow_id: number;
workflow_run_id?: number | null;
};
export type IntegrationResponse = {
id: number;
integration_id: string;
@ -390,6 +385,7 @@ export type SuperuserWorkflowRunsListResponse = {
*/
export type TelephonyConfigurationResponse = {
twilio?: TwilioConfigurationResponse | null;
vonage?: VonageConfigurationResponse | null;
};
export type TestSessionResponse = {
@ -502,6 +498,45 @@ export type ValidationError = {
type: string;
};
/**
* Request schema for Vonage configuration.
*/
export type VonageConfigurationRequest = {
provider?: string;
/**
* Vonage API Key
*/
api_key?: string | null;
/**
* Vonage API Secret
*/
api_secret?: string | null;
/**
* Vonage Application ID
*/
application_id: string;
/**
* Private key for JWT generation
*/
private_key: string;
/**
* List of Vonage phone numbers (without + prefix)
*/
from_numbers: Array<string>;
};
/**
* Response schema for Vonage configuration with masked sensitive fields.
*/
export type VonageConfigurationResponse = {
provider: string;
application_id: string;
api_key: string | null;
api_secret: string | null;
private_key: string;
from_numbers: Array<string>;
};
export type WorkflowError = {
kind: ItemKind;
id: string | null;
@ -621,8 +656,110 @@ export type WorkflowTemplateResponse = {
created_at: string;
};
export type ApiRoutesTelephonyInitiateCallRequest = {
workflow_id: number;
workflow_run_id?: number | null;
phone_number?: string | null;
};
export type ApiRoutesTwilioInitiateCallRequest = {
workflow_id: number;
workflow_run_id?: number | null;
};
export type InitiateCallApiV1TelephonyInitiateCallPostData = {
body: ApiRoutesTelephonyInitiateCallRequest;
headers?: {
authorization?: string | null;
};
path?: never;
query?: never;
url: '/api/v1/telephony/initiate-call';
};
export type InitiateCallApiV1TelephonyInitiateCallPostErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type InitiateCallApiV1TelephonyInitiateCallPostError = InitiateCallApiV1TelephonyInitiateCallPostErrors[keyof InitiateCallApiV1TelephonyInitiateCallPostErrors];
export type InitiateCallApiV1TelephonyInitiateCallPostResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostData = {
body?: never;
headers?: {
'x-twilio-signature'?: string | null;
};
path: {
workflow_run_id: number;
};
query?: never;
url: '/api/v1/telephony/status-callback/{workflow_run_id}';
};
export type HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostError = HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostErrors[keyof HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostErrors];
export type HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type HandleVonageEventsApiV1TelephonyEventsWorkflowRunIdPostData = {
body?: never;
path: {
workflow_run_id: number;
};
query?: never;
url: '/api/v1/telephony/events/{workflow_run_id}';
};
export type HandleVonageEventsApiV1TelephonyEventsWorkflowRunIdPostErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type HandleVonageEventsApiV1TelephonyEventsWorkflowRunIdPostError = HandleVonageEventsApiV1TelephonyEventsWorkflowRunIdPostErrors[keyof HandleVonageEventsApiV1TelephonyEventsWorkflowRunIdPostErrors];
export type HandleVonageEventsApiV1TelephonyEventsWorkflowRunIdPostResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type InitiateCallApiV1TwilioInitiateCallPostData = {
body: InitiateCallRequest;
body: ApiRoutesTwilioInitiateCallRequest;
headers?: {
authorization?: string | null;
};
@ -1957,7 +2094,9 @@ export type GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData =
authorization?: string | null;
};
path?: never;
query?: never;
query?: {
provider?: string | null;
};
url: '/api/v1/organizations/telephony-config';
};
@ -1984,7 +2123,7 @@ export type GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetRespons
export type GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse = GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponses[keyof GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponses];
export type SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData = {
body: TwilioConfigurationRequest;
body: TwilioConfigurationRequest | VonageConfigurationRequest;
headers?: {
authorization?: string | null;
};
@ -2809,4 +2948,4 @@ export type HealthApiV1HealthGetResponses = {
export type ClientOptions = {
baseUrl: 'http://127.0.0.1:8000' | (string & {});
};
};