From 827978d18d39e296902dfd2c989d28009cf27cc4 Mon Sep 17 00:00:00 2001 From: spencerpincott Date: Thu, 6 Feb 2020 00:11:48 -0500 Subject: [PATCH] Add Twilio client wrapper and types Add voice state manager --- package.json | 1 + src/App.tsx | 34 +++++------ src/Hooks/voice-state.ts | 53 +++++++++++++++++ src/Wrappers/default-config.ts | 1 + src/Wrappers/voice.ts | 53 +++++++++++++++++ tsconfig.json | 3 +- types/twilio-client.d.ts | 103 +++++++++++++++++++++++++++++++++ 7 files changed, 229 insertions(+), 19 deletions(-) create mode 100644 src/Hooks/voice-state.ts create mode 100644 src/Wrappers/default-config.ts create mode 100644 src/Wrappers/voice.ts create mode 100644 types/twilio-client.d.ts diff --git a/package.json b/package.json index 2a4eb14..183b2bc 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "react": "^16.12.0", "react-dom": "^16.12.0", "react-scripts": "3.3.1", + "twilio-client": "^1.9.7", "typescript": "^3.7.5" }, "scripts": { diff --git a/src/App.tsx b/src/App.tsx index eb55739..6a3ef60 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,26 +1,24 @@ import React from 'react'; import logo from './logo.svg'; import './App.css'; +import useVoice from './Wrappers/voice'; +import { voiceToken } from './Wrappers/default-config'; const App = () => { - return ( -
-
- logo -

- Edit src/App.tsx and save to reload. -

- - Learn React - -
-
- ); + const { startDevice, answer, hangup, voiceState } = useVoice(); + + window.setTimeout(() => startDevice(voiceToken), 5000); + + return ( +
+
+ logo +

+ Device is currently {voiceState.deviceState}. +

+
+
+ ); } export default App; diff --git a/src/Hooks/voice-state.ts b/src/Hooks/voice-state.ts new file mode 100644 index 0000000..4aa27de --- /dev/null +++ b/src/Hooks/voice-state.ts @@ -0,0 +1,53 @@ +import { useState } from 'react' + +export type DeviceState = "Ringing" | "Connected" | "Muted" | "Disconnected" | "Offline"; + +export interface VoiceState +{ + deviceState: DeviceState; + callCreatedTime: number; + callEstablishedTime: number; + callPhoneNumber: string; +} + +export interface VoiceStateManager +{ + onDeviceStateUpdate(state: DeviceState, phoneNumber?: string): void; + voiceState: VoiceState; +} + +export default function useVoiceState(): VoiceStateManager +{ + const [deviceState, setDeviceState] = useState("Offline"); + const [callCreatedTime, setCallCreatedime] = useState(0); + const [callEstablishedTime, setCallEstablishedTime] = useState(0); + const [callPhoneNumber, setCallPhoneNumber] = useState(""); + + const onDeviceStateUpdate = (state: DeviceState, phoneNumber?: string) => + { + + if (state === "Ringing" || (state === "Connected" && deviceState === "Disconnected")) + { + setCallCreatedime(Date.now()); + } + + + if (state === "Connected" && (deviceState === "Ringing" || deviceState === "Disconnected")) + { + setCallEstablishedTime(Date.now()); + } + + setDeviceState(state); + if (phoneNumber) { setCallPhoneNumber(phoneNumber); } + } + + return { + onDeviceStateUpdate, + voiceState: { + deviceState, + callCreatedTime, + callEstablishedTime, + callPhoneNumber + } + } +} \ No newline at end of file diff --git a/src/Wrappers/default-config.ts b/src/Wrappers/default-config.ts new file mode 100644 index 0000000..6362fae --- /dev/null +++ b/src/Wrappers/default-config.ts @@ -0,0 +1 @@ +export const voiceToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InNjb3BlOmNsaWVudDppbmNvbWluZz9jbGllbnROYW1lPXNwZW5jZXIgc2NvcGU6Y2xpZW50Om91dGdvaW5nP2FwcFNpZD1BUGJmNzMyOTg4YjcxYTMzZDFiOGY1ZjY1MmI1NDU4MmQ0JmNsaWVudE5hbWU9c3BlbmNlciIsImlzcyI6IkFDOWQyYzcxOTc0NmIxZTg5MWU2ZjVlZmQwMmE5Yjg0MzAiLCJleHAiOjE1ODA5NjA0NTAsImlhdCI6MTU4MDk1Njg1MH0.LOlWmq52LjPLM4ONlMdnUybZf6NDk1Xeye-qjVRI_O4"; diff --git a/src/Wrappers/voice.ts b/src/Wrappers/voice.ts new file mode 100644 index 0000000..ad7ae97 --- /dev/null +++ b/src/Wrappers/voice.ts @@ -0,0 +1,53 @@ +import { Device, Connection } from "twilio-client"; +import useVoiceState, { VoiceState } from "../Hooks/voice-state"; + +export interface Voice +{ + startDevice(token: string): void; + answer(): void; + hangup(): void; + voiceState: VoiceState; +} + +export default function useVoice() +{ + const state = useVoiceState(); + let device: Device; + let connection: Connection; + + const startDevice = (token: string) => + { + device = new Device(token); + + device.on("ready", () => state.onDeviceStateUpdate("Disconnected")); + device.on("offline", () => state.onDeviceStateUpdate("Offline")); + device.on("disconnect", () => state.onDeviceStateUpdate("Disconnected")); + device.on("cancel", () => state.onDeviceStateUpdate("Disconnected")); + + device.on("incoming", (incomingConnection: Connection) => + { + state.onDeviceStateUpdate("Ringing", incomingConnection.parameters.From); + + incomingConnection.on("accept", () => state.onDeviceStateUpdate("Connected")); + incomingConnection.on("mute", muted => state.onDeviceStateUpdate(muted ? "Muted" : "Connected")); + connection = incomingConnection; + }); + } + + const answer = () => + { + connection.accept(); + } + + const hangup = () => + { + device.disconnectAll(); + } + + return { + startDevice, + answer, + hangup, + voiceState: state.voiceState, + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index f2850b7..2fa5bec 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,7 @@ "jsx": "react" }, "include": [ - "src" + "src", + "types" ] } diff --git a/types/twilio-client.d.ts b/types/twilio-client.d.ts new file mode 100644 index 0000000..840e9dd --- /dev/null +++ b/types/twilio-client.d.ts @@ -0,0 +1,103 @@ +declare module "twilio-client" +{ + interface SetupParams + { + allowIncomingWhileBusy: boolean; + audioConstraints: boolean; + backoffMaxMs: number; + codecPreferences: ("pcmu" | "opus")[]; + closeProtection: boolean; + debug: boolean; + dscp: boolean; + enableRingingState: boolean; + fakeLocalDTMF: boolean; + iceServers: string[]; + region: "au1" | "br1" | "ie1" | "de1" | "jp1" | "sg1" | "us1" | "us2" | "gll"; + rtcConfiguration: object; + sounds: object; + warnings: boolean; + } + + interface ConnectionParameters + { + CallSid: string; + AccountSid: string; + From: string; + To: string; + ApiVersion: string; + } + + type ConnectionStatus = "pending" | "connecting" | "ringing" | "open" | "closed"; + type ConnectionEvent = "accept" | "disconnect" | "error" | "mute" | "ringing" | "sample" | "volume" | "warning" | "warning-cleared"; + + interface Connection + { + parameters: ConnectionParameters; + customParameters: Map; + options: object; + accept(audioConstraints?: any): void; + reject(): void; + ignore(): void; + disconnect(): void; + mute(mute: boolean): void; + isMuted(): boolean; + getRemoteStream(): any; + sendDigits(digits: string): void; + status(): ConnectionStatus; + on(event: ConnectionEvent, handler: (...args: any[]) => void): void; + on(event: "mute", handler: (muted: boolean, connection: Connection) => void): void; + } + + interface MediaDevicesInfo + { + deviceId: string; + groupId: string; + kind: string; + label: string; + } + + interface AudioOutputCollection + { + get(): Set; + set(deviceId: string | string[]): void; + test(soundUrl: string): void; + } + + type AudioDeviceEvent = "deviceChange" | "inputVolume"; + + interface DeviceAudio + { + setAudioConstraints(audioConstraints: object): void; + setInputDevice(id: string): void; + unsetAudioContraints(): void; + unsetInputDevice(id: string): void; + on(event: AudioDeviceEvent, callback: (data: any) => void): void; + audioContraits: object; + + availableOutputDevices: Map; + availableInputDevices: Map; + inputDevice: MediaDevicesInfo; + speakerDevices: AudioOutputCollection; + ringtoneDevices: AudioOutputCollection; + + isOutputSelectionSupported: boolean; + } + + type DeviceStatus = "ready" | "offline" | "busy"; + type DeviceEvent = "cancel" | "connect" | "disconnect" | "error" | "incoming" | "offline" | "ready"; + + class Device + { + constructor(token: string, params?: SetupParams); + public setup(token: string, params?: SetupParams): void; + public connect(params: any, audioConstraints?: any): Connection; + public activeConnection(): Connection; + public destroy(): void; + public disconnectAll(): void; + public status(): DeviceStatus; + public on(event: DeviceEvent, handler: (...args: any[]) => void): void; + public audio: DeviceAudio; + } + + exports.Device = Device; +} \ No newline at end of file