@zyratalk1/zyra-twilio-wrapper 1.2.8 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -79,6 +79,11 @@ interface WelcomeConfigResult {
79
79
  welcomeType: WelcomeType;
80
80
  routingConfigId: number;
81
81
  }
82
+ interface HoldMusicUrlConfigResult {
83
+ success: boolean;
84
+ message: string;
85
+ routingConfigId: number;
86
+ }
82
87
  /**
83
88
  * Routing Config
84
89
  */
@@ -366,6 +371,10 @@ declare class ReactNativeVoipSdk {
366
371
  deregisterWebhook(): Promise<CallResponse<null>>;
367
372
  isWebhookConfigured(): Promise<CallResponse<WebhookConfigResult>>;
368
373
  welcomeMessageConfig(welcomeType: "audio" | "tts", welcomeMessage: string): Promise<CallResponse<WelcomeConfigResult>>;
374
+ /**
375
+ * Set hold music audio URL (MP3 or WAV HTTPS URL, e.g. from upload-audio).
376
+ */
377
+ holdMusicUrlConfig(audioUrl: string): Promise<CallResponse<HoldMusicUrlConfigResult>>;
369
378
  destroy(): void;
370
379
  private ensureAuthenticated;
371
380
  }
package/dist/index.d.ts CHANGED
@@ -79,6 +79,11 @@ interface WelcomeConfigResult {
79
79
  welcomeType: WelcomeType;
80
80
  routingConfigId: number;
81
81
  }
82
+ interface HoldMusicUrlConfigResult {
83
+ success: boolean;
84
+ message: string;
85
+ routingConfigId: number;
86
+ }
82
87
  /**
83
88
  * Routing Config
84
89
  */
@@ -366,6 +371,10 @@ declare class ReactNativeVoipSdk {
366
371
  deregisterWebhook(): Promise<CallResponse<null>>;
367
372
  isWebhookConfigured(): Promise<CallResponse<WebhookConfigResult>>;
368
373
  welcomeMessageConfig(welcomeType: "audio" | "tts", welcomeMessage: string): Promise<CallResponse<WelcomeConfigResult>>;
374
+ /**
375
+ * Set hold music audio URL (MP3 or WAV HTTPS URL, e.g. from upload-audio).
376
+ */
377
+ holdMusicUrlConfig(audioUrl: string): Promise<CallResponse<HoldMusicUrlConfigResult>>;
369
378
  destroy(): void;
370
379
  private ensureAuthenticated;
371
380
  }
