├── .env.example ├── .eslintrc.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json ├── public ├── dialer.css ├── dialer.jsx ├── flags │ ├── flags.css │ └── flags.png └── index.html └── spec └── index.spec.js /.env.example: -------------------------------------------------------------------------------- 1 | # Twilio API credentials 2 | # Found at https://www.twilio.com/user/account/settings 3 | TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXX 4 | TWILIO_AUTH_TOKEN=your-token 5 | 6 | # Twilio phone number 7 | # Purchase one at https://www.twilio.com/user/account/phone-numbers 8 | TWILIO_NUMBER=+15551234567 9 | 10 | # App SID 11 | # Found at https://www.twilio.com/console/voice/dev-tools/twiml-apps 12 | TWILIO_TWIML_APP_SID=APXXXXXXXXXXXXXXXXXXXXXXXXX 13 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: google 2 | parserOptions: 3 | ecmaVersion: 6 4 | rules: 5 | require-jsdoc: 0 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional eslint cache 40 | .eslintcache 41 | 42 | # Optional REPL history 43 | .node_repl_history 44 | 45 | # Output of 'npm pack' 46 | *.tgz 47 | 48 | # .env file 49 | .env -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | - '6.0.0' 5 | install: 6 | - npm install 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Twilio Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Twilio 3 | 4 | 5 | # Browser Dialer - React 6 | [![Build Status](https://travis-ci.org/TwilioDevEd/browser-dialer-react.svg?branch=master)](https://travis-ci.org/TwilioDevEd/browser-dialer-react) 7 | 8 | Learn to implement a browser dialer application using the Twilio.js library and React. 9 | 10 | [Read the full tutorial here!](https://www.twilio.com/docs/tutorials/walkthrough/browser-dialer/node/react) 11 | 12 | ### Prerequisites 13 | 14 | 1. [Node.js](http://nodejs.org/) (version 6 or higher) 15 | 1. A Twilio account with a verified [phone number](https://www.twilio.com/console/phone-numbers/incoming). (Get a 16 | [free account](https://www.twilio.com/try-twilio?utm_campaign=tutorials&utm_medium=readme) 17 | here.) If you are using a Twilio Trial Account, you can learn all about it 18 | [here](https://www.twilio.com/help/faq/twilio-basics/how-does-twilios-free-trial-work). 19 | 20 | 21 | ### Local Development 22 | 23 | 1. First clone this repository and `cd` into it. 24 | 25 | ``` 26 | $ git clone git@github.com:TwilioDevEd/browser-dialer-react.git 27 | $ cd browser-dialer-react 28 | ``` 29 | 30 | 1. Copy the sample configuration file and edit it to match your configuration. 31 | 32 | ```bash 33 | $ cp .env.example .env 34 | ``` 35 | 36 | You can find your `TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN` in your 37 | [Twilio Account Settings](https://www.twilio.com/user/account/settings). 38 | You will also need a `TWILIO_NUMBER`, which you may find [here](https://www.twilio.com/user/account/phone-numbers/incoming), and you may find your `TWILIO_TWIML_APP_SID` [here](https://www.twilio.com/console/voice/dev-tools/twiml-apps). 39 | 40 | 1. Install dependencies. 41 | 42 | ```bash 43 | $ npm install 44 | ``` 45 | 46 | 1. Run the application. 47 | 48 | ```bash 49 | $ npm start 50 | ``` 51 | 52 | 1. Expose the application to the wider Internet using [ngrok](https://ngrok.com/). 53 | 54 | ```bash 55 | $ ngrok http 3000 56 | ``` 57 | 58 | Once you have started ngrok, update your App voice URL 59 | setting to use your ngrok hostname. It will look something like 60 | this: 61 | 62 | ``` 63 | http:///voice 64 | ``` 65 | 66 | ## Meta 67 | 68 | * No warranty expressed or implied. Software is as is. Diggity. 69 | * [MIT License](http://www.opensource.org/licenses/mit-license.html) 70 | * Lovingly crafted by Twilio Developer Education. 71 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('dotenv-safe').load(); 4 | const http = require('http'); 5 | const express = require('express'); 6 | const {urlencoded} = require('body-parser'); 7 | const twilio = require('twilio'); 8 | const ClientCapability = twilio.jwt.ClientCapability; 9 | const VoiceResponse = twilio.twiml.VoiceResponse; 10 | 11 | let app = express(); 12 | app.use(express.static(__dirname + '/public')); 13 | app.use(urlencoded({extended: false})); 14 | 15 | // Generate a Twilio Client capability token 16 | app.get('/token', (request, response) => { 17 | const capability = new ClientCapability({ 18 | accountSid: process.env.TWILIO_ACCOUNT_SID, 19 | authToken: process.env.TWILIO_AUTH_TOKEN, 20 | }); 21 | 22 | capability.addScope( 23 | new ClientCapability.OutgoingClientScope({ 24 | applicationSid: process.env.TWILIO_TWIML_APP_SID}) 25 | ); 26 | 27 | const token = capability.toJwt(); 28 | 29 | // Include token in a JSON response 30 | response.send({ 31 | token: token, 32 | }); 33 | }); 34 | 35 | // Create TwiML for outbound calls 36 | app.post('/voice', (request, response) => { 37 | let voiceResponse = new VoiceResponse(); 38 | voiceResponse.dial({ 39 | callerId: process.env.TWILIO_NUMBER, 40 | }, request.body.number); 41 | response.type('text/xml'); 42 | response.send(voiceResponse.toString()); 43 | }); 44 | 45 | app.use((error, req, res, next) => { 46 | res.status(500) 47 | res.send('Server Error') 48 | console.error(error.stack) 49 | next(error) 50 | }) 51 | 52 | let server = http.createServer(app); 53 | let port = process.env.PORT || 3000; 54 | server.listen(port, () => { 55 | console.log(`Express Server listening on *:${port}`); 56 | }); 57 | 58 | module.exports = app; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser-dailer-react", 3 | "version": "1.0.0", 4 | "description": "A browser-based dialer UI powered by Twilio Client", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "./node_modules/.bin/eslint . && NODE_ENV=test TWILIO_ACCOUNT_SID=my-account-sid TWILIO_AUTH_TOKEN=my-auth-token TWILIO_NUMBER=my-twilio-number TWILIO_TWIML_APP_SID=app-sid ./node_modules/mocha/bin/mocha spec --recursive" 9 | }, 10 | "author": "Twilio DevEd", 11 | "license": "MIT", 12 | "dependencies": { 13 | "body-parser": "^1.15.2", 14 | "dotenv-safe": "^3.0.0", 15 | "express": "^4.14.0", 16 | "twilio": "~3.0.0" 17 | }, 18 | "devDependencies": { 19 | "chai": "^3.5.0", 20 | "cheerio": "^0.22.0", 21 | "eslint": "^3.19.0", 22 | "eslint-config-google": "^0.7.1", 23 | "mocha": "^3.1.0", 24 | "supertest": "^2.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /public/dialer.css: -------------------------------------------------------------------------------- 1 | textarea:hover, input:hover, textarea:active, input:active, textarea:focus, 2 | button:focus, button:active, button:hover, label:focus, .btn:active, 3 | .btn.active { 4 | outline:0px !important; 5 | -webkit-appearance:none; 6 | } 7 | 8 | .dropdown-menu li a { 9 | padding: 3px 10px; 10 | } 11 | 12 | .btn-circle { 13 | width: 49px; 14 | height: 49px; 15 | text-align: center; 16 | padding: 5px 0; 17 | font-size: 20px; 18 | line-height: 2.00; 19 | border-radius: 30px; 20 | } 21 | 22 | .controls, .keys { 23 | padding-top: 10px; 24 | } 25 | 26 | .controls { 27 | float: left; 28 | } 29 | 30 | .controls .btn { 31 | display: block; 32 | margin-bottom: 20px; 33 | } 34 | 35 | .keys { 36 | padding-left: 20px; 37 | float: right; 38 | text-align: center; 39 | color: #fff; 40 | } 41 | 42 | .key-row { 43 | margin-bottom: 20px; 44 | } 45 | 46 | .key-row .btn { 47 | font-weight: 100; 48 | } 49 | 50 | .key-row .btn span { 51 | display: block; 52 | padding-top: 2px; 53 | font-size: 10px; 54 | color: #787878; 55 | } 56 | 57 | .log { 58 | background-color: #ddd; 59 | clear: both; 60 | padding: 20px 10px; 61 | text-align: center; 62 | font-size: 12px; 63 | color: #787878; 64 | } 65 | 66 | p { 67 | font-size: 10px; 68 | color: #787878; 69 | font-style: italic; 70 | margin-top: 10px; 71 | } 72 | 73 | #dialer { 74 | font-family: Helvetica, sans-serif; 75 | margin: 20px auto; 76 | padding: 0; 77 | width: 240px; 78 | text-align: center; 79 | } -------------------------------------------------------------------------------- /public/dialer.jsx: -------------------------------------------------------------------------------- 1 | var NumberInputText = React.createClass({ 2 | render: function() { 3 | return ( 4 |
5 | 7 |
8 | ); 9 | } 10 | }); 11 | 12 | var CountrySelectBox = React.createClass({ 13 | render: function() { 14 | var self = this; 15 | 16 | var CountryOptions = self.props.countries.map(function(country) { 17 | var flagClass = 'flag flag-' + country.code; 18 | 19 | return ( 20 |
  • 21 | self.props.handleOnChange(country.cc)}> 22 |
    23 | { country.name } (+{ country.cc }) 24 |
    25 |
  • 26 | ); 27 | }); 28 | 29 | return ( 30 |
    31 | 36 | 39 |
    40 | ); 41 | } 42 | }); 43 | 44 | var LogBox = React.createClass({ 45 | render: function() { 46 | return ( 47 |
    48 |
    {this.props.text}
    49 |

    {this.props.smallText}

    50 |
    51 | ); 52 | } 53 | }); 54 | 55 | var CallButton = React.createClass({ 56 | render: function() { 57 | return ( 58 | 62 | ); 63 | } 64 | }); 65 | 66 | var MuteButton = React.createClass({ 67 | render: function() { 68 | return ( 69 | 72 | ); 73 | } 74 | }); 75 | 76 | var DTMFTone = React.createClass({ 77 | // Handle numeric buttons 78 | sendDigit(digit) { 79 | Twilio.Device.activeConnection().sendDigits(digit); 80 | }, 81 | 82 | render: function() { 83 | return ( 84 |
    85 |
    86 | 87 | 90 | 93 |
    94 |
    95 | 98 | 101 | 104 |
    105 |
    106 | 109 | 112 | 115 |
    116 |
    117 | 118 | 119 | 120 |
    121 |
    122 | ); 123 | } 124 | }); 125 | 126 | var DialerApp = React.createClass({ 127 | getInitialState() { 128 | return { 129 | muted: false, 130 | log: 'Connecting...', 131 | onPhone: false, 132 | countryCode: '1', 133 | currentNumber: '', 134 | isValidNumber: false, 135 | countries: [ 136 | { name: 'United States', cc: '1', code: 'us' }, 137 | { name: 'Great Britain', cc: '44', code: 'gb' }, 138 | { name: 'Colombia', cc: '57', code: 'co' }, 139 | { name: 'Ecuador', cc: '593', code: 'ec' }, 140 | { name: 'Estonia', cc: '372', code: 'ee' }, 141 | { name: 'Germany', cc: '49', code: 'de' }, 142 | { name: 'Hong Kong', cc: '852', code: 'hk' }, 143 | { name: 'Ireland', cc: '353', code: 'ie' }, 144 | { name: 'Singapore', cc: '65', code: 'sg' }, 145 | { name: 'Spain', cc: '34', code: 'es' }, 146 | { name: 'Brazil', cc: '55', code: 'br' }, 147 | ] 148 | } 149 | }, 150 | 151 | // Initialize after component creation 152 | componentDidMount() { 153 | var self = this; 154 | 155 | // Fetch Twilio capability token from our Node.js server 156 | $.getJSON('/token').done(function(data) { 157 | Twilio.Device.setup(data.token); 158 | }).fail(function(err) { 159 | console.log(err); 160 | self.setState({log: 'Could not fetch token, see console.log'}); 161 | }); 162 | 163 | // Configure event handlers for Twilio Device 164 | Twilio.Device.disconnect(function() { 165 | self.setState({ 166 | onPhone: false, 167 | log: 'Call ended.' 168 | }); 169 | }); 170 | 171 | Twilio.Device.ready(function() { 172 | self.log = 'Connected'; 173 | }); 174 | }, 175 | 176 | // Handle country code selection 177 | handleChangeCountryCode(countryCode) { 178 | this.setState({countryCode: countryCode}); 179 | }, 180 | 181 | // Handle number input 182 | handleChangeNumber(e) { 183 | this.setState({ 184 | currentNumber: e.target.value, 185 | isValidNumber: /^([0-9]|#|\*)+$/.test(e.target.value.replace(/[-()\s]/g,'')) 186 | }); 187 | }, 188 | 189 | // Handle muting 190 | handleToggleMute() { 191 | var muted = !this.state.muted; 192 | 193 | this.setState({muted: muted}); 194 | Twilio.Device.activeConnection().mute(muted); 195 | }, 196 | 197 | // Make an outbound call with the current number, 198 | // or hang up the current call 199 | handleToggleCall() { 200 | if (!this.state.onPhone) { 201 | this.setState({ 202 | muted: false, 203 | onPhone: true 204 | }) 205 | // make outbound call with current number 206 | var n = '+' + this.state.countryCode + this.state.currentNumber.replace(/\D/g, ''); 207 | Twilio.Device.connect({ number: n }); 208 | this.setState({log: 'Calling ' + n}) 209 | } else { 210 | // hang up call in progress 211 | Twilio.Device.disconnectAll(); 212 | } 213 | }, 214 | 215 | render: function() { 216 | var self = this; 217 | 218 | return ( 219 |
    220 |
    221 | 222 | 224 | 225 | 226 | 227 |
    228 | 229 |
    230 | 231 | 232 | 233 | { this.state.onPhone ? : null } 234 | 235 |
    236 | 237 | { this.state.onPhone ? : null } 238 | 239 | 240 | 241 |
    242 | ); 243 | } 244 | }); 245 | 246 | ReactDOM.render( 247 | , 248 | document.getElementById('dialer-app') 249 | ); 250 | -------------------------------------------------------------------------------- /public/flags/flags.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Generated with CSS Flag Sprite generator (https://www.flag-sprites.com/) 3 | */ 4 | .flag{display:inline-block;width:16px;height:11px;background:url(flags.png) no-repeat}.flag.flag-gw{background-position:-144px -55px}.flag.flag-gu{background-position:-128px -55px}.flag.flag-gt{background-position:-112px -55px}.flag.flag-gs{background-position:-96px -55px}.flag.flag-gr{background-position:-80px -55px}.flag.flag-gq{background-position:-64px -55px}.flag.flag-gp{background-position:-48px -55px}.flag.flag-gy{background-position:-160px -55px}.flag.flag-gg{background-position:-208px -44px}.flag.flag-gf{background-position:-192px -44px}.flag.flag-ge{background-position:-176px -44px}.flag.flag-gd{background-position:-160px -44px}.flag.flag-gb{background-position:-144px -44px}.flag.flag-ga{background-position:-128px -44px}.flag.flag-gn{background-position:-32px -55px}.flag.flag-gm{background-position:-16px -55px}.flag.flag-gl{background-position:0 -55px}.flag.flag-gi{background-position:-240px -44px}.flag.flag-gh{background-position:-224px -44px}.flag.flag-lb{background-position:-208px -77px}.flag.flag-lc{background-position:-224px -77px}.flag.flag-la{background-position:-192px -77px}.flag.flag-tv{background-position:-32px -154px}.flag.flag-tw{background-position:-48px -154px}.flag.flag-tt{background-position:-16px -154px}.flag.flag-tr{background-position:0 -154px}.flag.flag-lk{background-position:0 -88px}.flag.flag-li{background-position:-240px -77px}.flag.flag-lv{background-position:-80px -88px}.flag.flag-to{background-position:-240px -143px}.flag.flag-lt{background-position:-48px -88px}.flag.flag-lu{background-position:-64px -88px}.flag.flag-lr{background-position:-16px -88px}.flag.flag-ls{background-position:-32px -88px}.flag.flag-th{background-position:-128px -143px}.flag.flag-tf{background-position:-96px -143px}.flag.flag-tg{background-position:-112px -143px}.flag.flag-td{background-position:-80px -143px}.flag.flag-tc{background-position:-64px -143px}.flag.flag-ly{background-position:-96px -88px}.flag.flag-do{background-position:-112px -33px}.flag.flag-dm{background-position:-96px -33px}.flag.flag-dj{background-position:-64px -33px}.flag.flag-dk{background-position:-80px -33px}.flag.flag-um{background-position:-112px -154px}.flag.flag-de{background-position:-48px -33px}.flag.flag-ye{background-position:-96px -165px}.flag.flag-dz{background-position:-128px -33px}.flag.flag-uy{background-position:-144px -154px}.flag.flag-yt{background-position:-112px -165px}.flag.flag-catalonia{background-position:-32px -22px}.flag.flag-vu{background-position:-16px -165px}.flag.flag-qa{background-position:-128px -121px}.flag.flag-tm{background-position:-208px -143px}.flag.flag-england{background-position:-208px -33px}.flag.flag-eh{background-position:-192px -33px}.flag.flag-wf{background-position:-48px -165px}.flag.flag-ee{background-position:-160px -33px}.flag.flag-eg{background-position:-176px -33px}.flag.flag-za{background-position:-128px -165px}.flag.flag-ec{background-position:-144px -33px}.flag.flag-us{background-position:-128px -154px}.flag.flag-eu{background-position:-16px -44px}.flag.flag-et{background-position:0 -44px}.flag.flag-zw{background-position:-176px -165px}.flag.flag-es{background-position:-240px -33px}.flag.flag-er{background-position:-224px -33px}.flag.flag-ru{background-position:-192px -121px}.flag.flag-rw{background-position:-208px -121px}.flag.flag-rs{background-position:-176px -121px}.flag.flag-re{background-position:-144px -121px}.flag.flag-it{background-position:-176px -66px}.flag.flag-ro{background-position:-160px -121px}.flag.flag-tk{background-position:-176px -143px}.flag.flag-tz{background-position:-64px -154px}.flag.flag-bd{background-position:-16px -11px}.flag.flag-be{background-position:-32px -11px}.flag.flag-bf{background-position:-48px -11px}.flag.flag-bg{background-position:-64px -11px}.flag.flag-vg{background-position:-224px -154px}.flag.flag-ba{background-position:-240px 0}.flag.flag-bb{background-position:0 -11px}.flag.flag-tibet{background-position:-144px -143px}.flag.flag-bm{background-position:-112px -11px}.flag.flag-bn{background-position:-128px -11px}.flag.flag-bo{background-position:-144px -11px}.flag.flag-bh{background-position:-80px -11px}.flag.flag-bj{background-position:-96px -11px}.flag.flag-bt{background-position:-192px -11px}.flag.flag-jm{background-position:-208px -66px}.flag.flag-bv{background-position:-208px -11px}.flag.flag-bw{background-position:-224px -11px}.flag.flag-ws{background-position:-64px -165px}.flag.flag-br{background-position:-160px -11px}.flag.flag-bs{background-position:-176px -11px}.flag.flag-je{background-position:-192px -66px}.flag.flag-by{background-position:-240px -11px}.flag.flag-bz{background-position:0 -22px}.flag.flag-tn{background-position:-224px -143px}.flag.flag-om{background-position:-144px -110px}.flag.flag-zm{background-position:-160px -165px}.flag.flag-ua{background-position:-80px -154px}.flag.flag-jo{background-position:-224px -66px}.flag.flag-mz{background-position:-192px -99px}.flag.flag-ck{background-position:-128px -22px}.flag.flag-xk{background-position:-80px -165px}.flag.flag-ci{background-position:-112px -22px}.flag.flag-ch{background-position:-96px -22px}.flag.flag-co{background-position:-192px -22px}.flag.flag-cn{background-position:-176px -22px}.flag.flag-cm{background-position:-160px -22px}.flag.flag-cl{background-position:-144px -22px}.flag.flag-ca{background-position:-16px -22px}.flag.flag-cg{background-position:-80px -22px}.flag.flag-cf{background-position:-64px -22px}.flag.flag-cd{background-position:-48px -22px}.flag.flag-cz{background-position:-32px -33px}.flag.flag-cy{background-position:-16px -33px}.flag.flag-vc{background-position:-192px -154px}.flag.flag-cr{background-position:-208px -22px}.flag.flag-cw{background-position:0 -33px}.flag.flag-cv{background-position:-240px -22px}.flag.flag-cu{background-position:-224px -22px}.flag.flag-ve{background-position:-208px -154px}.flag.flag-pr{background-position:-48px -121px}.flag.flag-ps{background-position:-64px -121px}.flag.flag-pw{background-position:-96px -121px}.flag.flag-pt{background-position:-80px -121px}.flag.flag-py{background-position:-112px -121px}.flag.flag-tl{background-position:-192px -143px}.flag.flag-iq{background-position:-128px -66px}.flag.flag-pa{background-position:-160px -110px}.flag.flag-pf{background-position:-192px -110px}.flag.flag-pg{background-position:-208px -110px}.flag.flag-pe{background-position:-176px -110px}.flag.flag-pk{background-position:-240px -110px}.flag.flag-ph{background-position:-224px -110px}.flag.flag-pn{background-position:-32px -121px}.flag.flag-kurdistan{background-position:-128px -77px}.flag.flag-pl{background-position:0 -121px}.flag.flag-pm{background-position:-16px -121px}.flag.flag-hr{background-position:-224px -55px}.flag.flag-ht{background-position:-240px -55px}.flag.flag-hu{background-position:0 -66px}.flag.flag-hk{background-position:-176px -55px}.flag.flag-hn{background-position:-208px -55px}.flag.flag-vn{background-position:0 -165px}.flag.flag-hm{background-position:-192px -55px}.flag.flag-jp{background-position:-240px -66px}.flag.flag-wales{background-position:-32px -165px}.flag.flag-me{background-position:-160px -88px}.flag.flag-md{background-position:-144px -88px}.flag.flag-mg{background-position:-176px -88px}.flag.flag-ma{background-position:-112px -88px}.flag.flag-mc{background-position:-128px -88px}.flag.flag-uz{background-position:-160px -154px}.flag.flag-mm{background-position:-240px -88px}.flag.flag-ml{background-position:-224px -88px}.flag.flag-mo{background-position:-16px -99px}.flag.flag-mn{background-position:0 -99px}.flag.flag-mh{background-position:-192px -88px}.flag.flag-mk{background-position:-208px -88px}.flag.flag-mu{background-position:-112px -99px}.flag.flag-mt{background-position:-96px -99px}.flag.flag-mw{background-position:-144px -99px}.flag.flag-mv{background-position:-128px -99px}.flag.flag-mq{background-position:-48px -99px}.flag.flag-mp{background-position:-32px -99px}.flag.flag-ms{background-position:-80px -99px}.flag.flag-mr{background-position:-64px -99px}.flag.flag-im{background-position:-80px -66px}.flag.flag-ug{background-position:-96px -154px}.flag.flag-my{background-position:-176px -99px}.flag.flag-mx{background-position:-160px -99px}.flag.flag-il{background-position:-64px -66px}.flag.flag-va{background-position:-176px -154px}.flag.flag-sa{background-position:-224px -121px}.flag.flag-ae{background-position:-16px 0}.flag.flag-ad{background-position:0 0}.flag.flag-ag{background-position:-48px 0}.flag.flag-af{background-position:-32px 0}.flag.flag-ai{background-position:-64px 0}.flag.flag-vi{background-position:-240px -154px}.flag.flag-is{background-position:-160px -66px}.flag.flag-ir{background-position:-144px -66px}.flag.flag-am{background-position:-96px 0}.flag.flag-al{background-position:-80px 0}.flag.flag-ao{background-position:-128px 0}.flag.flag-an{background-position:-112px 0}.flag.flag-as{background-position:-160px 0}.flag.flag-ar{background-position:-144px 0}.flag.flag-au{background-position:-192px 0}.flag.flag-at{background-position:-176px 0}.flag.flag-aw{background-position:-208px 0}.flag.flag-in{background-position:-96px -66px}.flag.flag-ic{background-position:-16px -66px}.flag.flag-az{background-position:-224px 0}.flag.flag-ie{background-position:-48px -66px}.flag.flag-id{background-position:-32px -66px}.flag.flag-zanzibar{background-position:-144px -165px}.flag.flag-ni{background-position:-32px -110px}.flag.flag-nl{background-position:-48px -110px}.flag.flag-no{background-position:-64px -110px}.flag.flag-na{background-position:-208px -99px}.flag.flag-nc{background-position:-224px -99px}.flag.flag-scotland{background-position:-16px -132px}.flag.flag-ne{background-position:-240px -99px}.flag.flag-nf{background-position:0 -110px}.flag.flag-ng{background-position:-16px -110px}.flag.flag-nz{background-position:-128px -110px}.flag.flag-sh{background-position:-80px -132px}.flag.flag-np{background-position:-80px -110px}.flag.flag-so{background-position:-176px -132px}.flag.flag-nr{background-position:-96px -110px}.flag.flag-nu{background-position:-112px -110px}.flag.flag-somaliland{background-position:-192px -132px}.flag.flag-fr{background-position:-112px -44px}.flag.flag-io{background-position:-112px -66px}.flag.flag-sv{background-position:0 -143px}.flag.flag-sb{background-position:-240px -121px}.flag.flag-fi{background-position:-32px -44px}.flag.flag-fj{background-position:-48px -44px}.flag.flag-fk{background-position:-64px -44px}.flag.flag-fm{background-position:-80px -44px}.flag.flag-fo{background-position:-96px -44px}.flag.flag-tj{background-position:-160px -143px}.flag.flag-sz{background-position:-48px -143px}.flag.flag-sy{background-position:-32px -143px}.flag.flag-sx{background-position:-16px -143px}.flag.flag-kg{background-position:-16px -77px}.flag.flag-ke{background-position:0 -77px}.flag.flag-ss{background-position:-224px -132px}.flag.flag-sr{background-position:-208px -132px}.flag.flag-ki{background-position:-48px -77px}.flag.flag-kh{background-position:-32px -77px}.flag.flag-kn{background-position:-80px -77px}.flag.flag-km{background-position:-64px -77px}.flag.flag-st{background-position:-240px -132px}.flag.flag-sk{background-position:-112px -132px}.flag.flag-kr{background-position:-112px -77px}.flag.flag-si{background-position:-96px -132px}.flag.flag-kp{background-position:-96px -77px}.flag.flag-kw{background-position:-144px -77px}.flag.flag-sn{background-position:-160px -132px}.flag.flag-sm{background-position:-144px -132px}.flag.flag-sl{background-position:-128px -132px}.flag.flag-sc{background-position:0 -132px}.flag.flag-kz{background-position:-176px -77px}.flag.flag-ky{background-position:-160px -77px}.flag.flag-sg{background-position:-64px -132px}.flag.flag-se{background-position:-48px -132px}.flag.flag-sd{background-position:-32px -132px} -------------------------------------------------------------------------------- /public/flags/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/browser-dialer-react/304cd764e93ca38e5421ab468facd7a1a85a20ce/public/flags/flags.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Browser Dialer UI with React 6 | 8 | 10 | 11 | 12 | 13 | 14 |
    15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let expect = require('chai').expect; 4 | let supertest = require('supertest'); 5 | let cheerio = require('cheerio'); 6 | let app = require('../index.js'); 7 | 8 | describe('voice route', function() { 9 | describe('POST /voice/', function() { 10 | it('responds with twiml', function(done) { 11 | let testApp = supertest(app); 12 | testApp 13 | .post('/voice/') 14 | .expect(200) 15 | .end(function(err, res) { 16 | let $ = cheerio.load(res.text); 17 | expect($('dial').length).to.equal(1); 18 | expect($('dial').attr().callerid).to.equal('my-twilio-number'); 19 | done(); 20 | }); 21 | }); 22 | }); 23 | }); 24 | 25 | describe('index route', function() { 26 | describe('GET /', function() { 27 | it('responds with 200', function(done) { 28 | let testApp = supertest(app); 29 | testApp 30 | .get('/') 31 | .expect(200, done); 32 | }); 33 | }); 34 | }); 35 | 36 | describe('token route', function() { 37 | describe('GET /token', function() { 38 | it('responds with token', function(done) { 39 | let testApp = supertest(app); 40 | testApp 41 | .get('/token') 42 | .expect(200) 43 | .end(function(err, res) { 44 | const jsonResponse = JSON.parse(res.text); 45 | expect(jsonResponse.token.length).to.be.above(0); 46 | done(); 47 | }); 48 | }); 49 | }); 50 | }); 51 | --------------------------------------------------------------------------------