├── .dockerignore ├── .env.example ├── .eslintignore ├── .eslintrc.yml ├── .github ├── dependabot.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── .mergify.yml ├── .prettierignore ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ConsoleHowTos ├── BuyVoicePhoneNumber │ ├── BuyVoicePhoneNumber.md │ └── screenshots │ │ ├── Click_Buy_Voice_Number.png │ │ ├── Phone_numbers_page.png │ │ ├── Review_Phone_Number.png │ │ ├── Search_for_voice_numbers.png │ │ └── Select_new_number_from_active_numbers_list.png ├── CreateAPIKey │ ├── CreateAPIKey.md │ └── screenshots │ │ ├── API_Key_and_Secret.png │ │ ├── API_Keys_Page.png │ │ └── Create_New_API_Key.png └── CreateNewTwiMLApp │ ├── CreateNewTwiMLApp.md │ └── screenshots │ ├── Create_new_TwiML_App.png │ ├── Find_your_TwiML_App_SID.png │ ├── Select_TwiML_App_from_list.png │ └── TwiML_Apps_Console.png ├── Dockerfile ├── LICENSE ├── README.md ├── config.js ├── docker-compose.yml ├── index.js ├── name_generator.js ├── package-lock.json ├── package.json ├── public ├── index.html ├── quickstart.js ├── site.css └── twilio.min.js ├── screenshots ├── BrowserToBrowserCall.png ├── ConfigurePhoneNumberWithTwiMLApp.png ├── InitializeDevice.png └── UpdateRequestURL.png ├── src ├── handler.js └── router.js └── tests └── handler.test.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 2 | 3 | # The Twilio number for making outbound and receiving inbound calls 4 | # Purchase a Twilio phone number in the console 5 | # https://www.twilio.com/console/phone-numbers/search 6 | TWILIO_CALLER_ID=+1XXXYYYZZZZ 7 | 8 | # SID of your TwiML Application 9 | # Create a new TwiML app in the console 10 | # https://www.twilio.com/console/voice/twiml/apps 11 | TWILIO_TWIML_APP_SID=APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 12 | 13 | # Your REST API Key information 14 | # Create a new key in the console (the TWILIO_API_KEY will be the SID shown starting with 'SK') 15 | # https://www.twilio.com/console/project/api-keys 16 | TWILIO_API_KEY=SKXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 17 | TWILIO_API_SECRET=XXXXXXXXXXXXXXXXX 18 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public/**/*.js 2 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: google 2 | parserOptions: 3 | ecmaVersion: 6 4 | rules: 5 | linebreak-style: 0 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js 5 | 6 | on: 7 | push: 8 | branches: [master, next] 9 | pull_request: 10 | branches: [master, next] 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.platform }} 15 | 16 | strategy: 17 | matrix: 18 | node-version: [10.x, 12.x] 19 | platform: [windows-latest, macos-latest, ubuntu-latest] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm test 29 | env: 30 | CI: true 31 | TWILIO_ACCOUNT_SID: ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 32 | TWILIO_TWIML_APP_SID: APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 33 | TWILIO_CALLER_ID: +1XXXYYYZZZZ 34 | TWILIO_API_KEY: SKXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 35 | TWILIO_API_SECRET: XXXXXXXXXXXXXXXXX 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | .env 5 | .tool-versions 6 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: automatic merge for Dependabot pull requests 3 | conditions: 4 | - author=dependabot-preview[bot] 5 | - status-success=build (10.x, macos-latest) 6 | - status-success=build (12.x, macos-latest) 7 | - status-success=build (10.x, windows-latest) 8 | - status-success=build (12.x, windows-latest) 9 | - status-success=build (10.x, ubuntu-latest) 10 | - status-success=build (12.x, ubuntu-latest) 11 | actions: 12 | merge: 13 | method: squash 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at open-source@twilio.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Twilio 2 | 3 | All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under. 4 | -------------------------------------------------------------------------------- /ConsoleHowTos/BuyVoicePhoneNumber/BuyVoicePhoneNumber.md: -------------------------------------------------------------------------------- 1 | # Buy a Voice Phone Number 2 | 3 | 1. Log in to your [Twilio Console](https://www.twilio.com/console) 4 | 5 | 2. Navigate to [Phone Numbers](https://www.twilio.com/console/phone-numbers/incoming) 6 | 7 | 3. Click on the 'Buy a number' button in the top right corner. 8 | 9 | ![screenshot of Phone Numbers landing screen](./screenshots/Phone_numbers_page.png) 10 | 11 | 4. On the 'Buy a Number' screen, select 'Voice' under capabilities, then click 'Search'. 12 | 13 | ![screenshot of Buy a Number screen](./screenshots/Search_for_voice_numbers.png) 14 | 15 | 5. Choose a phone number and click 'Buy'. 16 | 17 | ![screenshot of phone number search results list](./screenshots/Click_Buy_Voice_Number.png) 18 | 19 | 6. On the 'Review Phone Number' modal, click 'Buy (XXX) XXX-XXXX' button in bottom right. 20 | 21 | ![screenshot of Review Phone Number modal](./screenshots/Review_Phone_Number.png) 22 | -------------------------------------------------------------------------------- /ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Click_Buy_Voice_Number.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Click_Buy_Voice_Number.png -------------------------------------------------------------------------------- /ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Phone_numbers_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Phone_numbers_page.png -------------------------------------------------------------------------------- /ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Review_Phone_Number.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Review_Phone_Number.png -------------------------------------------------------------------------------- /ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Search_for_voice_numbers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Search_for_voice_numbers.png -------------------------------------------------------------------------------- /ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Select_new_number_from_active_numbers_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Select_new_number_from_active_numbers_list.png -------------------------------------------------------------------------------- /ConsoleHowTos/CreateAPIKey/CreateAPIKey.md: -------------------------------------------------------------------------------- 1 | # Create an API Key 2 | 3 | 1. Log in to your [Twilio Console](https://www.twilio.com/console) 4 | 5 | 2. Navigate to [Settings > API Keys](https://www.twilio.com/console/project/api-keys). 6 | 7 | 3. On the API Keys screen, click on the 'Create API Key' button in the top right corner. 8 | 9 | ![screenshot of "API Keys" page](./screenshots/API_Keys_Page.png) 10 | 11 | 4. Give your API Key a Friendly Name and click the 'Create API Key' Button. 12 | 13 | ![screenshot of "Create new API key" page](./screenshots/Create_New_API_Key.png) 14 | 15 | 5. On the 'Copy secret key' page, copy the SID and the Secret. The SID will be the `TWILIO_API_KEY` in your `.env` file. The Secret will be the `TWILIO_API_SECRET` in your `.env` file. You will only see the Secret on this screen, so you must copy it and keep it in a safe location. 16 | 17 | ![screenshot of "Copy secret key" page](./screenshots/API_Key_and_Secret.png) 18 | -------------------------------------------------------------------------------- /ConsoleHowTos/CreateAPIKey/screenshots/API_Key_and_Secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/CreateAPIKey/screenshots/API_Key_and_Secret.png -------------------------------------------------------------------------------- /ConsoleHowTos/CreateAPIKey/screenshots/API_Keys_Page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/CreateAPIKey/screenshots/API_Keys_Page.png -------------------------------------------------------------------------------- /ConsoleHowTos/CreateAPIKey/screenshots/Create_New_API_Key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/CreateAPIKey/screenshots/Create_New_API_Key.png -------------------------------------------------------------------------------- /ConsoleHowTos/CreateNewTwiMLApp/CreateNewTwiMLApp.md: -------------------------------------------------------------------------------- 1 | # Create a new TwiML App 2 | 3 | 1. Log in to your [Twilio Console](https://www.twilio.com/console) 4 | 5 | 2. Navigate to [Programmable Voice > TwiML > TwiML Apps](https://www.twilio.com/console/voice/twiml/apps) 6 | 7 | 3. Click on the "Create new TwiML App" button in the top right corner. 8 | 9 | ![screenshot of "TwiML Apps" page](./screenshots/TwiML_Apps_Console.png) 10 | 11 | 4. Give your TwiML App a friendly name and click 'Create'. 12 | 13 | ![screenshot of "Create a New TwiML App" page](./screenshots/Create_new_TwiML_App.png) 14 | 15 | 5. Click on your new TwiML App in the list of TwiML Apps. 16 | 17 | ![screenshot of TwiML App list](./screenshots/Select_TwiML_App_from_list.png) 18 | 19 | 6. Find your TwiML App's SID. You will need this to configure your project. 20 | 21 | ![screenshot of TwiML App information](./screenshots/Find_your_TwiML_App_SID.png) 22 | -------------------------------------------------------------------------------- /ConsoleHowTos/CreateNewTwiMLApp/screenshots/Create_new_TwiML_App.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/CreateNewTwiMLApp/screenshots/Create_new_TwiML_App.png -------------------------------------------------------------------------------- /ConsoleHowTos/CreateNewTwiMLApp/screenshots/Find_your_TwiML_App_SID.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/CreateNewTwiMLApp/screenshots/Find_your_TwiML_App_SID.png -------------------------------------------------------------------------------- /ConsoleHowTos/CreateNewTwiMLApp/screenshots/Select_TwiML_App_from_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/CreateNewTwiMLApp/screenshots/Select_TwiML_App_from_list.png -------------------------------------------------------------------------------- /ConsoleHowTos/CreateNewTwiMLApp/screenshots/TwiML_Apps_Console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/CreateNewTwiMLApp/screenshots/TwiML_Apps_Console.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 3000 12 | 13 | CMD [ "npm", "start" ] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Twilio Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Twilio 3 | 4 | 5 | # Twilio Voice JavaScript SDK Quickstart for Node.js 6 | 7 | ![](https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/workflows/Node.js/badge.svg) 8 | 9 | > This template is part of Twilio CodeExchange. If you encounter any issues with this code, please open an issue at [github.com/twilio-labs/code-exchange/issues](https://github.com/twilio-labs/code-exchange/issues). 10 | 11 | ## About 12 | 13 | This application should give you a ready-made starting point for writing your own voice apps with Twilio Voice JavaScript SDK 2.0 (Formerly known as Twilio Client). 14 | 15 | This application is built in Node. 16 | 17 | Implementations in other languages: 18 | 19 | | .NET | Java | Python | PHP | Ruby | 20 | | :---------- | :---------- | :---------------------------------------------------------------------------- | :---------- | :---------- | 21 | | [Done](https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-csharp) | [Done](https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-java)| [Done](https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-python) | [Done](https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-php) | [Done](https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-ruby) | 22 | 23 | ## Set Up 24 | 25 | ### Requirements 26 | 27 | - [Nodejs](https://nodejs.org/) version **14.0** or above. 28 | - [ngrok](https://ngrok.com/download) - this is used to expose your local development server to the internet. For more information, read [this Twilio blog post](https://www.twilio.com/blog/2015/09/6-awesome-reasons-to-use-ngrok-when-testing-webhooks.html). 29 | - A WebRTC enabled browser (Google Chrome or Mozilla Firefox are recommended). Edge and Internet Explorer will not work for testing. 30 | 31 | ### Create a TwiML Application, Purchase a Phone Number, Create an API Key 32 | 33 | 1. [Create a TwiML Application in the Twilio Console](https://www.twilio.com/console/voice/twiml/apps). Once you create the TwiML Application, click on it in your list of TwiML Apps to find the TwiML App SID. You will need this SID for your `.env` file. **Note:** You will need to configure the Voice "REQUEST URL" in your TwiML App later. 34 | 35 | - For detailed instructions with screenshots, see the [Create a TwiML App.md file](ConsoleHowTos/CreateNewTwiMLApp/CreateNewTwiMLApp.md) 36 | 37 | 2. [Purchase a Voice phone number](https://www.twilio.com/console/phone-numbers/incoming). You will need this phone number in [E.164 format](https://en.wikipedia.org/wiki/E.164) for your `.env` file. 38 | 39 | - For detailed instructions with screenshots, see the [Buy a Phone Number.md file](ConsoleHowTos/BuyVoicePhoneNumber/BuyVoicePhoneNumber.md) 40 | 41 | 3. [Create an API Key in the Twilio Console](https://www.twilio.com/console/project/api-keys). Keep the API Key SID and the API Secret in a safe place, since you will need them for your `.env` file. Your API KEY is needed to create an [Access Token](https://www.twilio.com/docs/iam/access-tokens). 42 | 43 | - For detailed instructions with screenshots, see the [Create an API Key.md file](ConsoleHowTos/CreateAPIKey/CreateAPIKey.md) 44 | 45 | ### Gather Config Values 46 | 47 | Before we begin local development, we need to collect all the config values we need to run the application. 48 | 49 | | Config Value | Description | 50 | | :------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 51 | | `TWILIO_ACCOUNT_SID` | Your primary Twilio account identifier - find this [in the console here](https://www.twilio.com/console). | 52 | | `TWILIO_TWIML_APP_SID` | The SID of the TwiML App you created in step 1 above. Find the SID [in the console here](https://www.twilio.com/console/voice/twiml/apps). | 53 | | `TWILIO_CALLER_ID` | Your Twilio phone number in [E.164 format](https://en.wikipedia.org/wiki/E.164) - you can [find your number here](https://www.twilio.com/console/phone-numbers/incoming) | 54 | | `TWILIO_API_KEY` / `TWILIO_API_SECRET` | The `TWILIO_API_KEY` is the API Key SID you created in step 3 above, and the `TWILIO_API_SECRET` is the secret associated with that key. | 55 | 56 | ### Local development 57 | 58 | 1. First clone this repository and cd into it: 59 | 60 | ```bash 61 | git clone https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-node.git 62 | cd voice-javascript-sdk-quickstart-node 63 | ``` 64 | 65 | 2. Create a configuration file for your application by copying the `.env.example` and edit the `.env` file with the configuration values from above. 66 | 67 | ```bash 68 | cp .env.example .env 69 | ``` 70 | 71 | 3. Install the dependencies. 72 | 73 | ```bash 74 | npm install 75 | ``` 76 | 77 | 4. Copy the `twilio.min.js` file from your `node_modules` to your `public` directory. Run the following from the root directory of your project: 78 | 79 | ```bash 80 | cp node_modules/@twilio/voice-sdk/dist/twilio.min.js public 81 | ``` 82 | **Note:** In order to keep this quickstart as simple as possible, this step is used to avoid any need for build tools like Webpack. 83 | 84 | 5. Launch local development web server. 85 | 86 | ```bash 87 | npm start 88 | ``` 89 | 90 | 6. Navigate to [http://localhost:3000](http://localhost:3000) in your browser. 91 | 92 | 7. Expose your application to the wider internet using `ngrok`. This step is **crucial** for the app to work as expected. 93 | 94 | ```bash 95 | ngrok http 3000 96 | ``` 97 | 98 | 8. `ngrok` will assign a unique URL to your tunnel. 99 | It might be something like `https://asdf456.ngrok.io`. You will need this to configure your TwiML app in the next step. 100 | 101 | 9. Configure your TwiML app 102 | 103 | - In the Twilio Console, navigate to [Programmable Voice > TwiML > TwiML Apps](https://www.twilio.com/console/voice/twiml/apps) 104 | - Select the TwiML App you created earlier 105 | - On your TwiML App's information page, find the 'Voice Configuration' section. 106 | - Change the Request URL to your ngrok url with `/voice` appended to the end. (E.g: `https://asdf456.ngrok.io/voice`) **Note:** You **must** use the https URL, otherwise some browsers will block 107 | microphone access. 108 | - Click the 'Save' button. 109 | 110 | ![screenshot of TwiML App Voice Configuration](./screenshots/UpdateRequestURL.png) 111 | 112 | You should now be ready to make and receive calls from your browser. 113 | 114 | ## Your Web Application 115 | 116 | When you navigate to `localhost:3000`, you should see the web application containing a 'Start up the Device' button. Click this button to initialize a `Twilio.Device`. 117 | 118 | ![screenshot of web app home page](./screenshots/InitializeDevice.png) 119 | 120 | When the `Twilio.Device` is initialized, you will be assigned a random "client name", which will appear in the 'Device Info' column on the left side of the page. This client name is used as the `identity` field when generating an Access Token for the `Twilio.Device`, and is also used to route SDK-to-SDK calls to the correct `Twilio.Device`. 121 | 122 | ### To make an outbound call to a phone number: 123 | 124 | - Under 'Make a Call', enter a phone number in [E.164 format](https://en.wikipedia.org/wiki/E.164) and press the 'Call' button 125 | 126 | ### To make a browser-to browser call: 127 | 128 | Open two browser windows to `localhost:3000` and click 'Start up the Device' button in both windows. You should see a different client name in each window. 129 | 130 | Enter one client's name in the other client's 'Make a Call' input and press the 'Call' button. 131 | 132 | ![screenshot of browser to browser call](./screenshots/BrowserToBrowserCall.png) 133 | 134 | ### Receiving Incoming Calls from a Non-Browser Device 135 | 136 | You will first need to configure your Twilio Voice Phone Number to use the TwiML App we created earlier. This tells Twilio how to handle an incoming call directed to your Twilio Voice Number. 137 | 138 | 1. Log in to your [Twilio Console](https://www.twilio.com/console) 139 | 2. Navigate to your [Active Numbers list](https://www.twilio.com/console/phone-numbers/incoming) 140 | 3. Click on the number you purchased earlier 141 | 4. Scroll down to find the 'Voice & Fax' section and look for 'CONFIGURE WITH' 142 | 5. Select 'TwiML' App 143 | 6. Under 'TWIML APP', choose the TwiML App you created earlier. 144 | 7. Click the 'Save' button at the bottom of the browser window. 145 | 146 | ![screenshot of phone number configuration](./screenshots/ConfigurePhoneNumberWithTwiMLApp.png) 147 | 148 | You can now call your Twilio Voice Phone Number from your cell or landline phone. 149 | 150 | **Note:** Since this is a quickstart with limited functionality, incoming calls will only be routed to your most recently-created `Twilio.Device`. 151 | 152 | ### Unknown Audio Devices 153 | 154 | If you see "Unknown Audio Output Device 1" in the "Ringtone" or "Speaker" devices lists, click the button below the boxes (Seeing "Unknown" Devices?) to have your browser identify your input and output devices. 155 | 156 | ### Docker 157 | 158 | If you have [Docker](https://www.docker.com/) already installed on your machine, you can use our `docker-compose.yml` to setup your project. 159 | 160 | 1. Make sure you have the project cloned. 161 | 2. Setup the `.env` file as outlined in the [Local Development](#local-development) steps. 162 | 3. Run `docker-compose up`. 163 | 4. Follow the steps in [Local Development](#local-development) on how to expose your port to Twilio using a tool like [ngrok](https://ngrok.com/) and configure the remaining parts of your application. 164 | 165 | ### Cloud deployment 166 | 167 | In addition to trying out this application locally, you can deploy it to a variety of host services. Heroku is one option, linked below. 168 | 169 | Please be aware that some of these services may charge you for the usage and/or might make the source code for this application visible to the public. When in doubt, research the respective hosting service first. 170 | 171 | | Service | | 172 | | :-------------------------------- | :---------------------------------------------------------------------------------- | 173 | | [Heroku](https://www.heroku.com/) | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) | 174 | 175 | ## Resources 176 | 177 | - The CodeExchange repository can be found [here](https://github.com/twilio-labs/code-exchange/). 178 | 179 | ## Contributing 180 | 181 | This template is open source and welcomes contributions. All contributions are subject to our [Code of Conduct](https://github.com/twilio-labs/.github/blob/master/CODE_OF_CONDUCT.md). 182 | 183 | ## License 184 | 185 | [MIT](http://www.opensource.org/licenses/mit-license.html) 186 | 187 | ## Disclaimer 188 | 189 | No warranty expressed or implied. Software is as is. 190 | 191 | [twilio]: https://www.twilio.com 192 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | const cfg = {}; 3 | 4 | if (process.env.NODE_ENV !== "test") { 5 | dotenv.config({ path: ".env" }); 6 | } else { 7 | dotenv.config({ path: ".env.example", silent: true }); 8 | } 9 | 10 | // HTTP Port to run our web application 11 | cfg.port = process.env.PORT || 3000; 12 | 13 | // Your Twilio account SID and auth token, both found at: 14 | // https://www.twilio.com/user/account 15 | // 16 | // A good practice is to store these string values as system environment 17 | // variables, and load them from there as we are doing below. Alternately, 18 | // you could hard code these values here as strings. 19 | cfg.accountSid = process.env.TWILIO_ACCOUNT_SID; 20 | 21 | cfg.twimlAppSid = process.env.TWILIO_TWIML_APP_SID; 22 | cfg.callerId = process.env.TWILIO_CALLER_ID; 23 | 24 | cfg.apiKey = process.env.TWILIO_API_KEY; 25 | cfg.apiSecret = process.env.TWILIO_API_SECRET; 26 | 27 | // Export configuration object 28 | module.exports = cfg; 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | app: 4 | restart: always 5 | build: . 6 | ports: 7 | - "3000:3000" 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | const path = require("path"); 3 | const express = require("express"); 4 | const bodyParser = require("body-parser"); 5 | 6 | const router = require("./src/router"); 7 | 8 | // Create Express webapp 9 | const app = express(); 10 | app.use(express.static(path.join(__dirname, "public"))); 11 | app.use(bodyParser.urlencoded({ extended: false })); 12 | app.use(bodyParser.json()); 13 | 14 | app.use(router); 15 | 16 | // Create http server and run it 17 | const server = http.createServer(app); 18 | const port = process.env.PORT || 3000; 19 | 20 | server.listen(port, function () { 21 | console.log("Express server running on *:" + port); 22 | }); 23 | -------------------------------------------------------------------------------- /name_generator.js: -------------------------------------------------------------------------------- 1 | const ADJECTIVES = [ 2 | "Awesome", 3 | "Bold", 4 | "Creative", 5 | "Dapper", 6 | "Eccentric", 7 | "Fiesty", 8 | "Golden", 9 | "Holy", 10 | "Ignominious", 11 | "Jolly", 12 | "Kindly", 13 | "Lucky", 14 | "Mushy", 15 | "Natural", 16 | "Oaken", 17 | "Precise", 18 | "Quiet", 19 | "Rowdy", 20 | "Sunny", 21 | "Tall", 22 | "Unique", 23 | "Vivid", 24 | "Wonderful", 25 | "Xtra", 26 | "Yawning", 27 | "Zesty", 28 | ]; 29 | 30 | const FIRST_NAMES = [ 31 | "Anna", 32 | "Bobby", 33 | "Cameron", 34 | "Danny", 35 | "Emmett", 36 | "Frida", 37 | "Gracie", 38 | "Hannah", 39 | "Isaac", 40 | "Jenova", 41 | "Kendra", 42 | "Lando", 43 | "Mufasa", 44 | "Nate", 45 | "Owen", 46 | "Penny", 47 | "Quincy", 48 | "Roddy", 49 | "Samantha", 50 | "Tammy", 51 | "Ulysses", 52 | "Victoria", 53 | "Wendy", 54 | "Xander", 55 | "Yolanda", 56 | "Zelda", 57 | ]; 58 | 59 | const LAST_NAMES = [ 60 | "Anchorage", 61 | "Berlin", 62 | "Cucamonga", 63 | "Davenport", 64 | "Essex", 65 | "Fresno", 66 | "Gunsight", 67 | "Hanover", 68 | "Indianapolis", 69 | "Jamestown", 70 | "Kane", 71 | "Liberty", 72 | "Minneapolis", 73 | "Nevis", 74 | "Oakland", 75 | "Portland", 76 | "Quantico", 77 | "Raleigh", 78 | "SaintPaul", 79 | "Tulsa", 80 | "Utica", 81 | "Vail", 82 | "Warsaw", 83 | "XiaoJin", 84 | "Yale", 85 | "Zimmerman", 86 | ]; 87 | 88 | const rand = (arr) => arr[Math.floor(Math.random() * arr.length)]; 89 | 90 | module.exports = () => rand(ADJECTIVES) + rand(FIRST_NAMES) + rand(LAST_NAMES); 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twilio-voice-javascript-sdk-quickstart-node", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Quick Start application template for Twilio Voice JavaScript SDK on Node.js", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "node index.js", 9 | "eslint": "./node_modules/.bin/eslint .", 10 | "test": "./node_modules/.bin/jest" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "github.com/TwilioDevEd/client-quickstart-node" 15 | }, 16 | "keywords": [ 17 | "twilio", 18 | "voip", 19 | "ip", 20 | "chat", 21 | "real", 22 | "time", 23 | "diggity" 24 | ], 25 | "author": "Twilio Developer Education", 26 | "license": "MIT", 27 | "engines": { 28 | "node": ">=10.x" 29 | }, 30 | "dependencies": { 31 | "@twilio/voice-sdk": "^2.2.0", 32 | "body-parser": "^1.19.0", 33 | "dotenv": "^10.0.0", 34 | "express": "^4.17.1", 35 | "twilio": "~3.64.0" 36 | }, 37 | "devDependencies": { 38 | "eslint": "^7.29.0", 39 | "eslint-config-google": "^0.14.0", 40 | "jest": "^27.0.5", 41 | "prettier": "2.3.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Twilio Voice JavaScript SDK Quickstart 5 | 6 | 7 | 8 |
9 |

