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}` },
});