├── .example.env ├── .gitignore ├── LICENSE ├── README.md ├── client ├── .example.env ├── package.json ├── public │ ├── favicon.ico │ └── index.html └── src │ ├── actions │ ├── constants.js │ └── index.js │ ├── components │ ├── app.js │ ├── login.js │ ├── main.js │ └── video-chat.js │ ├── index.js │ ├── reducers │ ├── index.js │ ├── room.js │ └── user.js │ └── store │ ├── configureStore.js │ └── preloadedState.js ├── package.json ├── server.js └── start-client.js /.example.env: -------------------------------------------------------------------------------- 1 | # Generated via Google API Console 2 | GOOGLE_CLIENT_ID= 3 | 4 | # Overall Twilio Account SID 5 | TWILIO_ACCOUNT_SID= 6 | 7 | # Configuration information for Programmable Video API 8 | TWILIO_API_KEY= 9 | TWILIO_API_SECRET= 10 | TWILIO_CONFIGURATION_SID= 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | # See http://help.github.com/ignore-files/ for more about ignoring files. 39 | 40 | # dependencies 41 | node_modules 42 | 43 | # testing 44 | coverage 45 | 46 | # production 47 | build 48 | 49 | # misc 50 | .DS_Store 51 | .env 52 | npm-debug.log 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Hangouts Clone using Twilio Programmable Video and React 2 | This demo project demonstrates how to use Twilio APIs in a React application to 3 | recreate a Google Hangouts app. 4 | 5 | ### Configure the sample application 6 | 7 | Before we run the application, we'll need to gather API credentials from Google 8 | and Twilio. 9 | 10 | ### Configure Google account information 11 | 12 | Since part of our application uses Google oAuth for authentication we will 13 | need configure the Google Client ID. We'll need to store this ID on the backend 14 | and frontend since it is stored in both. In `.env`, we will configure a 15 | `GOOGLE_CLIENT_ID` variable for the backend. 16 | 17 | | Config Value | Description | 18 | |--------------|-------------| 19 | | GOOGLE_CLIENT_ID | Client ID needed for oAuth - generate one [using these instructions](https://developers.google.com/identity/sign-in/web/devconsole-project). | 20 | 21 | We'll also configure the same ID under a different variable name in `client/.env`. 22 | 23 | | Config Value | Description | 24 | |--------------|-------------| 25 | | REACT_APP_GOOGLE_CLIENT_ID | The same ID as above under a different name | 26 | 27 | ### Configure Twilio account information 28 | 29 | | Config Value | Description | 30 | |--------------|-------------| 31 | | TWILIO_ACCOUNT_SID | Your primary Twilio account identifier - find this [in the console here](https://www.twilio.com/console). | 32 | | TWILIO_API_KEY | Used to authenticate - [generate one here](https://www.twilio.com/console/video/dev-tools/api-keys). | 33 | | TWILIO_API_SECRET | Used to authenticate - generated when you generate the key above. | 34 | 35 | ### Configure Twilio video settings 36 | 37 | | Config Value | Description | 38 | |--------------|-------------| 39 | | TWILIO_CONFIGURATION_SID | Identifier for a set of config properties for your video application - [find yours here](https://www.twilio.com/console/video/profiles). | 40 | 41 | ## Run the demo application 42 | 43 | This demo application runs two servers simulanteously. One is a server that serves 44 | the front-end assets of the application, the other is a server that exposes a backend 45 | endpoint that will be used for authentication. The front-end server contains a proxy 46 | to the backend server so that users can send a query from the frontend server 47 | to the backend server. You can read more about this type of development 48 | setup in [the official create-react-app documentation](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#node). 49 | 50 | So we need to install both our backend and frontend dependencies. 51 | Let's install the dependencies for our backend first. 52 | 53 | ``` 54 | npm install 55 | ``` 56 | 57 | We'll then install our frontend dependencies. 58 | 59 | ``` 60 | cd client 61 | npm install 62 | ``` 63 | 64 | Then we'll need to initiate both the backend and frontend servers. 65 | 66 | ``` 67 | cd .. 68 | npm start 69 | ``` 70 | 71 | Then navigate to [http://localhost:3000/login](http://localhost:3000/login) in 72 | order to use the application. 73 | 74 | ### Changing the application port 75 | 76 | You can change the port that the frontend server is configured to run on by 77 | settng the `PORT` varialbe in the `.env` file inside the `client` directory. 78 | An example is shown in `client/.example.env`. 79 | -------------------------------------------------------------------------------- /client/.example.env: -------------------------------------------------------------------------------- 1 | PORT= 2 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hangouts-clone-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://localhost:3001/", 6 | "devDependencies": { 7 | "react-scripts": "0.9.2", 8 | "redux-logger": "^2.8.1" 9 | }, 10 | "dependencies": { 11 | "aphrodite": "^1.1.0", 12 | "material-ui": "^0.16.7", 13 | "react": "^15.4.2", 14 | "react-dom": "^15.4.2", 15 | "react-google-login": "^2.8.1", 16 | "react-redux": "^5.0.2", 17 | "react-router": "^3.0.2", 18 | "react-router-redux": "^4.0.7", 19 | "react-tap-event-plugin": "^2.0.1", 20 | "redux": "^3.6.0", 21 | "redux-actions": "^1.2.1", 22 | "shortid": "^2.2.6", 23 | "twilio-video": "^1.0.0-beta4" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test --env=jsdom", 29 | "eject": "react-scripts eject" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/hangouts-clone-react/8b5fe467f9955a2c2d7717b53923cc19741a6fdc/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | Hangouts Clone with React 17 | 18 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /client/src/actions/constants.js: -------------------------------------------------------------------------------- 1 | export const SET_USER_TOKEN = 'SET_USER_TOKEN'; 2 | export const SET_USER_ID = 'SET_USER_ID'; 3 | export const SET_USER_NAME = 'SET_USER_NAME'; 4 | 5 | export const SET_ROOM_ID = 'SET_ROOM_ID'; 6 | export const CLEAR_ROOM = 'CLEAR_ROOM'; 7 | -------------------------------------------------------------------------------- /client/src/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as constants from './constants'; 2 | 3 | export const setUserToken = (token) => { 4 | return { 5 | type: constants.SET_USER_TOKEN, 6 | token, 7 | }; 8 | }; 9 | 10 | export const setUserId = (id) => { 11 | return { 12 | type: constants.SET_USER_ID, 13 | id, 14 | }; 15 | }; 16 | 17 | export const setUserName = (username) => { 18 | return { 19 | type: constants.SET_USER_NAME, 20 | username, 21 | }; 22 | }; 23 | 24 | export const setRoomId = (id) => { 25 | return { 26 | type: constants.SET_ROOM_ID, 27 | id, 28 | }; 29 | }; 30 | 31 | export const clearRoom = () => { 32 | return { 33 | type: constants.CLEAR_ROOM, 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /client/src/components/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, css } from 'aphrodite'; 3 | import injectTapEventPlugin from 'react-tap-event-plugin'; 4 | import MultiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 5 | 6 | injectTapEventPlugin(); 7 | 8 | const backgroundURL = "https://source.unsplash.com/category/nature/1600x900/daily"; 9 | const styles = StyleSheet.create({ 10 | background: { 11 | backgroundImage: `url(${backgroundURL})`, 12 | backgroundRepead: 'no-repeat', 13 | backgroundSize: 'cover', 14 | minHeight: '100%', 15 | minWidth: '100%', 16 | position: 'fixed', 17 | ':before': { 18 | content: '', 19 | position: 'absolute', 20 | top: 0, 21 | right: 0, 22 | bottom: 0, 23 | left: 0, 24 | background: 'radial-gradient(circle, #909090, #0c0c0c)', 25 | opacity: 0.6, 26 | } 27 | }, 28 | videoBackground: { 29 | background: 'black', 30 | minHeight: '100%', 31 | minWidth: '100%', 32 | }, 33 | }); 34 | 35 | class App extends React.Component { 36 | render() { 37 | const atVideo = this.props.location.pathname.includes("video"); 38 | return ( 39 | 40 |
41 | {this.props.children} 42 |
43 |
44 | ); 45 | } 46 | } 47 | 48 | export default App; 49 | -------------------------------------------------------------------------------- /client/src/components/login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { StyleSheet, css } from 'aphrodite'; 4 | import GoogleLogin from 'react-google-login'; 5 | 6 | import { 7 | setUserToken, 8 | setUserId, 9 | setUserName, 10 | } from '../actions'; 11 | 12 | const styles = StyleSheet.create({ 13 | content: { 14 | fontFamily: 'Roboto', 15 | color: '#fff', 16 | textAlign: 'left', 17 | textShadow: 'black 0.1em 0.1em 0.2em', 18 | fontWeight: '300', 19 | position: 'absolute', 20 | top: '50%', 21 | left: '50%', 22 | transform: 'translateX(-50%) translateY(-50%)', 23 | }, 24 | }); 25 | 26 | class Login extends React.Component { 27 | constructor() { 28 | super(); 29 | this.onSuccess = this.onSuccess.bind(this); 30 | this.onFailure = this.onFailure.bind(this); 31 | } 32 | 33 | onSuccess(response) { 34 | const tokenId = response.tokenId; 35 | const googleId = response.profileObj.googleId; 36 | const email = response.profileObj.email; 37 | fetch('/token', { 38 | method: 'POST', 39 | headers: { 40 | 'Accept': 'application/json', 41 | 'Content-Type': 'application/json' 42 | }, 43 | body: JSON.stringify({ 44 | tokenId: tokenId, 45 | googleId: googleId, 46 | }) 47 | }).then((response) => { 48 | if (response.status === 200) { 49 | return response.json(); 50 | } else { 51 | alert('Authentication via Google was unsuccessful. Please try again!'); 52 | } 53 | }).then((json) => { 54 | this.props.postSuccess(json.token, json.identity, email); 55 | const state = this.context.router.location.state; 56 | if (state && state.nextPathname) { 57 | this.context.router.push(state.nextPathname); 58 | } else { 59 | this.context.router.push('/main'); 60 | } 61 | }); 62 | } 63 | 64 | onFailure(response) { 65 | alert('Oops! There was an error while logging you in!'); 66 | } 67 | 68 | render() { 69 | return ( 70 |
71 |

Hi, there!

72 |

Get started by logging in with your Google Account.

73 | 78 |
79 | ); 80 | } 81 | } 82 | 83 | Login.contextTypes = { 84 | router: React.PropTypes.object.isRequired, 85 | }; 86 | 87 | export const mapDispatchToProps = (dispatch) => ({ 88 | postSuccess: (token, id, username) => { 89 | dispatch(setUserToken(token)); 90 | dispatch(setUserId(id)); 91 | dispatch(setUserName(username)); 92 | } 93 | }); 94 | 95 | export default connect(null, mapDispatchToProps)(Login); 96 | -------------------------------------------------------------------------------- /client/src/components/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import shortid from 'shortid'; 4 | import { StyleSheet, css } from 'aphrodite'; 5 | import VideoCall from 'material-ui/svg-icons/av/video-call'; 6 | 7 | import { 8 | setRoomId, 9 | } from '../actions'; 10 | 11 | const styles = StyleSheet.create({ 12 | content: { 13 | fontFamily: 'Roboto', 14 | color: '#fff', 15 | textAlign: 'left', 16 | textShadow: 'black 0.1em 0.1em 0.2em', 17 | fontWeight: '300', 18 | position: 'absolute', 19 | top: '50%', 20 | left: '50%', 21 | transform: 'translateX(-50%) translateY(-50%)', 22 | } 23 | }); 24 | 25 | class Main extends React.Component { 26 | constructor(props) { 27 | super(props); 28 | this.createRoom = this.createRoom.bind(this); 29 | } 30 | createRoom() { 31 | const id = shortid.generate(); 32 | this.props.createRoom(id); 33 | this.context.router.push(`/video/${id}`); 34 | } 35 | 36 | render() { 37 | return ( 38 |
39 |

Let's get started!

40 |

Click the button below to create a new video chat room!

41 |
42 | 50 |

VIDEO CALL

51 |
52 |
53 | ); 54 | } 55 | } 56 | 57 | Main.contextTypes = { 58 | router: React.PropTypes.object.isRequired, 59 | } 60 | 61 | export const mapDispatchToProps = (dispatch) => ({ 62 | createRoom: (id) => { 63 | dispatch(setRoomId(id)); 64 | } 65 | }); 66 | 67 | export default connect(null, mapDispatchToProps)(Main); 68 | -------------------------------------------------------------------------------- /client/src/components/video-chat.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Video from 'twilio-video'; 3 | import { StyleSheet, css } from 'aphrodite'; 4 | import { connect } from 'react-redux'; 5 | import MicOff from 'material-ui/svg-icons/av/mic-off'; 6 | import VideoCamOff from 'material-ui/svg-icons/av/videocam-off'; 7 | import CallEnd from 'material-ui/svg-icons/communication/call-end'; 8 | 9 | import { 10 | clearRoom 11 | } from '../actions'; 12 | 13 | const styles = StyleSheet.create({ 14 | toolbar: { 15 | backgroundColor: 'rgba(0, 0, 0, 0.75)', 16 | padding: '20px', 17 | margin: '0 auto', 18 | width: '200px', 19 | }, 20 | warning: { 21 | backgroundColor: 'rgba(0, 0, 0, 0.75)', 22 | padding: '30px', 23 | fontFamily: 'Roboto', 24 | color: 'red', 25 | position: 'absolute', 26 | top: '50%', 27 | left: '50%', 28 | transform: 'translateX(-50%) translateY(-50%)', 29 | }, 30 | icon: { 31 | padding: '20px', 32 | }, 33 | localMedia: { 34 | width: '480px', 35 | margin: '0 auto', 36 | }, 37 | }); 38 | 39 | class VideoChat extends React.Component { 40 | constructor(props) { 41 | super(props); 42 | 43 | this.muteAudio = this.muteAudio.bind(this); 44 | this.disableVideo = this.disableVideo.bind(this); 45 | this.exitRoom = this.exitRoom.bind(this); 46 | 47 | this.videoClient = new Video.Client(props.user.token); 48 | this.state = { 49 | activeRoom: null, 50 | muted: false, 51 | paused: false, 52 | }; 53 | } 54 | 55 | componentDidMount() { 56 | this.videoClient.connect({to: this.props.room.id}).then((room) => { 57 | this.setState({ 58 | activeRoom: room, 59 | }); 60 | room.on('participantConnected', (participant) => { 61 | participant.media.attach(this.remoteMedia); 62 | }); 63 | room.on('participantDisconnected', (participant) => { 64 | participant.media.detach(); 65 | }); 66 | room.on('disconnected', () => { 67 | room.localParticipant.media.detach(); 68 | room.participants.forEach((participant, key) => { 69 | participant.media.detach(); 70 | }); 71 | this.exitRoom(); 72 | }); 73 | room.localParticipant.media.attach(this.localMedia); 74 | if (room.participants) { 75 | room.participants.forEach((participant, key) => { 76 | participant.media.attach(this.remoteMedia); 77 | }); 78 | } 79 | this.setState({ 80 | activeRoom: room, 81 | }); 82 | }, (error) => { 83 | console.log(error); 84 | }); 85 | } 86 | 87 | muteAudio() { 88 | if (this.state.activeRoom.localParticipant.media.isMuted) { 89 | this.state.activeRoom.localParticipant.media.mute(false); 90 | this.setState({ 91 | muted: false, 92 | }); 93 | } else { 94 | this.state.activeRoom.localParticipant.media.mute(true); 95 | this.setState({ 96 | muted: true, 97 | }); 98 | } 99 | } 100 | 101 | disableVideo() { 102 | const videoTracks = this.state.activeRoom.localParticipant.media.videoTracks; 103 | if (this.state.activeRoom.localParticipant.media.isPaused) { 104 | videoTracks.forEach((value, key) => { 105 | value.enable(); 106 | }); 107 | this.setState({ 108 | paused: false, 109 | }); 110 | } else { 111 | videoTracks.forEach((value, key) => { 112 | value.disable(); 113 | }); 114 | this.setState({ 115 | paused: true, 116 | }); 117 | } 118 | } 119 | 120 | exitRoom() { 121 | this.setState({ 122 | activeRoom: false, 123 | }); 124 | this.state.activeRoom.disconnect(); 125 | this.props.clearRoom(); 126 | this.context.router.push('/main'); 127 | } 128 | 129 | render() { 130 | if (!navigator.webkitGetUserMedia && !navigator.mozGetUserMedia) { 131 | return ( 132 |
133 |

Oops! WebRTC is not available in your browser.

134 |
135 | ); 136 | } 137 | 138 | if (!this.state.activeRoom) { 139 | return false; 140 | } 141 | 142 | return ( 143 |
144 |
145 | 146 | 147 | 148 |
149 |
{ this.localMedia = localMedia; }}/> 150 |
{ this.remoteMedia = remoteMedia; }}/> 151 |
152 | ); 153 | } 154 | } 155 | 156 | VideoChat.contextTypes = { 157 | router: React.PropTypes.object.isRequired, 158 | }; 159 | 160 | const mapStateToProps = (state) => ({ 161 | user: state.user, 162 | room: state.room, 163 | }); 164 | 165 | const mapDispatchToProps = (dispatch) => ({ 166 | clearRoom: () => { 167 | dispatch(clearRoom()); 168 | } 169 | }); 170 | 171 | export default connect(mapStateToProps, mapDispatchToProps)(VideoChat); 172 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { syncHistoryWithStore } from 'react-router-redux'; 5 | import { Router, Route, browserHistory } from 'react-router'; 6 | 7 | import configureStore from './store/configureStore'; 8 | import preloadedState from './store/preloadedState'; 9 | 10 | import App from './components/app'; 11 | import Login from './components/login'; 12 | import Main from './components/main'; 13 | import VideoChat from './components/video-chat'; 14 | 15 | import { setRoomId } from './actions'; 16 | 17 | const store = configureStore(preloadedState); 18 | const history = syncHistoryWithStore(browserHistory, store); 19 | 20 | const validateState = (store) => { 21 | return (nextState, replace) => { 22 | const state = store.getState(); 23 | const token = state.user.token; 24 | const room = state.room.id; 25 | if (!token && !room) { 26 | store.dispatch(setRoomId(nextState.params.id)); 27 | replace({ 28 | pathname: '/login', 29 | state: { nextPathname: `/video/${nextState.params.id}` } 30 | }); 31 | } 32 | }; 33 | }; 34 | 35 | const routes = ( 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | 43 | ReactDOM.render( 44 | 45 | 46 | , 47 | document.getElementById('root')); 48 | -------------------------------------------------------------------------------- /client/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer as routing } from 'react-router-redux'; 3 | import user from './user'; 4 | import room from './room'; 5 | 6 | const rootReducer = combineReducers({ 7 | user, 8 | room, 9 | routing 10 | }); 11 | 12 | export default rootReducer; 13 | -------------------------------------------------------------------------------- /client/src/reducers/room.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions'; 2 | import * as constants from '../actions/constants'; 3 | 4 | export default handleActions({ 5 | [constants.SET_ROOM_ID]: function setRoomId(state, action) { 6 | return Object.assign({}, state, { 7 | id: action.id, 8 | }); 9 | }, 10 | [constants.CLEAR_ROOM]: function clearRoom(state, action) { 11 | return Object.assign({}, state, { 12 | id: null, 13 | }); 14 | }, 15 | }, {}); 16 | -------------------------------------------------------------------------------- /client/src/reducers/user.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions'; 2 | import * as constants from '../actions/constants'; 3 | 4 | export default handleActions({ 5 | [constants.SET_USER_TOKEN]: function setUserToken(state, action) { 6 | return Object.assign({}, state, { 7 | token: action.token, 8 | }); 9 | }, 10 | [constants.SET_USER_ID]: function setUserId(state, action) { 11 | return Object.assign({}, state, { 12 | id: action.id, 13 | }); 14 | }, 15 | [constants.SET_USER_NAME]: function setUserName(state, action) { 16 | return Object.assign({}, state, { 17 | username: action.username 18 | }); 19 | 20 | }, 21 | }, {}); 22 | -------------------------------------------------------------------------------- /client/src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import createLogger from 'redux-logger'; 3 | import rootReducer from '../reducers'; 4 | 5 | const configureStore = preloadedState => { 6 | const store = createStore( 7 | rootReducer, 8 | preloadedState, 9 | compose(applyMiddleware(createLogger())) 10 | ); 11 | 12 | return store; 13 | } 14 | 15 | export default configureStore; 16 | -------------------------------------------------------------------------------- /client/src/store/preloadedState.js: -------------------------------------------------------------------------------- 1 | const preloadedState = { 2 | user: { 3 | id: null, 4 | }, 5 | room: { 6 | id: null, 7 | }, 8 | }; 9 | 10 | export default preloadedState; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hangouts-clone-react", 3 | "version": "1.0.0", 4 | "description": "A clone of Google Hangouts using React and WebRTC", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "concurrently \"npm run server\" \"npm run client\"", 8 | "server": "babel-node server.js", 9 | "client": "babel-node start-client.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/TwilioDevEd/hangouts-clone-react.git" 14 | }, 15 | "author": "", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/TwilioDevEd/hangouts-clone-react/issues" 19 | }, 20 | "homepage": "https://github.com/TwilioDevEd/hangouts-clone-react#readme", 21 | "devDependencies": { 22 | "babel-cli": "^6.23.0", 23 | "concurrently": "^3.3.0" 24 | }, 25 | "dependencies": { 26 | "body-parser": "^1.16.1", 27 | "dotenv": "^4.0.0", 28 | "express": "^4.14.1", 29 | "google-auth-library": "^0.10.0", 30 | "twilio": "^2.11.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').load(); 2 | 3 | const GoogleAuth = require('google-auth-library'); 4 | const authentication = new GoogleAuth(); 5 | const client = new authentication.OAuth2(process.env.GOOGLE_CLIENT_ID); 6 | 7 | const AccessToken = require('twilio').AccessToken; 8 | const VideoGrant = AccessToken.VideoGrant; 9 | 10 | const express = require('express'); 11 | const bodyParser = require('body-parser'); 12 | 13 | const app = express(); 14 | 15 | app.use(bodyParser.urlencoded({ extended: false })); 16 | app.use(bodyParser.json()); 17 | 18 | app.post('/token', (request, response) => { 19 | const body = request.body; 20 | 21 | if (!body.tokenId && !body.googleId) { 22 | response.status(500); 23 | response.send({ 24 | error: 'Invalid payload provided!', 25 | }); 26 | } else { 27 | // Validate Goolge ID token sent from front-end 28 | client.verifyIdToken(body.tokenId, process.env.GOOGLE_CLIENT_ID, (e, login) => { 29 | const payload = login.getPayload(); 30 | const userId = payload.sub; 31 | if (userId === body.googleId) { 32 | const token = new AccessToken( 33 | process.env.TWILIO_ACCOUNT_SID, 34 | process.env.TWILIO_API_KEY, 35 | process.env.TWILIO_API_SECRET 36 | ); 37 | 38 | token.identity = body.googleId; 39 | 40 | const grant = new VideoGrant(); 41 | grant.configurationProfileSid = process.env.TWILIO_CONFIGURATION_SID; 42 | token.addGrant(grant); 43 | 44 | response.send({ 45 | identity: body.googleId, 46 | token: token.toJwt(), 47 | }); 48 | } else { 49 | response.status(500); 50 | response.send({ 51 | error: 'Authentication was unsuccessful!', 52 | }); 53 | } 54 | }); 55 | } 56 | }); 57 | 58 | app.listen(3001, () => { 59 | console.log("App listening on port 3001"); 60 | }); 61 | -------------------------------------------------------------------------------- /start-client.js: -------------------------------------------------------------------------------- 1 | const args = ['start']; 2 | const opts = { stdio: 'inherit', cwd: 'client', shell: true }; 3 | require('child_process').spawn('npm', args, opts); 4 | --------------------------------------------------------------------------------