Twilio Voice JavaScript SDK Quickstart

10 | 11 |
12 |
13 |
14 |

Your Device Info

15 |
16 |
17 | 18 | 19 | 20 |
22 | 23 |
24 |
25 |
26 |

Make a Call

27 |
28 |
29 | 32 | 33 | 34 |
35 | 36 |
37 |

Incoming Call Controls

38 |

39 | Incoming Call from 40 |

41 | 42 | 43 | 44 |
45 |
46 | 47 |
48 |

49 | 50 |
51 |
52 |
53 |
54 |
55 |

Event Log

56 |
57 |
58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /public/quickstart.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | const speakerDevices = document.getElementById("speaker-devices"); 3 | const ringtoneDevices = document.getElementById("ringtone-devices"); 4 | const outputVolumeBar = document.getElementById("output-volume"); 5 | const inputVolumeBar = document.getElementById("input-volume"); 6 | const volumeIndicators = document.getElementById("volume-indicators"); 7 | const callButton = document.getElementById("button-call"); 8 | const outgoingCallHangupButton = document.getElementById("button-hangup-outgoing"); 9 | const callControlsDiv = document.getElementById("call-controls"); 10 | const audioSelectionDiv = document.getElementById("output-selection"); 11 | const getAudioDevicesButton = document.getElementById("get-devices"); 12 | const logDiv = document.getElementById("log"); 13 | const incomingCallDiv = document.getElementById("incoming-call"); 14 | const incomingCallHangupButton = document.getElementById( 15 | "button-hangup-incoming" 16 | ); 17 | const incomingCallAcceptButton = document.getElementById( 18 | "button-accept-incoming" 19 | ); 20 | const incomingCallRejectButton = document.getElementById( 21 | "button-reject-incoming" 22 | ); 23 | const phoneNumberInput = document.getElementById("phone-number"); 24 | const incomingPhoneNumberEl = document.getElementById("incoming-number"); 25 | const startupButton = document.getElementById("startup-button"); 26 | 27 | let device; 28 | let token; 29 | 30 | // Event Listeners 31 | 32 | callButton.onclick = (e) => { 33 | e.preventDefault(); 34 | makeOutgoingCall(); 35 | }; 36 | getAudioDevicesButton.onclick = getAudioDevices; 37 | speakerDevices.addEventListener("change", updateOutputDevice); 38 | ringtoneDevices.addEventListener("change", updateRingtoneDevice); 39 | 40 | 41 | // SETUP STEP 1: 42 | // Browser client should be started after a user gesture 43 | // to avoid errors in the browser console re: AudioContext 44 | startupButton.addEventListener("click", startupClient); 45 | 46 | // SETUP STEP 2: Request an Access Token 47 | async function startupClient() { 48 | log("Requesting Access Token..."); 49 | 50 | try { 51 | const data = await $.getJSON("/token"); 52 | log("Got a token."); 53 | token = data.token; 54 | setClientNameUI(data.identity); 55 | intitializeDevice(); 56 | } catch (err) { 57 | console.log(err); 58 | log("An error occurred. See your browser console for more information."); 59 | } 60 | } 61 | 62 | // SETUP STEP 3: 63 | // Instantiate a new Twilio.Device 64 | function intitializeDevice() { 65 | logDiv.classList.remove("hide"); 66 | log("Initializing device"); 67 | device = new Twilio.Device(token, { 68 | logLevel:1, 69 | // Set Opus as our preferred codec. Opus generally performs better, requiring less bandwidth and 70 | // providing better audio quality in restrained network conditions. 71 | codecPreferences: ["opus", "pcmu"], 72 | }); 73 | 74 | addDeviceListeners(device); 75 | 76 | // Device must be registered in order to receive incoming calls 77 | device.register(); 78 | } 79 | 80 | // SETUP STEP 4: 81 | // Listen for Twilio.Device states 82 | function addDeviceListeners(device) { 83 | device.on("registered", function () { 84 | log("Twilio.Device Ready to make and receive calls!"); 85 | callControlsDiv.classList.remove("hide"); 86 | }); 87 | 88 | device.on("error", function (error) { 89 | log("Twilio.Device Error: " + error.message); 90 | }); 91 | 92 | device.on("incoming", handleIncomingCall); 93 | 94 | device.audio.on("deviceChange", updateAllAudioDevices.bind(device)); 95 | 96 | // Show audio selection UI if it is supported by the browser. 97 | if (device.audio.isOutputSelectionSupported) { 98 | audioSelectionDiv.classList.remove("hide"); 99 | } 100 | } 101 | 102 | // MAKE AN OUTGOING CALL 103 | 104 | async function makeOutgoingCall() { 105 | var params = { 106 | // get the phone number to call from the DOM 107 | To: phoneNumberInput.value, 108 | }; 109 | 110 | if (device) { 111 | log(`Attempting to call ${params.To} ...`); 112 | 113 | // Twilio.Device.connect() returns a Call object 114 | const call = await device.connect({ params }); 115 | 116 | // add listeners to the Call 117 | // "accepted" means the call has finished connecting and the state is now "open" 118 | call.on("accept", updateUIAcceptedOutgoingCall); 119 | call.on("disconnect", updateUIDisconnectedOutgoingCall); 120 | call.on("cancel", updateUIDisconnectedOutgoingCall); 121 | 122 | outgoingCallHangupButton.onclick = () => { 123 | log("Hanging up ..."); 124 | call.disconnect(); 125 | }; 126 | 127 | } else { 128 | log("Unable to make call."); 129 | } 130 | } 131 | 132 | function updateUIAcceptedOutgoingCall(call) { 133 | log("Call in progress ..."); 134 | callButton.disabled = true; 135 | outgoingCallHangupButton.classList.remove("hide"); 136 | volumeIndicators.classList.remove("hide"); 137 | bindVolumeIndicators(call); 138 | } 139 | 140 | function updateUIDisconnectedOutgoingCall() { 141 | log("Call disconnected."); 142 | callButton.disabled = false; 143 | outgoingCallHangupButton.classList.add("hide"); 144 | volumeIndicators.classList.add("hide"); 145 | } 146 | 147 | // HANDLE INCOMING CALL 148 | 149 | function handleIncomingCall(call) { 150 | log(`Incoming call from ${call.parameters.From}`); 151 | 152 | //show incoming call div and incoming phone number 153 | incomingCallDiv.classList.remove("hide"); 154 | incomingPhoneNumberEl.innerHTML = call.parameters.From; 155 | 156 | //add event listeners for Accept, Reject, and Hangup buttons 157 | incomingCallAcceptButton.onclick = () => { 158 | acceptIncomingCall(call); 159 | }; 160 | 161 | incomingCallRejectButton.onclick = () => { 162 | rejectIncomingCall(call); 163 | }; 164 | 165 | incomingCallHangupButton.onclick = () => { 166 | hangupIncomingCall(call); 167 | }; 168 | 169 | // add event listener to call object 170 | call.on("cancel", handleDisconnectedIncomingCall); 171 | call.on("disconnect", handleDisconnectedIncomingCall); 172 | call.on("reject", handleDisconnectedIncomingCall); 173 | } 174 | 175 | // ACCEPT INCOMING CALL 176 | 177 | function acceptIncomingCall(call) { 178 | call.accept(); 179 | 180 | //update UI 181 | log("Accepted incoming call."); 182 | incomingCallAcceptButton.classList.add("hide"); 183 | incomingCallRejectButton.classList.add("hide"); 184 | incomingCallHangupButton.classList.remove("hide"); 185 | } 186 | 187 | // REJECT INCOMING CALL 188 | 189 | function rejectIncomingCall(call) { 190 | call.reject(); 191 | log("Rejected incoming call"); 192 | resetIncomingCallUI(); 193 | } 194 | 195 | // HANG UP INCOMING CALL 196 | 197 | function hangupIncomingCall(call) { 198 | call.disconnect(); 199 | log("Hanging up incoming call"); 200 | resetIncomingCallUI(); 201 | } 202 | 203 | // HANDLE CANCELLED INCOMING CALL 204 | 205 | function handleDisconnectedIncomingCall() { 206 | log("Incoming call ended."); 207 | resetIncomingCallUI(); 208 | } 209 | 210 | // MISC USER INTERFACE 211 | 212 | // Activity log 213 | function log(message) { 214 | logDiv.innerHTML += `