package/dist/index.js CHANGED
@@ -319,11 +319,13 @@ var TwilioVoiceAdapter = class {
319
319
  async endCall(input) {
320
320
  console.log("[VOIP SDK] endCall() invoked", { callId: input.callId });
321
321
  const tracked = this.calls.get(input.callId);
322
- if (!tracked) return { success: false, error: "Call not found" };
322
+ if (!tracked) {
323
+ console.log("[VOIP SDK] endCall() call already ended", { callId: input.callId });
324
+ return { success: true, data: {} };
325
+ }
323
326
  try {
324
- await tracked.call.disconnect();
325
327
  this.calls.delete(input.callId);
326
- this.emit("onCallEnded", { callId: input.callId });
328
+ await tracked.call.disconnect();
327
329
  console.log("[VOIP SDK] call ended", { callId: input.callId });
328
330
  return { success: true, data: {} };
329
331
  } catch (error) {
@@ -694,6 +696,10 @@ var TwilioVoiceAdapter = class {
694
696
  console.log("[VOIP DEBUG] call disconnected while held \u2014 keeping tracked entry", { callId });
695
697
  return;
696
698
  }
699
+ if (!heldEntry) {
700
+ console.log("[VOIP DEBUG] call disconnected but already removed", { callId });
701
+ return;
702
+ }
697
703
  this.calls.delete(callId);
698
704
  this.emit("onCallEnded", {
699
705
  callId,
@@ -1377,7 +1383,77 @@ async function isWebhookConfigured(deps) {
1377
1383
  }
1378
1384
  }
1379
1385
 
1386
+ // src/holdMusicUrlConfig.ts
1387
+ function isHttpUrl(value) {
1388
+ try {
1389
+ const url = new URL(value);
1390
+ return url.protocol === "http:" || url.protocol === "https:";
1391
+ } catch {
1392
+ return false;
1393
+ }
1394
+ }
1395
+ function validateHoldMusicUrlConfigInput(input) {
1396
+ const audioUrl = input.audioUrl?.trim() ?? "";
1397
+ if (!audioUrl) {
1398
+ throw new Error("audioUrl is required.");
1399
+ }
1400
+ if (!isHttpUrl(audioUrl)) {
1401
+ throw new Error(
1402
+ "audioUrl must be a valid URL (e.g. S3 URL from upload-audio with purpose hold)."
1403
+ );
1404
+ }
1405
+ try {
1406
+ const pathname = new URL(audioUrl).pathname;
1407
+ const ext = pathname.replace(/^.*\./, "").toLowerCase();
1408
+ if (ext !== "mp3" && ext !== "wav") {
1409
+ throw new Error(
1410
+ "Audio URL must point to an .mp3 or .wav file. Only MP3 and WAV formats are accepted."
1411
+ );
1412
+ }
1413
+ } catch (err) {
1414
+ if (err instanceof Error && err.message.startsWith("Audio URL")) throw err;
1415
+ throw new Error("audioUrl must be a valid URL with .mp3 or .wav file.");
1416
+ }
1417
+ return { audioUrl };
1418
+ }
1419
+ async function holdMusicUrlConfig(deps, input) {
1420
+ deps.ensureAuthenticated();
1421
+ try {
1422
+ const payload = validateHoldMusicUrlConfigInput(input);
1423
+ const res = await deps.axiosInstance.post(
1424
+ `${deps.serverUrl}/voipSdk.holdMusicUrlConfig`,
1425
+ payload,
1426
+ {
1427
+ headers: {
1428
+ Authorization: `Bearer ${deps.sdkToken}`
1429
+ }
1430
+ }
1431
+ );
1432
+ const data = res?.data?.result ?? res?.data ?? null;
1433
+ if (!data) {
1434
+ throw new Error("Invalid response from server");
1435
+ }
1436
+ return createSuccessResult(data.message || "Hold music URL updated successfully", {
1437
+ success: data.success,
1438
+ message: data.message,
1439
+ routingConfigId: data.routingConfigId
1440
+ });
1441
+ } catch (error) {
1442
+ return createErrorResult(
1443
+ error instanceof Error ? error : "Failed to upsert hold music configuration"
1444
+ );
1445
+ }
1446
+ }
1447
+
1380
1448
  // src/welcomeMessageConfig.ts
1449
+ var MAX_AUDIO_SIZE_BYTES = 2 * 1024 * 1024;
1450
+ var ALLOWED_AUDIO_EXTENSIONS = ["mp3", "wav"];
1451
+ var ALLOWED_AUDIO_MIME_TYPES = [
1452
+ "audio/mpeg",
1453
+ "audio/mp3",
1454
+ "audio/wav",
1455
+ "audio/x-wav"
1456
+ ];
1381
1457
  function isUrl(value) {
1382
1458
  try {
1383
1459
  const url = new URL(value);
@@ -1389,7 +1465,28 @@ function isUrl(value) {
1389
1465
  function isAudioLikeInput(value) {
1390
1466
  return value.startsWith("http://") || value.startsWith("https://") || value.startsWith("data:audio/");
1391
1467
  }
1468
+ function validateAudioFile(file) {
1469
+ const ext = file.name.split(".").pop()?.toLowerCase() ?? "";
1470
+ const mime = (file.type ?? "").toLowerCase();
1471
+ const extOk = ALLOWED_AUDIO_EXTENSIONS.includes(ext);
1472
+ const mimeOk = ALLOWED_AUDIO_MIME_TYPES.includes(mime) || extOk;
1473
+ if (!mimeOk) {
1474
+ return "Only .mp3 and .wav audio files are accepted.";
1475
+ }
1476
+ if (file.size !== void 0 && file.size > MAX_AUDIO_SIZE_BYTES) {
1477
+ const mb = (file.size / (1024 * 1024)).toFixed(1);
1478
+ return `File size is ${mb} MB \u2014 maximum allowed is 2 MB.`;
1479
+ }
1480
+ return null;
1481
+ }
1392
1482
  function validateWelcomeConfigInput(input) {
1483
+ if (input.audioFile) {
1484
+ const audioError = validateAudioFile(input.audioFile);
1485
+ if (audioError) {
1486
+ throw new Error(audioError);
1487
+ }
1488
+ return input;
1489
+ }
1393
1490
  const welcomeMessage = input.welcomeMessage?.trim();
1394
1491
  if (!welcomeMessage) {
1395
1492
  throw new Error("Payload is missing or empty. welcomeMessage is required.");
@@ -1413,10 +1510,50 @@ function validateWelcomeConfigInput(input) {
1413
1510
  welcomeMessage
1414
1511
  };
1415
1512
  }
1513
+ async function uploadAudioFile(deps, file, currentAudioUrl) {
1514
+ let uploadBase;
1515
+ try {
1516
+ uploadBase = new URL(deps.serverUrl).origin;
1517
+ } catch {
1518
+ uploadBase = deps.serverUrl.replace(/\/voipSdk.*$/, "").replace(/\/$/, "");
1519
+ }
1520
+ const uploadUrl = `${uploadBase}/api/voip/upload-audio`;
1521
+ const formData = new FormData();
1522
+ formData.append("file", {
1523
+ uri: file.uri,
1524
+ name: file.name,
1525
+ type: file.type || "audio/mpeg"
1526
+ });
1527
+ if (currentAudioUrl) {
1528
+ formData.append("currentUrl", currentAudioUrl);
1529
+ }
1530
+ const response = await fetch(uploadUrl, {
1531
+ method: "POST",
1532
+ headers: {
1533
+ Authorization: `Bearer ${deps.sdkToken}`
1534
+ },
1535
+ body: formData
1536
+ });
1537
+ const json = await response.json();
1538
+ if (!response.ok || !json.success) {
1539
+ throw new Error(json.message ?? `Audio upload failed with status ${response.status}`);
1540
+ }
1541
+ if (!json.url) {
1542
+ throw new Error("Server did not return an audio URL after upload.");
1543
+ }
1544
+ return json.url;
1545
+ }
1416
1546
  async function welcomeMessageConfig(deps, input) {
1417
1547
  deps.ensureAuthenticated();
1418
1548
  try {
1419
- const payload = validateWelcomeConfigInput(input);
1549
+ validateWelcomeConfigInput(input);
1550
+ let welcomeType = input.welcomeType;
1551
+ let welcomeMessage = input.welcomeMessage?.trim() ?? "";
1552
+ if (input.audioFile) {
1553
+ welcomeMessage = await uploadAudioFile(deps, input.audioFile);
1554
+ welcomeType = "audio";
1555
+ }
1556
+ const payload = { welcomeType, welcomeMessage };
1420
1557
  const res = await deps.axiosInstance.post(
1421
1558
  `${deps.serverUrl}/voipSdk.welcomeMessageConfig`,
1422
1559
  payload,
@@ -1477,8 +1614,11 @@ var ConfigService = class {
1477
1614
  async isWebhookConfigured() {
1478
1615
  return isWebhookConfigured(this.withDeps());
1479
1616
  }
1480
- async welcomeMessageConfig(welcomeType, welcomeMessage) {
1481
- return welcomeMessageConfig(this.withDeps(), { welcomeType, welcomeMessage });
1617
+ async welcomeMessageConfig(welcomeType, welcomeMessage, audioFile) {
1618
+ return welcomeMessageConfig(this.withDeps(), { welcomeType, welcomeMessage, audioFile });
1619
+ }
1620
+ async holdMusicUrlConfig(audioUrl) {
1621
+ return holdMusicUrlConfig(this.withDeps(), { audioUrl });
1482
1622
  }
1483
1623
  async getVoipProviderConfigStatus() {
1484
1624
  return getVoipProviderConfigStatus(this.withDeps());
@@ -2118,6 +2258,12 @@ var ReactNativeVoipSdk = class {
2118
2258
  async welcomeMessageConfig(welcomeType, welcomeMessage) {
2119
2259
  return this.configService.welcomeMessageConfig(welcomeType, welcomeMessage);
2120
2260
  }
2261
+ /**
2262
+ * Set hold music audio URL (MP3 or WAV HTTPS URL, e.g. from upload-audio).
2263
+ */
2264
+ async holdMusicUrlConfig(audioUrl) {
2265
+ return this.configService.holdMusicUrlConfig(audioUrl);
2266
+ }
2121
2267
  destroy() {
2122
2268
  logVoipDebug("destroy() invoked");
2123
2269
  this.authManager.destroy();