├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── example ├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── README.md ├── build │ ├── app.js │ └── index.html ├── config.sample.js ├── gulpfile.js ├── package.json ├── server.js └── src │ ├── app │ ├── app.js │ ├── myapp.jsx │ └── reducers.js │ └── www │ └── index.html ├── gulp ├── config.js └── tasks │ ├── build.js │ ├── lint.js │ ├── spec-watch.js │ └── spec.js ├── gulpfile.js ├── package.json ├── spec ├── middleware.spec.js └── util │ └── mock.js └── src ├── actions.js ├── constants.js ├── index.js └── middleware.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "mocha": true, 6 | "es6": true 7 | }, 8 | 9 | "ecmaFeatures": { 10 | "arrowFunctions": true, 11 | "binaryLiterals": true, 12 | "blockBindings": true, 13 | "classes": true, 14 | "defaultParams": true, 15 | "destructuring": true, 16 | "forOf": true, 17 | "modules": true, 18 | "objectLiteralComputedProperties": true, 19 | "objectLiteralDuplicateProperties": true, 20 | "objectLiteralShorthandMethods": true, 21 | "objectLiteralShorthandProperties": true, 22 | "octalLiterals": true, 23 | "regexUFlag": true, 24 | "regexYFlag": true, 25 | "spread": true, 26 | "superInFunctions": true, 27 | "templateStrings": true, 28 | "unicodeCodePointEscapes": true, 29 | "globalReturn": true 30 | }, 31 | 32 | "rules": { 33 | "no-alert": 0, 34 | "no-array-constructor": 0, 35 | "no-arrow-condition": 0, 36 | "no-bitwise": 0, 37 | "no-caller": 0, 38 | "no-case-declarations": 0, 39 | "no-catch-shadow": 0, 40 | "no-class-assign": 0, 41 | "no-cond-assign": 2, 42 | "no-console": 2, 43 | "no-const-assign": 0, 44 | "no-constant-condition": 2, 45 | "no-continue": 0, 46 | "no-control-regex": 2, 47 | "no-debugger": 2, 48 | "no-delete-var": 2, 49 | "no-div-regex": 0, 50 | "no-dupe-class-members": 0, 51 | "no-dupe-keys": 2, 52 | "no-dupe-args": 2, 53 | "no-duplicate-case": 2, 54 | "no-else-return": 0, 55 | "no-empty": 2, 56 | "no-empty-character-class": 2, 57 | "no-empty-label": 0, 58 | "no-empty-pattern": 0, 59 | "no-eq-null": 0, 60 | "no-eval": 0, 61 | "no-ex-assign": 2, 62 | "no-extend-native": 0, 63 | "no-extra-bind": 0, 64 | "no-extra-boolean-cast": 2, 65 | "no-extra-parens": 0, 66 | "no-extra-semi": 2, 67 | "no-fallthrough": 2, 68 | "no-floating-decimal": 0, 69 | "no-func-assign": 2, 70 | "no-implicit-coercion": 0, 71 | "no-implied-eval": 0, 72 | "no-inline-comments": 0, 73 | "no-inner-declarations": [2, "functions"], 74 | "no-invalid-regexp": 2, 75 | "no-invalid-this": 0, 76 | "no-irregular-whitespace": 2, 77 | "no-iterator": 0, 78 | "no-label-var": 0, 79 | "no-labels": 0, 80 | "no-lone-blocks": 0, 81 | "no-lonely-if": 0, 82 | "no-loop-func": 0, 83 | "no-mixed-requires": [0, false], 84 | "no-mixed-spaces-and-tabs": [2, false], 85 | "linebreak-style": [0, "unix"], 86 | "no-multi-spaces": 0, 87 | "no-multi-str": 0, 88 | "no-multiple-empty-lines": [0, {"max": 2}], 89 | "no-native-reassign": 0, 90 | "no-negated-condition": 0, 91 | "no-negated-in-lhs": 2, 92 | "no-nested-ternary": 0, 93 | "no-new": 0, 94 | "no-new-func": 0, 95 | "no-new-object": 0, 96 | "no-new-require": 0, 97 | "no-new-wrappers": 0, 98 | "no-obj-calls": 2, 99 | "no-octal": 2, 100 | "no-octal-escape": 0, 101 | "no-param-reassign": 0, 102 | "no-path-concat": 0, 103 | "no-plusplus": 0, 104 | "no-process-env": 0, 105 | "no-process-exit": 0, 106 | "no-proto": 0, 107 | "no-redeclare": 2, 108 | "no-regex-spaces": 2, 109 | "no-restricted-imports": 0, 110 | "no-restricted-modules": 0, 111 | "no-restricted-syntax": 0, 112 | "no-return-assign": 0, 113 | "no-script-url": 0, 114 | "no-self-compare": 0, 115 | "no-sequences": 0, 116 | "no-shadow": 0, 117 | "no-shadow-restricted-names": 0, 118 | "no-spaced-func": 0, 119 | "no-sparse-arrays": 2, 120 | "no-sync": 0, 121 | "no-ternary": 0, 122 | "no-trailing-spaces": 0, 123 | "no-this-before-super": 0, 124 | "no-throw-literal": 0, 125 | "no-undef": 2, 126 | "no-undef-init": 0, 127 | "no-undefined": 0, 128 | "no-unexpected-multiline": 0, 129 | "no-underscore-dangle": 0, 130 | "no-unneeded-ternary": 0, 131 | "no-unreachable": 2, 132 | "no-unused-expressions": 0, 133 | "no-unused-vars": [2, {"vars": "all", "args": "none"}], 134 | "no-use-before-define": 0, 135 | "no-useless-call": 0, 136 | "no-useless-concat": 0, 137 | "no-void": 0, 138 | "no-var": 0, 139 | "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], 140 | "no-with": 0, 141 | "no-magic-numbers": 0, 142 | 143 | "array-bracket-spacing": [0, "never"], 144 | "arrow-body-style": [0, "as-needed"], 145 | "arrow-parens": 0, 146 | "arrow-spacing": 0, 147 | "accessor-pairs": 0, 148 | "block-scoped-var": 0, 149 | "block-spacing": 0, 150 | "brace-style": [0, "1tbs"], 151 | "callback-return": 0, 152 | "camelcase": 0, 153 | "comma-dangle": [2, "never"], 154 | "comma-spacing": 0, 155 | "comma-style": 0, 156 | "complexity": [0, 11], 157 | "computed-property-spacing": [0, "never"], 158 | "consistent-return": 0, 159 | "consistent-this": [0, "that"], 160 | "constructor-super": 0, 161 | "curly": [0, "all"], 162 | "default-case": 0, 163 | "dot-location": 0, 164 | "dot-notation": [0, { "allowKeywords": true }], 165 | "eol-last": 0, 166 | "eqeqeq": 0, 167 | "func-names": 0, 168 | "func-style": [0, "declaration"], 169 | "generator-star-spacing": 0, 170 | "global-require": 0, 171 | "guard-for-in": 0, 172 | "handle-callback-err": 0, 173 | "id-length": 0, 174 | "indent": 0, 175 | "init-declarations": 0, 176 | "jsx-quotes": [0, "prefer-double"], 177 | "key-spacing": [0, { "beforeColon": false, "afterColon": true }], 178 | "lines-around-comment": 0, 179 | "max-depth": [0, 4], 180 | "max-len": [0, 80, 4], 181 | "max-nested-callbacks": [0, 2], 182 | "max-params": [0, 3], 183 | "max-statements": [0, 10], 184 | "new-cap": 0, 185 | "new-parens": 0, 186 | "newline-after-var": 0, 187 | "object-curly-spacing": [0, "never"], 188 | "object-shorthand": 0, 189 | "one-var": [0, "always"], 190 | "operator-assignment": [0, "always"], 191 | "operator-linebreak": 0, 192 | "padded-blocks": 0, 193 | "prefer-arrow-callback": 0, 194 | "prefer-const": 0, 195 | "prefer-reflect": 0, 196 | "prefer-rest-params": 0, 197 | "prefer-spread": 0, 198 | "prefer-template": 0, 199 | "quote-props": 0, 200 | "quotes": [0, "double"], 201 | "radix": 0, 202 | "id-match": 0, 203 | "require-jsdoc": 0, 204 | "require-yield": 0, 205 | "semi": 0, 206 | "semi-spacing": [0, {"before": false, "after": true}], 207 | "sort-vars": 0, 208 | "space-after-keywords": [0, "always"], 209 | "space-before-keywords": [0, "always"], 210 | "space-before-blocks": [0, "always"], 211 | "space-before-function-paren": [0, "always"], 212 | "space-in-parens": [0, "never"], 213 | "space-infix-ops": 0, 214 | "space-return-throw-case": 0, 215 | "space-unary-ops": [0, { "words": true, "nonwords": false }], 216 | "spaced-comment": 0, 217 | "strict": 0, 218 | "use-isnan": 2, 219 | "valid-jsdoc": 0, 220 | "valid-typeof": 2, 221 | "vars-on-top": 0, 222 | "wrap-iife": 0, 223 | "wrap-regex": 0, 224 | "yield-star-spacing": 0, 225 | "yoda": [0, "never"] 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | example 3 | gulp 4 | spec 5 | gulpfile.js 6 | .eslintrc 7 | .editorconfig 8 | .babelrc 9 | .travis.yml 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5.0.0" 4 | after_script: NODE_ENV=test istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Nuno Rosa 2 | 3 | 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 | 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twiliojs Redux 2 | 3 | [Twilio's](https://www.twilio.com/docs/api/client) javascript SDK [middleware](http://rackt.github.io/redux/docs/advanced/Middleware.html) for Redux. 4 | 5 | ``` 6 | npm install twiliojs-redux --save 7 | ``` 8 | 9 | [![NPM](https://nodei.co/npm/twiliojs-redux.png?downloads=true)](https://nodei.co/npm/twiliojs-redux/) 10 | 11 | [![Build Status](https://travis-ci.org/yarcub/twiliojs-redux.svg?branch=master)](https://travis-ci.org/yarcub/twiliojs-redux) 12 | 13 | ## Motivation 14 | A learning experience with redux and Twilio's javascript SDK. Feel free to improve. 15 | Major and minor versions follow the version number of Twilio javascript SDK. Currently 1.2 16 | 17 | ## How to 18 | 19 | Middleware expects the following dependencies: 20 | * **Twilio**: the global Twilio instance 21 | * **token**: it expects a function that receives store state and returns a Promise of a capability token 22 | * **opts**: Twilio sdk [options](https://www.twilio.com/docs/api/client/device) 23 | 24 | #### Apply middleware 25 | 26 | ```javascript 27 | import {createStore, applyMiddleware} from 'redux'; 28 | import {middleware} from 'twiliojs-redux'; 29 | import fetch from 'isomorphic-fetch'; 30 | import rootReducer from './reducers'; 31 | 32 | const token = (state) => { 33 | return fetch('capability_token', { 34 | headers: { 'Authorization': `Bearer ${state.authToken}` } 35 | }).then((response) => response.text()) 36 | } 37 | 38 | const createStoreWithMiddleware = applyMiddleware( 39 | middleware(Twilio.Device, token, {debug:true}) 40 | )(createStore) 41 | 42 | const store = createStoreWithMiddleware(rootReducer); 43 | ``` 44 | 45 | #### Actions dispatched by middleware 46 | There also a set of actions dispatched by the middleware when something relevant happens. Action types can be found on the module's `constants` object. 47 | 48 | | Action | Payload | Description | 49 | |---------------|------------|---------| 50 | |CHANGE_DEVICE_STATUS|`{status:'ready', silent:true}`| Device and incoming sound status| 51 | |DEVICE_ERROR| `{code:31208, message:'User denied access to microphone.'}`| Error occurrence. Codes and messages are the same as sent by Twilio SDK| 52 | |ADD_ACTIVE_CALL|`{from:'+351910000000', to:'+351960000000', status:'pending', direction: 'inbound', created_at: '1970-01-01T00:00:00.001Z'}`| Active call start inbound or outbound| 53 | |ESTABLISHED_CALL| `{sid:'1235', status:'open'}`|The active call is established, we now have a new status and the connection sid| 54 | |DISCONECTED_CALL| `{status:'closed'}`| Closed the active connection | 55 | |MISSED_CALL|`{}`| Missed an active call before it was answered | 56 | |SET_CALL_MUTE| `true` | Call is muted | 57 | 58 | #### Actions Creators 59 | Smart component can dispatch actions that are interpreted by middleware. Action creators are available on the module's `actions` object. 60 | 61 | | Action Creator| 62 | |---------------| 63 | |`makeCall(from, to)`| 64 | |`acceptCall()`| 65 | |`rejectCall()`| 66 | |`ignoreCall()`| 67 | |`toggleMute()`| 68 | |`hangupCall()`| 69 | |`sendDigits(digits)`| 70 | 71 | #### Example 72 | There's an simple [example](example) project setup with redux dev tools. 73 | -------------------------------------------------------------------------------- /example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react","es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /example/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /example/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "mocha": true, 6 | "es6": true 7 | }, 8 | "plugins": [ 9 | "react" 10 | ], 11 | 12 | "ecmaFeatures": { 13 | "jsx": true, 14 | "arrowFunctions": true, 15 | "binaryLiterals": true, 16 | "blockBindings": true, 17 | "classes": true, 18 | "defaultParams": true, 19 | "destructuring": true, 20 | "forOf": true, 21 | "modules": true, 22 | "objectLiteralComputedProperties": true, 23 | "objectLiteralDuplicateProperties": true, 24 | "objectLiteralShorthandMethods": true, 25 | "objectLiteralShorthandProperties": true, 26 | "octalLiterals": true, 27 | "regexUFlag": true, 28 | "regexYFlag": true, 29 | "spread": true, 30 | "superInFunctions": true, 31 | "templateStrings": true, 32 | "unicodeCodePointEscapes": true, 33 | "globalReturn": true 34 | }, 35 | 36 | "rules": { 37 | "no-alert": 0, 38 | "no-array-constructor": 0, 39 | "no-arrow-condition": 0, 40 | "no-bitwise": 0, 41 | "no-caller": 0, 42 | "no-case-declarations": 0, 43 | "no-catch-shadow": 0, 44 | "no-class-assign": 0, 45 | "no-cond-assign": 2, 46 | "no-console": 2, 47 | "no-const-assign": 0, 48 | "no-constant-condition": 2, 49 | "no-continue": 0, 50 | "no-control-regex": 2, 51 | "no-debugger": 2, 52 | "no-delete-var": 2, 53 | "no-div-regex": 0, 54 | "no-dupe-class-members": 0, 55 | "no-dupe-keys": 2, 56 | "no-dupe-args": 2, 57 | "no-duplicate-case": 2, 58 | "no-else-return": 0, 59 | "no-empty": 2, 60 | "no-empty-character-class": 2, 61 | "no-empty-label": 0, 62 | "no-empty-pattern": 0, 63 | "no-eq-null": 0, 64 | "no-eval": 0, 65 | "no-ex-assign": 2, 66 | "no-extend-native": 0, 67 | "no-extra-bind": 0, 68 | "no-extra-boolean-cast": 2, 69 | "no-extra-parens": 0, 70 | "no-extra-semi": 2, 71 | "no-fallthrough": 2, 72 | "no-floating-decimal": 0, 73 | "no-func-assign": 2, 74 | "no-implicit-coercion": 0, 75 | "no-implied-eval": 0, 76 | "no-inline-comments": 0, 77 | "no-inner-declarations": [2, "functions"], 78 | "no-invalid-regexp": 2, 79 | "no-invalid-this": 0, 80 | "no-irregular-whitespace": 2, 81 | "no-iterator": 0, 82 | "no-label-var": 0, 83 | "no-labels": 0, 84 | "no-lone-blocks": 0, 85 | "no-lonely-if": 0, 86 | "no-loop-func": 0, 87 | "no-mixed-requires": [0, false], 88 | "no-mixed-spaces-and-tabs": [2, false], 89 | "linebreak-style": [0, "unix"], 90 | "no-multi-spaces": 0, 91 | "no-multi-str": 0, 92 | "no-multiple-empty-lines": [0, {"max": 2}], 93 | "no-native-reassign": 0, 94 | "no-negated-condition": 0, 95 | "no-negated-in-lhs": 2, 96 | "no-nested-ternary": 0, 97 | "no-new": 0, 98 | "no-new-func": 0, 99 | "no-new-object": 0, 100 | "no-new-require": 0, 101 | "no-new-wrappers": 0, 102 | "no-obj-calls": 2, 103 | "no-octal": 2, 104 | "no-octal-escape": 0, 105 | "no-param-reassign": 0, 106 | "no-path-concat": 0, 107 | "no-plusplus": 0, 108 | "no-process-env": 0, 109 | "no-process-exit": 0, 110 | "no-proto": 0, 111 | "no-redeclare": 2, 112 | "no-regex-spaces": 2, 113 | "no-restricted-imports": 0, 114 | "no-restricted-modules": 0, 115 | "no-restricted-syntax": 0, 116 | "no-return-assign": 0, 117 | "no-script-url": 0, 118 | "no-self-compare": 0, 119 | "no-sequences": 0, 120 | "no-shadow": 0, 121 | "no-shadow-restricted-names": 0, 122 | "no-spaced-func": 0, 123 | "no-sparse-arrays": 2, 124 | "no-sync": 0, 125 | "no-ternary": 0, 126 | "no-trailing-spaces": 0, 127 | "no-this-before-super": 0, 128 | "no-throw-literal": 0, 129 | "no-undef": 2, 130 | "no-undef-init": 0, 131 | "no-undefined": 0, 132 | "no-unexpected-multiline": 0, 133 | "no-underscore-dangle": 0, 134 | "no-unneeded-ternary": 0, 135 | "no-unreachable": 2, 136 | "no-unused-expressions": 0, 137 | "no-unused-vars": [2, {"vars": "all", "args": "none"}], 138 | "no-use-before-define": 0, 139 | "no-useless-call": 0, 140 | "no-useless-concat": 0, 141 | "no-void": 0, 142 | "no-var": 0, 143 | "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], 144 | "no-with": 0, 145 | "no-magic-numbers": 0, 146 | 147 | "array-bracket-spacing": [0, "never"], 148 | "arrow-body-style": [0, "as-needed"], 149 | "arrow-parens": 0, 150 | "arrow-spacing": 0, 151 | "accessor-pairs": 0, 152 | "block-scoped-var": 0, 153 | "block-spacing": 0, 154 | "brace-style": [0, "1tbs"], 155 | "callback-return": 0, 156 | "camelcase": 0, 157 | "comma-dangle": [2, "never"], 158 | "comma-spacing": 0, 159 | "comma-style": 0, 160 | "complexity": [0, 11], 161 | "computed-property-spacing": [0, "never"], 162 | "consistent-return": 0, 163 | "consistent-this": [0, "that"], 164 | "constructor-super": 0, 165 | "curly": [0, "all"], 166 | "default-case": 0, 167 | "dot-location": 0, 168 | "dot-notation": [0, { "allowKeywords": true }], 169 | "eol-last": 0, 170 | "eqeqeq": 0, 171 | "func-names": 0, 172 | "func-style": [0, "declaration"], 173 | "generator-star-spacing": 0, 174 | "global-require": 0, 175 | "guard-for-in": 0, 176 | "handle-callback-err": 0, 177 | "id-length": 0, 178 | "indent": 0, 179 | "init-declarations": 0, 180 | "jsx-quotes": [0, "prefer-double"], 181 | "key-spacing": [0, { "beforeColon": false, "afterColon": true }], 182 | "lines-around-comment": 0, 183 | "max-depth": [0, 4], 184 | "max-len": [0, 80, 4], 185 | "max-nested-callbacks": [0, 2], 186 | "max-params": [0, 3], 187 | "max-statements": [0, 10], 188 | "new-cap": 0, 189 | "new-parens": 0, 190 | "newline-after-var": 0, 191 | "object-curly-spacing": [0, "never"], 192 | "object-shorthand": 0, 193 | "one-var": [0, "always"], 194 | "operator-assignment": [0, "always"], 195 | "operator-linebreak": 0, 196 | "padded-blocks": 0, 197 | "prefer-arrow-callback": 0, 198 | "prefer-const": 0, 199 | "prefer-reflect": 0, 200 | "prefer-rest-params": 0, 201 | "prefer-spread": 0, 202 | "prefer-template": 0, 203 | "quote-props": 0, 204 | "quotes": [0, "double"], 205 | "radix": 0, 206 | "id-match": 0, 207 | "require-jsdoc": 0, 208 | "require-yield": 0, 209 | "semi": 0, 210 | "semi-spacing": [0, {"before": false, "after": true}], 211 | "sort-vars": 0, 212 | "space-after-keywords": [0, "always"], 213 | "space-before-keywords": [0, "always"], 214 | "space-before-blocks": [0, "always"], 215 | "space-before-function-paren": [0, "always"], 216 | "space-in-parens": [0, "never"], 217 | "space-infix-ops": 0, 218 | "space-return-throw-case": 0, 219 | "space-unary-ops": [0, { "words": true, "nonwords": false }], 220 | "spaced-comment": 0, 221 | "strict": 0, 222 | "use-isnan": 2, 223 | "valid-jsdoc": 0, 224 | "valid-typeof": 2, 225 | "vars-on-top": 0, 226 | "wrap-iife": 0, 227 | "wrap-regex": 0, 228 | "yield-star-spacing": 0, 229 | "yoda": [0, "never"] 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | config.js 3 | build 4 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | Download [ngrok](https://ngrok.com/), it'll enable us to expose the backend to the outside world. 2 | 3 | ### Step 1 - Expose localhost:3000 to the world 4 | Run `ngrok http 3000`. It should output something similar to: 5 | 6 | `forwarding http://fabd148a.ngrok.io -> localhost:3000 ` 7 | 8 | ### Step 2 - Configure Twilio phone number 9 | Configure number's voice url using the hostname given by ngrok. Example: 10 | 11 | `http:///twilml/incoming/voice` 12 | 13 | Create a [TwilML App](https://www.twilio.com/user/account/voice/dev-tools/twiml-apps) with the outgoing voice url: 14 | 15 | `http:///twilml/outgoing/voice` 16 | 17 | Copy app `sid`, that will be needed in a minute. 18 | 19 | ### Step 3 - Configure project 20 | 1. Rename `config.sample.js` to `config.js` 21 | 2. Edit `config.js` with your [Twilio account credentials](https://www.twilio.com/user/account/settings). 22 | * `account_sid` 23 | * `auth_token` 24 | * `app_sid` 25 | 26 | ### Step 4 - Build & Launch 27 | 28 | ``` 29 | npm install 30 | gulp build 31 | node server 32 | ``` 33 | -------------------------------------------------------------------------------- /example/build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Twilio redux middleware demo 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/config.sample.js: -------------------------------------------------------------------------------- 1 | /* 2 | * rename this to config.js 3 | */ 4 | module.exports = { 5 | twilio: { 6 | account_sid: '*****', 7 | auth_token: '*****', 8 | client_id: 'my-client', 9 | app_sid: '*****' 10 | }, 11 | logging: { 12 | level: 'debug' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var browserify = require('browserify'); 3 | var babelify = require('babelify'); 4 | var rimraf = require('rimraf'); 5 | var source = require('vinyl-source-stream'); 6 | var browserSync = require('browser-sync'); 7 | var reload = browserSync.reload; 8 | var gutil = require('gulp-util'); 9 | 10 | var config = { 11 | entryFile: './src/app/app.js', 12 | htmlFiles: './src/www/**/*', 13 | outputDir: './build/', 14 | outputFile: 'app.js' 15 | }; 16 | 17 | // clean the output directory 18 | gulp.task('clean', function(cb){ 19 | rimraf(config.outputDir, cb); 20 | }); 21 | 22 | var bundler; 23 | function getBundler() { 24 | if (!bundler) { 25 | bundler = browserify(config.entryFile, { debug: true }); 26 | } 27 | return bundler; 28 | } 29 | 30 | function bundle() { 31 | return getBundler() 32 | .transform(babelify) 33 | .bundle() 34 | .on('error', function(err) { gutil.log('Error: ' + err.message); }) 35 | .pipe(source(config.outputFile)) 36 | .pipe(gulp.dest(config.outputDir)) 37 | .pipe(reload({ stream: true })); 38 | } 39 | 40 | gulp.task('build', ['html'], function() { 41 | return bundle(); 42 | }); 43 | 44 | gulp.task('html', ['clean'], function() { 45 | return gulp.src(config.htmlFiles) 46 | .pipe(gulp.dest(config.outputDir)); 47 | }); 48 | 49 | gulp.task('watch', function() { 50 | getBundler().on('update', function() { 51 | gulp.start('build') 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twiliojs-redux-example", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "bucker": "^1.1.1", 13 | "hapi": "^11.1.2", 14 | "inert": "^3.2.0", 15 | "isomorphic-fetch": "^2.2.0", 16 | "react": "^0.14.3", 17 | "react-dom": "^0.14.3", 18 | "redux": "^3.0.4", 19 | "redux-devtools": "^2.1.5", 20 | "twilio": "^2.6.0", 21 | "twiliojs-redux": "file:///Users/yarcub/trunk/twiliojs-redux" 22 | }, 23 | "devDependencies": { 24 | "babel-preset-es2015": "^6.3.13", 25 | "babel-preset-react": "^6.3.13", 26 | "babelify": "^7.2.0", 27 | "browser-sync": "^2.10.0", 28 | "browserify": "^12.0.1", 29 | "browserify-shim": "^3.8.11", 30 | "eslint": "^1.10.3", 31 | "eslint-plugin-react": "^3.11.3", 32 | "gulp": "^3.9.0", 33 | "gulp-babel": "^6.1.1", 34 | "gulp-util": "^3.0.7", 35 | "vinyl-source-stream": "^1.1.0", 36 | "watchify": "^3.6.1" 37 | }, 38 | "browserify": { 39 | "transform": [ 40 | "browserify-shim" 41 | ] 42 | }, 43 | "browserify-shim": { 44 | "Twilio": "global:Twilio" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi'); 2 | var bucker = require('bucker'); 3 | var Twilio = require('twilio'); 4 | var config = require('./config'); 5 | 6 | var server = new Hapi.Server(); 7 | server.connection({ port: 3000 }); 8 | 9 | var logger = bucker.createLogger({level: config.logging.level}); 10 | 11 | server.register(require('inert'), function (err) { 12 | 13 | if (err) { 14 | throw err; 15 | } 16 | 17 | server.route({ 18 | method: 'GET', 19 | path: '/{param*}', 20 | handler: { 21 | directory: { 22 | path: 'build' 23 | } 24 | } 25 | }); 26 | 27 | server.route({ method: 'POST', path: '/twilml/outgoing/voice', handler: function(request, reply){ 28 | logRequest(request); 29 | var twilml = new Twilio.TwimlResponse(); 30 | twilml.dial(request.payload.To, {callerId: request.payload.From}); 31 | var response = reply(twilml.toString()); 32 | response.type('application/xml'); 33 | } 34 | }); 35 | 36 | server.route({ method: 'POST', path: '/twilml/outgoing/fallback', handler: function(request, reply){ 37 | logRequest(request); 38 | reply(); 39 | } 40 | }); 41 | 42 | server.route({ method: 'POST', path: '/twilml/incoming/voice', handler: function(request, reply){ 43 | logRequest(request); 44 | var twilml = new Twilio.TwimlResponse(); 45 | twilml.dial({callerId: request.payload.From}, function(){ 46 | this.client(config.twilio.client_id); 47 | }) 48 | var response = reply(twilml.toString()); 49 | response.type('application/xml'); 50 | } 51 | }); 52 | 53 | 54 | server.route({ 55 | method: 'GET', 56 | path: '/capability_token', 57 | handler: function (request, reply) { 58 | var capability = new Twilio.Capability(config.twilio.account_sid, config.twilio.auth_token); 59 | capability.allowClientIncoming(config.twilio.client_id); 60 | capability.allowClientOutgoing(config.twilio.app_sid); 61 | 62 | var token = capability.generate(); 63 | reply(token); 64 | } 65 | }); 66 | }); 67 | 68 | var logRequest = function(log){ 69 | logger.debug(log); 70 | } 71 | 72 | server.start(function () { 73 | logger.info('Server running at:', server.info.uri); 74 | }); 75 | -------------------------------------------------------------------------------- /example/src/app/app.js: -------------------------------------------------------------------------------- 1 | import {middleware} from 'twiliojs-redux'; 2 | import {createStore, applyMiddleware, compose} from 'redux'; 3 | import {devTools} from 'redux-devtools'; 4 | import {Provider} from 'react-redux' 5 | import Twilio from 'Twilio'; 6 | import fetch from 'isomorphic-fetch'; 7 | import React from 'react'; 8 | import ReactDOM from 'react-dom'; 9 | import { DevTools, DebugPanel, LogMonitor } from 'redux-devtools/lib/react'; 10 | import MyApp from './myapp.jsx'; 11 | 12 | import rootReducer from './reducers'; 13 | 14 | const getToken = ()=>{ 15 | return fetch('capability_token') 16 | .then((response) => response.text()) 17 | } 18 | 19 | const createStoreWithMiddleware = compose( 20 | applyMiddleware(middleware(Twilio.Device, getToken,{debug:true})), 21 | devTools() 22 | )(createStore); 23 | 24 | const store = createStoreWithMiddleware(rootReducer, {}); 25 | 26 | class Root extends React.Component { 27 | render() { 28 | return ( 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | ); 38 | } 39 | } 40 | 41 | ReactDOM.render(, document.getElementById('app-container')); 42 | 43 | /* 44 | 45 | {() => } 46 | 47 | */ 48 | -------------------------------------------------------------------------------- /example/src/app/myapp.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import React from 'react'; 3 | import {actions} from 'twiliojs-redux'; 4 | 5 | class MyApp extends React.Component{ 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = {}; 10 | } 11 | 12 | handleChange(event) { 13 | this.setState({call_to: event.target.value}); 14 | } 15 | 16 | render() { 17 | return ( 18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | ) 30 | } 31 | } 32 | 33 | function mapStateToProps(state) { 34 | return { 35 | isOnline: state.get('online') 36 | } 37 | } 38 | 39 | function mapDispatchToProps(dispatch) { 40 | return { 41 | makeCall: (to) => () => { 42 | dispatch(actions.makeCall('+351308803679', to)); 43 | }, 44 | answer: () => { 45 | dispatch(actions.acceptCall()); 46 | }, 47 | reject: () => { 48 | dispatch(actions.rejectCall()); 49 | }, 50 | mute: () => { 51 | dispatch(actions.toggleMute()); 52 | }, 53 | hangup: () => { 54 | dispatch(actions.hangupCall()); 55 | }, 56 | sendDigits: (digits) => () => { 57 | dispatch(actions.sendDigits(digits)); 58 | } 59 | } 60 | } 61 | 62 | export default connect( 63 | mapStateToProps, 64 | mapDispatchToProps 65 | )(MyApp) 66 | -------------------------------------------------------------------------------- /example/src/app/reducers.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import {constants} from 'twiliojs-redux'; 3 | 4 | 5 | function connectivityReducer(state, action){ 6 | switch(action.type){ 7 | case constants.CHANGE_DEVICE_STATUS: 8 | return action.payload.status === 'ready' 9 | default: 10 | return state; 11 | } 12 | } 13 | 14 | 15 | export default function(state, action) { 16 | if(!state) return Immutable.Map({}); 17 | 18 | return Immutable.Map({ 19 | online: connectivityReducer(state.online, action) 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /example/src/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Twilio redux middleware demo 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /gulp/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | spec: { 3 | src: ['./spec/*.js'], 4 | watch_src: ['./**/*.js'] 5 | }, 6 | build: { 7 | output: 'lib' 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /gulp/tasks/build.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const babel = require('gulp-babel'); 3 | const config = require('../config').build; 4 | 5 | gulp.task('build',['lint','spec'], () => { 6 | return gulp.src('src/**/*.js') 7 | .pipe(babel()) 8 | .pipe(gulp.dest(config.output)); 9 | }); 10 | -------------------------------------------------------------------------------- /gulp/tasks/lint.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | eslint = require('gulp-eslint'); 3 | 4 | gulp.task('lint', function () { 5 | return gulp.src(['src/**/*.js']) 6 | .pipe(eslint()) 7 | .pipe(eslint.format()) 8 | .pipe(eslint.failAfterError()); 9 | }); 10 | -------------------------------------------------------------------------------- /gulp/tasks/spec-watch.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | config = require('../config').spec; 3 | 4 | gulp.task('spec-watch', function(){ 5 | gulp.watch(config.watch_src, ['spec']); 6 | }); 7 | -------------------------------------------------------------------------------- /gulp/tasks/spec.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | gutil = require('gulp-util'), 3 | mocha = require('gulp-mocha'), 4 | config = require('../config').spec; 5 | 6 | gulp.task('spec', function(){ 7 | return gulp.src(config.src, {read: false}) 8 | .pipe(mocha({reporter: 'spec'})) 9 | }); 10 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var requireDir; 2 | 3 | requireDir = require('require-dir'); 4 | 5 | requireDir( 6 | './gulp/tasks', 7 | { 8 | recurse: true 9 | } 10 | ); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twiliojs-redux", 3 | "version": "1.2.0", 4 | "description": "Twiliojs middleware for Redux", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "gulp spec" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/yarcub/twiliojs-redux.git" 12 | }, 13 | "homepage": "https://github.com/yarcub/twiliojs-redux", 14 | "keywords": [ 15 | "twilio", 16 | "redux", 17 | "middleware", 18 | "redux-middleware", 19 | "flux" 20 | ], 21 | "author": "Nuno Rosa ", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "babel-preset-es2015": "^6.3.13", 25 | "chai": "^3.4.1", 26 | "eslint": "^3.10.2", 27 | "gulp": "^3.9.0", 28 | "gulp-babel": "^6.1.1", 29 | "gulp-eslint": "^3.0.1", 30 | "gulp-mocha": "^3.0.1", 31 | "gulp-util": "^3.0.7", 32 | "gulp-watch": "^4.3.5", 33 | "mocha": "^3.1.2", 34 | "redux": "^3.0.4", 35 | "require-dir": "^0.3.0", 36 | "sinon": "^1.17.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /spec/middleware.spec.js: -------------------------------------------------------------------------------- 1 | const middleware = require('./../src/middleware'), 2 | sinon = require('sinon'), 3 | mockStoreWithMiddleware = require('./util/mock'), 4 | actions = require('./../src/actions'); 5 | 6 | const store = { 7 | dispatch: () => {}, 8 | getState: () => {} 9 | } 10 | 11 | const getToken = () => {return Promise.resolve('token')}; 12 | 13 | var device; 14 | var originalNow = Date.now; 15 | 16 | beforeEach(()=>{ 17 | var activeConnection = { 18 | accept: sinon.spy(), 19 | reject: sinon.spy(), 20 | ignore: sinon.spy(), 21 | mute: sinon.spy(), 22 | sendDigits: sinon.spy(), 23 | disconnect: sinon.spy(), 24 | isMuted(){return false} 25 | } 26 | device = { 27 | object: { 28 | activeConnection: () => {return activeConnection} 29 | }, 30 | setup: sinon.spy(), 31 | ready: sinon.spy(), 32 | offline: sinon.spy(), 33 | error: sinon.spy(), 34 | incoming: sinon.spy(), 35 | cancel: sinon.spy(), 36 | connect: sinon.spy(), 37 | disconnect: sinon.spy(), 38 | activeConnection: ()=>{return activeConnection} 39 | }; 40 | 41 | Date.now = () => {return new Date(1)}; 42 | }) 43 | 44 | afterEach(()=>{ 45 | Date.now = originalNow; 46 | }) 47 | 48 | describe('Twilio Middleware', () => { 49 | 50 | it('should setup device when token resolved', (done) => { 51 | middleware(device, getToken, {})(store); 52 | 53 | setTimeout(()=>{ 54 | sinon.assert.calledWith(device.setup, 'token', {}); 55 | done(); 56 | },0) 57 | }) 58 | 59 | it('should add callbacks for device events', (done) =>{ 60 | middleware(device, getToken, {})(store); 61 | 62 | setTimeout(()=>{ 63 | sinon.assert.called(device.ready); 64 | sinon.assert.called(device.offline); 65 | sinon.assert.called(device.error); 66 | sinon.assert.called(device.incoming); 67 | sinon.assert.called(device.cancel); 68 | sinon.assert.called(device.connect); 69 | sinon.assert.called(device.disconnect); 70 | done(); 71 | },0) 72 | }) 73 | 74 | it('should setup device when device is offline', (done) => { 75 | middleware(device, getToken, {})(store); 76 | 77 | device.offline = (handler) => { 78 | handler({ 79 | status(){ return 'offline'; }, 80 | sounds:{ 81 | incoming(){ return false; } 82 | } 83 | }); 84 | }; 85 | 86 | setTimeout(()=>{ 87 | sinon.assert.alwaysCalledWith(device.setup, 'token', {}); 88 | done(); 89 | },0) 90 | }); 91 | 92 | it('should dispatch change device status action on ready', (done) => { 93 | const expectedActions = [ 94 | { 95 | type: '@@twilioRedux/changeDeviceStatus', 96 | payload: { 97 | status: 'ready', silent: true 98 | } 99 | } 100 | ] 101 | 102 | device.ready = (handler) => { 103 | handler({ 104 | status(){ return 'ready'; }, 105 | sounds:{ 106 | incoming(){ return false; } 107 | } 108 | }); 109 | }; 110 | 111 | mockStoreWithMiddleware( 112 | {}, 113 | expectedActions, 114 | [middleware(device, getToken, {})], 115 | done 116 | ) 117 | }) 118 | 119 | it('should dispatch change device status action on offline', (done) => { 120 | const expectedActions = [ 121 | { 122 | type: '@@twilioRedux/changeDeviceStatus', 123 | payload: { 124 | status: 'offline', silent: true 125 | } 126 | } 127 | ] 128 | 129 | device.offline = (handler) => { 130 | handler({ 131 | status(){ return 'offline'; }, 132 | sounds:{ 133 | incoming(){ return false; } 134 | } 135 | }); 136 | }; 137 | 138 | mockStoreWithMiddleware( 139 | {}, 140 | expectedActions, 141 | [middleware(device, getToken, {})], 142 | done 143 | ) 144 | }) 145 | 146 | it('should dispatch device error action on error', (done) => { 147 | const expectedActions = [ 148 | { 149 | type: '@@twilioRedux/deviceError', 150 | payload: { 151 | code: '1234', 152 | message: 'an error occurred' 153 | }, 154 | isError: true 155 | } 156 | ] 157 | 158 | device.error = (handler) => { 159 | handler({code: '1234', message: 'an error occurred'}); 160 | }; 161 | 162 | mockStoreWithMiddleware( 163 | {}, 164 | expectedActions, 165 | [middleware(device, getToken, {})], 166 | done 167 | ) 168 | }) 169 | 170 | it('should dispatch add active call action on incoming connection', (done) => { 171 | const expectedActions = [ 172 | { 173 | type: '@@twilioRedux/addActiveCall', 174 | payload: { 175 | from: '+351910000000', 176 | to: '+351960000000', 177 | status: 'pending', 178 | direction: 'inbound', 179 | created_at: Date.now() 180 | } 181 | } 182 | ] 183 | 184 | device.incoming = (handler) => { 185 | handler({ 186 | parameters: { 187 | CallSid: '1234', 188 | From: '+351910000000', 189 | To: '+351960000000' 190 | }, 191 | status(){return 'pending'} 192 | }); 193 | }; 194 | 195 | mockStoreWithMiddleware( 196 | {}, 197 | expectedActions, 198 | [middleware(device, getToken, {})], 199 | done 200 | ) 201 | }) 202 | 203 | it('should dispatch missed call action on cancel connection', (done) => { 204 | const expectedActions = [ 205 | { 206 | type: '@@twilioRedux/missedCall' 207 | } 208 | ] 209 | 210 | device.cancel = (handler) => { 211 | handler({}); 212 | }; 213 | 214 | mockStoreWithMiddleware( 215 | {}, 216 | expectedActions, 217 | [middleware(device, getToken, {})], 218 | done 219 | ) 220 | }) 221 | 222 | it('should dispatch establised call action on connected connection', (done) => { 223 | const expectedActions = [ 224 | { 225 | type: '@@twilioRedux/callEstablished', 226 | payload: { 227 | sid: '1234', 228 | status: 'open' 229 | } 230 | } 231 | ] 232 | 233 | device.connect = (handler) => { 234 | handler({ 235 | parameters: { 236 | CallSid: '1234', 237 | From: '+351910000000', 238 | To: '+351960000000' 239 | }, 240 | status(){return 'open'} 241 | }); 242 | }; 243 | 244 | mockStoreWithMiddleware( 245 | {}, 246 | expectedActions, 247 | [middleware(device, getToken, {})], 248 | done 249 | ) 250 | }) 251 | 252 | it('should dispatch disconnected call action on disconnected connection', (done) => { 253 | const expectedActions = [ 254 | { 255 | type: '@@twilioRedux/disconnectedCall', 256 | payload: { 257 | status: 'closed' 258 | } 259 | } 260 | ] 261 | 262 | device.disconnect = (handler) => { 263 | handler({ 264 | parameters: { 265 | CallSid: '1234', 266 | From: '+351910000000', 267 | To: '+351960000000' 268 | }, 269 | status(){return 'closed'} 270 | }); 271 | }; 272 | 273 | mockStoreWithMiddleware( 274 | {}, 275 | expectedActions, 276 | [middleware(device, getToken, {})], 277 | done 278 | ) 279 | }) 280 | 281 | it('should call twilio connect on make call action', (done) => { 282 | const expectedActions = [ 283 | { 284 | type: '@@twilioRedux/addActiveCall', 285 | payload: { 286 | from: '+351910000000', 287 | to: '+351960000000', 288 | status: 'pending', 289 | created_at: Date.now(), 290 | direction: 'outbound' 291 | } 292 | } 293 | ] 294 | const store = mockStoreWithMiddleware( 295 | {}, 296 | expectedActions, 297 | [middleware(device, getToken, {})], 298 | done 299 | ) 300 | 301 | store.dispatch(actions.makeCall('+351910000000', '+351960000000')) 302 | sinon.assert.calledWith(device.connect, {From: '+351910000000', To: '+351960000000'}); 303 | }) 304 | 305 | it('should accept active twilio connection on accept call action', (done) => { 306 | const store = mockStoreWithMiddleware( 307 | {}, 308 | [], 309 | [middleware(device, getToken, {})], 310 | done 311 | ) 312 | 313 | const constraints = {optional:{sourceid: 'xxx'}}; 314 | 315 | store.dispatch(actions.acceptCall(constraints)) 316 | 317 | setTimeout(()=>{ 318 | sinon.assert.calledWith(device.activeConnection().accept, constraints); 319 | done(); 320 | },0) 321 | }) 322 | 323 | it('should reject active twilio connection on reject call action', (done) => { 324 | const store = mockStoreWithMiddleware( 325 | {}, 326 | [], 327 | [middleware(device, getToken, {})], 328 | done 329 | ) 330 | 331 | store.dispatch(actions.rejectCall()) 332 | 333 | setTimeout(()=>{ 334 | sinon.assert.called(device.activeConnection().reject); 335 | done(); 336 | },0) 337 | }) 338 | 339 | it('should ignore active twilio connection on ignore call action', (done) => { 340 | const store = mockStoreWithMiddleware( 341 | {}, 342 | [], 343 | [middleware(device, getToken, {})], 344 | done 345 | ) 346 | 347 | store.dispatch(actions.ignoreCall()); 348 | 349 | setTimeout(()=>{ 350 | sinon.assert.called(device.activeConnection().ignore); 351 | done(); 352 | },0) 353 | }) 354 | 355 | it('should mute active twilio connection on mute call action', (done) => { 356 | const store = mockStoreWithMiddleware( 357 | {}, 358 | [], 359 | [middleware(device, getToken, {})], 360 | done 361 | ) 362 | 363 | store.dispatch(actions.toggleMute()); 364 | 365 | setTimeout(()=>{ 366 | sinon.assert.calledWith(device.activeConnection().mute, true); 367 | done(); 368 | },0) 369 | }) 370 | 371 | it('should send digits to active twilio connection on send digits action', (done) => { 372 | const store = mockStoreWithMiddleware( 373 | {}, 374 | [], 375 | [middleware(device, getToken, {})], 376 | done 377 | ) 378 | 379 | store.dispatch(actions.sendDigits('25#*')); 380 | 381 | setTimeout(()=>{ 382 | sinon.assert.calledWith(device.activeConnection().sendDigits, '25#*'); 383 | done(); 384 | },0) 385 | }) 386 | 387 | it('should disconnect active twilio connection on hangup action', (done) => { 388 | const store = mockStoreWithMiddleware( 389 | {}, 390 | [], 391 | [middleware(device, getToken, {})], 392 | done 393 | ) 394 | 395 | store.dispatch(actions.hangupCall()); 396 | 397 | setTimeout(()=>{ 398 | sinon.assert.calledWith(device.activeConnection().disconnect); 399 | done(); 400 | },0) 401 | }) 402 | 403 | it('should add outbound active call on make call action', (done) => { 404 | const expectedActions = [ 405 | { 406 | type: '@@twilioRedux/addActiveCall', 407 | payload: { 408 | from: '+351910000000', 409 | to: '+351960000000', 410 | direction: 'outbound', 411 | status: 'pending', 412 | created_at: Date.now() 413 | } 414 | } 415 | ] 416 | const store = mockStoreWithMiddleware( 417 | {}, 418 | expectedActions, 419 | [middleware(device, getToken, {})], 420 | done 421 | ) 422 | 423 | store.dispatch(actions.makeCall('+351910000000', '+351960000000')); 424 | }) 425 | 426 | }); 427 | -------------------------------------------------------------------------------- /spec/util/mock.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const applyMiddleware = require('redux').applyMiddleware; 3 | 4 | module.exports = function mockStoreWithMiddleware(getState, expectedActions, middlewares, done) { 5 | if (!Array.isArray(expectedActions)) { 6 | throw new Error('expectedActions should be an array of expected actions.') 7 | } 8 | if (typeof done !== 'undefined' && typeof done !== 'function') { 9 | throw new Error('done should either be undefined or function.') 10 | } 11 | 12 | function mockStoreWithoutMiddleware() { 13 | return { 14 | getState() { 15 | return typeof getState === 'function' ? 16 | getState() : 17 | getState 18 | }, 19 | 20 | dispatch(action) { 21 | const expectedAction = expectedActions.shift() 22 | 23 | try { 24 | expect(action).to.eql(expectedAction) 25 | if (done && !expectedActions.length) { 26 | done() 27 | } 28 | return action 29 | } catch (e) { 30 | done(e) 31 | } 32 | } 33 | } 34 | } 35 | 36 | const mockStoreWithMiddleware = applyMiddleware( 37 | ...middlewares 38 | )(mockStoreWithoutMiddleware) 39 | 40 | return mockStoreWithMiddleware() 41 | } 42 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | const constants = require('./constants'); 2 | 3 | module.exports = { 4 | changeDeviceStatus(device) { 5 | return { 6 | type: constants.CHANGE_DEVICE_STATUS, 7 | payload: { 8 | status: device.status(), 9 | silent: !device.sounds.incoming() 10 | } 11 | } 12 | }, 13 | deviceError(err) { 14 | return { 15 | type: constants.DEVICE_ERROR, 16 | payload: { 17 | code: err.code, 18 | message: err.message 19 | }, 20 | isError: true 21 | } 22 | }, 23 | addActiveCall(from, to, direction, timestamp) { //Refactor to addActiveCall 24 | return { 25 | type: constants.ADD_ACTIVE_CALL, 26 | payload: { 27 | from: from, 28 | to: to, 29 | status: 'pending', 30 | created_at: timestamp, 31 | direction: direction 32 | } 33 | } 34 | }, 35 | missedCall(conn) { 36 | return { 37 | type: constants.MISSED_CALL 38 | } 39 | }, 40 | establishedCall(conn){ 41 | return { 42 | type: constants.ESTABLISHED_CALL, 43 | payload: { 44 | sid: conn.parameters.CallSid, 45 | status: conn.status() 46 | } 47 | } 48 | }, 49 | disconnectedCall(conn){ 50 | return { 51 | type: constants.DISCONECTED_CALL, 52 | payload: { 53 | status: conn.status() 54 | } 55 | } 56 | }, 57 | setCallMute(isMuted){ 58 | return { 59 | type: constants.SET_CALL_MUTE, 60 | payload: isMuted 61 | } 62 | }, 63 | /*PUBLIC ACTIONS*/ 64 | makeCall(from, to){ 65 | return { 66 | '@@isTwilioRedux': true, 67 | type: constants.MAKE_CALL, 68 | payload:{ 69 | From: from, 70 | To: to 71 | } 72 | } 73 | }, 74 | acceptCall(audioConstraints){ 75 | return { 76 | '@@isTwilioRedux': true, 77 | type: constants.ACCEPT_CALL, 78 | payload: audioConstraints 79 | } 80 | }, 81 | rejectCall(){ 82 | return { 83 | '@@isTwilioRedux': true, 84 | type: constants.REJECT_CALL 85 | } 86 | }, 87 | ignoreCall(){ 88 | return { 89 | '@@isTwilioRedux': true, 90 | type: constants.IGNORE_CALL 91 | } 92 | }, 93 | toggleMute(){ 94 | return { 95 | '@@isTwilioRedux': true, 96 | type: constants.TOGGLE_MUTE 97 | } 98 | }, 99 | sendDigits(digits){ 100 | return { 101 | '@@isTwilioRedux': true, 102 | type: constants.SEND_DIGITS, 103 | payload: digits 104 | } 105 | }, 106 | hangupCall(){ 107 | return { 108 | '@@isTwilioRedux': true, 109 | type: constants.HANGUP_CALL 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | CHANGE_DEVICE_STATUS: '@@twilioRedux/changeDeviceStatus', 3 | DEVICE_ERROR: '@@twilioRedux/deviceError', 4 | ADD_ACTIVE_CALL: '@@twilioRedux/addActiveCall', 5 | MISSED_CALL: '@@twilioRedux/missedCall', 6 | ESTABLISHED_CALL: '@@twilioRedux/callEstablished', 7 | DISCONECTED_CALL: '@@twilioRedux/disconnectedCall', 8 | MAKE_CALL: '@@twilioRedux/makeCall', 9 | ACCEPT_CALL: '@@twilioRedux/acceptCall', 10 | REJECT_CALL: '@@twilioRedux/rejectCall', 11 | IGNORE_CALL: '@@twilioRedux/ignoreCall', 12 | TOGGLE_MUTE: '@@twilioRedux/TOGGLE_MUTE', 13 | SEND_DIGITS: '@@twilioRedux/sendDigits', 14 | HANGUP_CALL: '@@twilioRedux/hangupCall', 15 | SET_CALL_MUTE: '@@twilioRedux/setCallMute' 16 | } 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var actions = require('./actions'); 2 | 3 | module.exports = { 4 | middleware: require('./middleware'), 5 | constants: require('./constants'), 6 | actions: { 7 | makeCall: actions.makeCall, 8 | acceptCall: actions.acceptCall, 9 | rejectCall: actions.rejectCall, 10 | ignoreCall: actions.ignoreCall, 11 | toggleMute: actions.toggleMute, 12 | sendDigits: actions.sendDigits, 13 | hangupCall: actions.hangupCall 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/middleware.js: -------------------------------------------------------------------------------- 1 | var actions = require('./actions'), 2 | constants = require('./constants'); 3 | 4 | const middleware = (twilioDevice, token, opts) => store => { 5 | const getToken = () => token(store.getState()) 6 | 7 | getToken().then( value => { 8 | twilioDevice.ready( device => { 9 | store.dispatch(actions.changeDeviceStatus(device)); 10 | }); 11 | 12 | twilioDevice.offline( device => { 13 | store.dispatch(actions.changeDeviceStatus(device)); 14 | getToken().then( value => { 15 | twilioDevice.setup(value, opts); 16 | }) 17 | }); 18 | 19 | twilioDevice.error( error => { 20 | store.dispatch(actions.deviceError(error)); 21 | }); 22 | 23 | twilioDevice.incoming( conn => { 24 | store.dispatch(actions.addActiveCall( 25 | conn.parameters.From, 26 | conn.parameters.To, 27 | 'inbound', 28 | Date.now() 29 | )); 30 | conn.accept( conn => { 31 | 32 | }); 33 | 34 | conn.mute( (isMuted, conn) => { 35 | store.dispatch(actions.setCallMute(isMuted)); 36 | }) 37 | }); 38 | 39 | twilioDevice.cancel( conn => { 40 | store.dispatch(actions.missedCall()); 41 | }); 42 | 43 | twilioDevice.connect( conn => { 44 | store.dispatch(actions.establishedCall(conn)); 45 | }); 46 | 47 | twilioDevice.disconnect( conn => { 48 | store.dispatch(actions.disconnectedCall(conn)); 49 | }); 50 | 51 | twilioDevice.setup(value, opts); 52 | }); 53 | 54 | return next => action => { 55 | const device = twilioDevice.object; 56 | if(action['@@isTwilioRedux']){ 57 | switch(action.type){ 58 | case constants.MAKE_CALL: 59 | twilioDevice.connect(action.payload); 60 | next(actions.addActiveCall( 61 | action.payload.From, 62 | action.payload.To, 63 | 'outbound', 64 | Date.now() 65 | )) 66 | return; 67 | case constants.ACCEPT_CALL: 68 | device.activeConnection().accept(action.payload); 69 | return; 70 | case constants.REJECT_CALL: 71 | device.activeConnection().reject(); 72 | return; 73 | case constants.IGNORE_CALL: 74 | device.activeConnection().ignore(); 75 | return; 76 | case constants.TOGGLE_MUTE: 77 | device.activeConnection().mute(!device.activeConnection().isMuted()); 78 | return; 79 | case constants.SEND_DIGITS: 80 | device.activeConnection().sendDigits(action.payload); 81 | return; 82 | case constants.HANGUP_CALL: 83 | device.activeConnection().disconnect(); 84 | return; 85 | default: 86 | throw new Error('Unknown twilio action'); 87 | } 88 | } 89 | return next(action); 90 | } 91 | } 92 | 93 | module.exports = middleware; 94 | --------------------------------------------------------------------------------