>  ${message}

`; 215 | logDiv.scrollTop = logDiv.scrollHeight; 216 | } 217 | 218 | function setClientNameUI(clientName) { 219 | var div = document.getElementById("client-name"); 220 | div.innerHTML = `Your client name: ${clientName}`; 221 | } 222 | 223 | function resetIncomingCallUI() { 224 | incomingPhoneNumberEl.innerHTML = ""; 225 | incomingCallAcceptButton.classList.remove("hide"); 226 | incomingCallRejectButton.classList.remove("hide"); 227 | incomingCallHangupButton.classList.add("hide"); 228 | incomingCallDiv.classList.add("hide"); 229 | } 230 | 231 | // AUDIO CONTROLS 232 | 233 | async function getAudioDevices() { 234 | await navigator.mediaDevices.getUserMedia({ audio: true }); 235 | updateAllAudioDevices.bind(device); 236 | } 237 | 238 | function updateAllAudioDevices() { 239 | if (device) { 240 | updateDevices(speakerDevices, device.audio.speakerDevices.get()); 241 | updateDevices(ringtoneDevices, device.audio.ringtoneDevices.get()); 242 | } 243 | } 244 | 245 | function updateOutputDevice() { 246 | const selectedDevices = Array.from(speakerDevices.children) 247 | .filter((node) => node.selected) 248 | .map((node) => node.getAttribute("data-id")); 249 | 250 | device.audio.speakerDevices.set(selectedDevices); 251 | } 252 | 253 | function updateRingtoneDevice() { 254 | const selectedDevices = Array.from(ringtoneDevices.children) 255 | .filter((node) => node.selected) 256 | .map((node) => node.getAttribute("data-id")); 257 | 258 | device.audio.ringtoneDevices.set(selectedDevices); 259 | } 260 | 261 | function bindVolumeIndicators(call) { 262 | call.on("volume", function (inputVolume, outputVolume) { 263 | var inputColor = "red"; 264 | if (inputVolume < 0.5) { 265 | inputColor = "green"; 266 | } else if (inputVolume < 0.75) { 267 | inputColor = "yellow"; 268 | } 269 | 270 | inputVolumeBar.style.width = Math.floor(inputVolume * 300) + "px"; 271 | inputVolumeBar.style.background = inputColor; 272 | 273 | var outputColor = "red"; 274 | if (outputVolume < 0.5) { 275 | outputColor = "green"; 276 | } else if (outputVolume < 0.75) { 277 | outputColor = "yellow"; 278 | } 279 | 280 | outputVolumeBar.style.width = Math.floor(outputVolume * 300) + "px"; 281 | outputVolumeBar.style.background = outputColor; 282 | }); 283 | } 284 | 285 | // Update the available ringtone and speaker devices 286 | function updateDevices(selectEl, selectedDevices) { 287 | selectEl.innerHTML = ""; 288 | 289 | device.audio.availableOutputDevices.forEach(function (device, id) { 290 | var isActive = selectedDevices.size === 0 && id === "default"; 291 | selectedDevices.forEach(function (device) { 292 | if (device.deviceId === id) { 293 | isActive = true; 294 | } 295 | }); 296 | 297 | var option = document.createElement("option"); 298 | option.label = device.label; 299 | option.setAttribute("data-id", id); 300 | if (isActive) { 301 | option.setAttribute("selected", "selected"); 302 | } 303 | selectEl.appendChild(option); 304 | }); 305 | } 306 | }); 307 | -------------------------------------------------------------------------------- /public/site.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Share+Tech+Mono); 2 | 3 | body, 4 | p { 5 | padding: 0; 6 | margin: auto; 7 | font-family: Arial, Helvetica, sans-serif; 8 | } 9 | 10 | h1 { 11 | text-align: center; 12 | } 13 | 14 | h2 { 15 | margin-top: 0; 16 | border-bottom: 1px solid black; 17 | } 18 | 19 | button { 20 | margin-bottom: 10px; 21 | } 22 | 23 | label { 24 | text-align: left; 25 | font-size: 1.25em; 26 | color: #777776; 27 | display: block; 28 | } 29 | 30 | header { 31 | text-align: center; 32 | } 33 | 34 | main { 35 | padding: 3em; 36 | max-width: 1200px; 37 | margin: 0 auto; 38 | display: flex; 39 | justify-content: space-between; 40 | align-items: flex-start; 41 | } 42 | 43 | .left-column, 44 | .center-column, 45 | .right-column { 46 | width: 30%; 47 | min-width: 16em; 48 | margin: 0 1.5em; 49 | text-align: center; 50 | } 51 | 52 | /* Left Column */ 53 | #client-name { 54 | text-align: left; 55 | margin-bottom: 1em; 56 | font-family: "Helvetica Light", Helvetica, sans-serif; 57 | font-size: 1.25em; 58 | color: #777776; 59 | } 60 | 61 | select { 62 | width: 300px; 63 | height: 60px; 64 | margin-bottom: 10px; 65 | } 66 | 67 | /* Center Column */ 68 | input { 69 | font-family: Helvetica-LightOblique, Helvetica, sans-serif; 70 | font-style: oblique; 71 | font-size: 1em; 72 | width: 100%; 73 | height: 2.5em; 74 | padding: 0; 75 | display: block; 76 | margin: 10px 0; 77 | } 78 | 79 | div#volume-indicators { 80 | padding: 10px; 81 | margin-top: 20px; 82 | width: 500px; 83 | text-align: left; 84 | } 85 | 86 | div#volume-indicators > div { 87 | display: block; 88 | height: 20px; 89 | width: 0; 90 | } 91 | 92 | /* Right Column */ 93 | .right-column { 94 | padding: 0 1.5em; 95 | } 96 | 97 | #log { 98 | text-align: left; 99 | border: 1px solid #686865; 100 | padding: 10px; 101 | height: 9.5em; 102 | overflow-y: scroll; 103 | } 104 | 105 | .log-entry { 106 | color: #686865; 107 | font-family: "Share Tech Mono", "Courier New", Courier, fixed-width; 108 | font-size: 1.25em; 109 | line-height: 1.25em; 110 | margin-left: 1em; 111 | text-indent: -1.25em; 112 | width: 90%; 113 | } 114 | 115 | /* Other Styles */ 116 | .hide { 117 | position: absolute !important; 118 | top: -9999px !important; 119 | left: -9999px !important; 120 | } 121 | 122 | button:disabled { 123 | cursor: not-allowed; 124 | } 125 | -------------------------------------------------------------------------------- /screenshots/BrowserToBrowserCall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/screenshots/BrowserToBrowserCall.png -------------------------------------------------------------------------------- /screenshots/ConfigurePhoneNumberWithTwiMLApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/screenshots/ConfigurePhoneNumberWithTwiMLApp.png -------------------------------------------------------------------------------- /screenshots/InitializeDevice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/screenshots/InitializeDevice.png -------------------------------------------------------------------------------- /screenshots/UpdateRequestURL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/screenshots/UpdateRequestURL.png -------------------------------------------------------------------------------- /src/handler.js: -------------------------------------------------------------------------------- 1 | const VoiceResponse = require("twilio").twiml.VoiceResponse; 2 | const AccessToken = require("twilio").jwt.AccessToken; 3 | const VoiceGrant = AccessToken.VoiceGrant; 4 | 5 | const nameGenerator = require("../name_generator"); 6 | const config = require("../config"); 7 | 8 | var identity; 9 | 10 | exports.tokenGenerator = function tokenGenerator() { 11 | identity = nameGenerator(); 12 | 13 | const accessToken = new AccessToken( 14 | config.accountSid, 15 | config.apiKey, 16 | config.apiSecret 17 | ); 18 | accessToken.identity = identity; 19 | const grant = new VoiceGrant({ 20 | outgoingApplicationSid: config.twimlAppSid, 21 | incomingAllow: true, 22 | }); 23 | accessToken.addGrant(grant); 24 | 25 | // Include identity and token in a JSON response 26 | return { 27 | identity: identity, 28 | token: accessToken.toJwt(), 29 | }; 30 | }; 31 | 32 | exports.voiceResponse = function voiceResponse(requestBody) { 33 | const toNumberOrClientName = requestBody.To; 34 | const callerId = config.callerId; 35 | let twiml = new VoiceResponse(); 36 | 37 | // If the request to the /voice endpoint is TO your Twilio Number, 38 | // then it is an incoming call towards your Twilio.Device. 39 | if (toNumberOrClientName == callerId) { 40 | let dial = twiml.dial(); 41 | 42 | // This will connect the caller with your Twilio.Device/client 43 | dial.client(identity); 44 | 45 | } else if (requestBody.To) { 46 | // This is an outgoing call 47 | 48 | // set the callerId 49 | let dial = twiml.dial({ callerId }); 50 | 51 | // Check if the 'To' parameter is a Phone Number or Client Name 52 | // in order to use the appropriate TwiML noun 53 | const attr = isAValidPhoneNumber(toNumberOrClientName) 54 | ? "number" 55 | : "client"; 56 | dial[attr]({}, toNumberOrClientName); 57 | } else { 58 | twiml.say("Thanks for calling!"); 59 | } 60 | 61 | return twiml.toString(); 62 | }; 63 | 64 | /** 65 | * Checks if the given value is valid as phone number 66 | * @param {Number|String} number 67 | * @return {Boolean} 68 | */ 69 | function isAValidPhoneNumber(number) { 70 | return /^[\d\+\-\(\) ]+$/.test(number); 71 | } 72 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | const Router = require("express").Router; 2 | const { tokenGenerator, voiceResponse } = require("./handler"); 3 | 4 | const router = new Router(); 5 | 6 | router.get("/token", (req, res) => { 7 | res.send(tokenGenerator()); 8 | }); 9 | 10 | router.post("/voice", (req, res) => { 11 | res.set("Content-Type", "text/xml"); 12 | res.send(voiceResponse(req.body)); 13 | }); 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /tests/handler.test.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const { tokenGenerator, voiceResponse } = require("../src/handler"); 3 | 4 | const when = describe; 5 | 6 | describe("#tokenGenerator", () => { 7 | it("generates a new token", () => { 8 | const token = tokenGenerator(); 9 | const decoded = jwt.decode(token.token, { complete: true }); 10 | 11 | expect(decoded.payload).toHaveProperty("jti"); 12 | expect(decoded.payload).toHaveProperty("grants"); 13 | expect(decoded.payload.grants.identity).toBe(token.identity); 14 | }); 15 | }); 16 | 17 | describe("#voiceResponse", () => { 18 | when("receives an empty or no value value", () => { 19 | it("returns a goodbye message", () => { 20 | const twiml = voiceResponse(); 21 | const count = countWord(twiml); 22 | 23 | // TwiML Verbs 24 | expect(count("Say")).toBe(2); 25 | 26 | // TwiML content 27 | expect(twiml).toContain("Thanks for calling!"); 28 | }); 29 | }); 30 | 31 | when("receives a value as string", () => { 32 | it("returns a dial verb with the client attribute", () => { 33 | const toNumber = "BigBoss"; 34 | const twiml = voiceResponse(toNumber); 35 | const count = countWord(twiml); 36 | 37 | // TwiML Verbs 38 | expect(count("Dial")).toBe(2); 39 | expect(count("Client")).toBe(2); 40 | 41 | // TwiML options 42 | expect(twiml).toContain(toNumber); 43 | }); 44 | }); 45 | 46 | when("receives a valid phone number", () => { 47 | it("returns a dial verb with the number attribute", () => { 48 | const toNumber = "+1235555555"; 49 | const twiml = voiceResponse(toNumber); 50 | const count = countWord(twiml); 51 | 52 | // TwiML Verbs 53 | expect(count("Dial")).toBe(2); 54 | expect(count("Number")).toBe(2); 55 | 56 | // TwiML options 57 | expect(twiml).toContain(toNumber); 58 | }); 59 | }); 60 | }); 61 | 62 | /** 63 | * Counts how many times a word is repeated 64 | * @param {String} paragraph 65 | * @return {String[]} 66 | */ 67 | function countWord(paragraph) { 68 | return (word) => { 69 | const regex = new RegExp(`\<${word}[ | \/?\>]|\<\/${word}?\>`); 70 | return paragraph.split(regex).length - 1; 71 | }; 72 | } 73 | --------------------------------------------------------------------------------