├── .env.example
├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── server
├── config.js
├── index.js
└── tokens.js
└── src
├── App.css
├── App.js
├── App.test.js
├── Lobby.js
├── Participant.js
├── Room.js
├── VideoChat.js
├── index.css
├── index.js
├── logo.svg
└── serviceWorker.js
/.env.example:
--------------------------------------------------------------------------------
1 | TWILIO_ACCOUNT_SID=
2 | TWILIO_API_KEY=
3 | TWILIO_API_SECRET=
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
24 | .eslintcache
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019 Phil Nash
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Twilio Video chat with React Hooks
2 |
3 | This is an example video chat application built with [Twilio Video](https://www.twilio.com/docs/video) and React, using Hooks.
4 |
5 | Learn how to build this entire application in the blog post [Build a Twilio Video Chat with React Hooks](https://www.twilio.com/blog/video-chat-react-hooks).
6 |
7 | ## Preparing the application
8 |
9 | To run the application you will need a [Twilio account](https://www.twilio.com/try-twilio) and Node.js and npm installed. Start by cloning or downloading the repo to your machine.
10 |
11 | ```bash
12 | git clone https://github.com/philnash/twilio-video-react-hooks.git
13 | cd twilio-video-react-hooks
14 | ```
15 |
16 | Install the dependencies:
17 |
18 | ```bash
19 | npm install
20 | ```
21 |
22 | Create a `.env` file by copying the `.env.example`.
23 |
24 | ```bash
25 | cp .env.example .env
26 | ```
27 |
28 | ### Credentials
29 |
30 | You will need your Twilio Account SID, available in your [Twilio console](https://www.twilio.com/console). Add it to the `.env` file.
31 |
32 | You will also need an API key and secret, you can create these under the [Programmable Video Tools in your console](https://www.twilio.com/console/video/project/api-keys). Create a key pair and add them to the `.env` file too.
33 |
34 | ## Running the application
35 |
36 | Once you have completed the above you can run the application with:
37 |
38 | ```bash
39 | npm run dev
40 | ```
41 |
42 | This will open in your browser at [localhost:3000](http://localhost:3000).
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-express-starter",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "body-parser": "^1.19.0",
7 | "express": "^4.17.1",
8 | "express-pino-logger": "^6.0.0",
9 | "react": "^17.0.2",
10 | "react-dom": "^17.0.2",
11 | "react-scripts": "^5.0.1",
12 | "twilio": "^3.69.0",
13 | "twilio-video": "^2.17.1"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject",
20 | "server": "node-env-run server --exec nodemon | pino-colada",
21 | "dev": "run-p server start"
22 | },
23 | "proxy": "http://localhost:3001",
24 | "eslintConfig": {
25 | "extends": "react-app"
26 | },
27 | "browserslist": [
28 | ">0.2%",
29 | "not dead",
30 | "not ie <= 11",
31 | "not op_mini all"
32 | ],
33 | "devDependencies": {
34 | "@types/express": "^4.17.11",
35 | "@types/qs": "^6.9.4",
36 | "node-env-run": "^4.0.2",
37 | "nodemon": "^2.0.20",
38 | "npm-run-all": "^4.1.5",
39 | "pino-colada": "^2.2.0",
40 | "typescript": "^4.1.3"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/philnash/twilio-video-react-hooks/89ee30d839425a36a42642c67d8c84b1a18e8aeb/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
23 | React App
24 |
25 |
26 |
27 |
28 | You need to enable JavaScript to run this app.
29 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/server/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | twilio: {
3 | accountSid: process.env.TWILIO_ACCOUNT_SID,
4 | apiKey: process.env.TWILIO_API_KEY,
5 | apiSecret: process.env.TWILIO_API_SECRET
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const config = require('./config');
2 | const express = require('express');
3 | const bodyParser = require('body-parser');
4 | const pino = require('express-pino-logger')();
5 | const { videoToken } = require('./tokens');
6 |
7 | const app = express();
8 | app.use(bodyParser.urlencoded({ extended: false }));
9 | app.use(bodyParser.json());
10 | app.use(pino);
11 |
12 | const sendTokenResponse = (token, res) => {
13 | res.set('Content-Type', 'application/json');
14 | res.send(
15 | JSON.stringify({
16 | token: token.toJwt()
17 | })
18 | );
19 | };
20 |
21 | app.get('/api/greeting', (req, res) => {
22 | const name = req.query.name || 'World';
23 | res.setHeader('Content-Type', 'application/json');
24 | res.send(JSON.stringify({ greeting: `Hello ${name}!` }));
25 | });
26 |
27 | app.get('/video/token', (req, res) => {
28 | const identity = req.query.identity;
29 | const room = req.query.room;
30 | const token = videoToken(identity, room, config);
31 | sendTokenResponse(token, res);
32 |
33 | });
34 | app.post('/video/token', (req, res) => {
35 | const identity = req.body.identity;
36 | const room = req.body.room;
37 | const token = videoToken(identity, room, config);
38 | sendTokenResponse(token, res);
39 | });
40 |
41 | app.listen(3001, () =>
42 | console.log('Express server is running on localhost:3001')
43 | );
44 |
--------------------------------------------------------------------------------
/server/tokens.js:
--------------------------------------------------------------------------------
1 | const twilio = require('twilio');
2 | const AccessToken = twilio.jwt.AccessToken;
3 | const { VideoGrant } = AccessToken;
4 |
5 | const generateToken = config => {
6 | return new AccessToken(
7 | config.twilio.accountSid,
8 | config.twilio.apiKey,
9 | config.twilio.apiSecret
10 | );
11 | };
12 |
13 | const videoToken = (identity, room, config) => {
14 | let videoGrant;
15 | if (typeof room !== 'undefined') {
16 | videoGrant = new VideoGrant({ room });
17 | } else {
18 | videoGrant = new VideoGrant();
19 | }
20 | const token = generateToken(config);
21 | token.addGrant(videoGrant);
22 | token.identity = identity;
23 | return token;
24 | };
25 |
26 | module.exports = { videoToken };
27 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | html {
8 | height: 100%;
9 | }
10 |
11 | body {
12 | font-family: Helvetica, Arial, sans-serif;
13 | color: #333e5a;
14 | min-height: 100%;
15 | }
16 |
17 | header {
18 | background: #f0293e;
19 | color: #fff;
20 | text-align: center;
21 | flex-grow: 0;
22 | margin-bottom: 2em;
23 | }
24 |
25 | h1 {
26 | font-weight: 300;
27 | padding: 0.4em 0;
28 | }
29 |
30 | #root {
31 | min-height: 100vh;
32 | }
33 |
34 | .app {
35 | min-height: 100vh;
36 | display: flex;
37 | flex-direction: column;
38 | }
39 |
40 | main {
41 | background: #ffffff;
42 | flex-grow: 1;
43 | }
44 |
45 | form {
46 | max-width: 300px;
47 | margin: 0 auto;
48 | }
49 |
50 | h2 {
51 | font-weight: 300;
52 | margin-bottom: 1em;
53 | text-align: center;
54 | }
55 |
56 | form > div {
57 | width: 100%;
58 | margin-bottom: 1em;
59 | }
60 | form > div > label {
61 | display: block;
62 | margin-bottom: 0.3em;
63 | }
64 | form > div > input {
65 | display: block;
66 | width: 100%;
67 | font-size: 16px;
68 | padding: 0.4em;
69 | border-radius: 6px;
70 | border: 1px solid #333e5a;
71 | }
72 |
73 | button {
74 | background: #333e5a;
75 | color: #fff;
76 | font-size: 16px;
77 | padding: 0.4em;
78 | border-radius: 6px;
79 | border: 1px solid transparent;
80 | }
81 | button:hover {
82 | filter: brightness(150%);
83 | }
84 |
85 | .room {
86 | position: relative;
87 | }
88 | .room button {
89 | position: absolute;
90 | top: 0;
91 | right: 20px;
92 | }
93 | .room > h3 {
94 | text-align: center;
95 | font-weight: 300;
96 | margin-bottom: 1em;
97 | }
98 |
99 | .local-participant {
100 | text-align: center;
101 | margin-bottom: 2em;
102 | }
103 | .remote-participants {
104 | display: flex;
105 | flex-wrap: nowrap;
106 | justify-content: space-between;
107 | padding: 0 2em 2em;
108 | }
109 | .participant {
110 | background: #333e5a;
111 | padding: 10px;
112 | border-radius: 6px;
113 | display: inline-block;
114 | margin-right: 10px;
115 | }
116 | .participant:last-child {
117 | margin-right: 0;
118 | }
119 | .participant h3 {
120 | text-align: center;
121 | padding-bottom: 0.5em;
122 | color: #fff;
123 | }
124 |
125 | video {
126 | width: 100%;
127 | max-width: 600px;
128 | display: block;
129 | margin: 0 auto;
130 | border-radius: 6px;
131 | }
132 |
133 | footer {
134 | background: #333e5a;
135 | color: #fff;
136 | text-align: center;
137 | flex-grow: 0;
138 | padding: 1em 0;
139 | }
140 |
141 | footer a {
142 | color: #fff;
143 | }
144 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './App.css';
3 | import VideoChat from './VideoChat';
4 |
5 | const App = () => {
6 | return (
7 |
8 |
9 | Video Chat with Hooks
10 |
11 |
12 |
13 |
14 |
23 |
24 | );
25 | };
26 |
27 | export default App;
28 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render( , div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/Lobby.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Lobby = ({
4 | username,
5 | handleUsernameChange,
6 | roomName,
7 | handleRoomNameChange,
8 | handleSubmit,
9 | connecting,
10 | }) => {
11 | return (
12 |
42 | );
43 | };
44 |
45 | export default Lobby;
46 |
--------------------------------------------------------------------------------
/src/Participant.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 |
3 | const Participant = ({ participant }) => {
4 | const [videoTracks, setVideoTracks] = useState([]);
5 | const [audioTracks, setAudioTracks] = useState([]);
6 |
7 | const videoRef = useRef();
8 | const audioRef = useRef();
9 |
10 | const trackpubsToTracks = (trackMap) =>
11 | Array.from(trackMap.values())
12 | .map((publication) => publication.track)
13 | .filter((track) => track !== null);
14 |
15 | useEffect(() => {
16 | setVideoTracks(trackpubsToTracks(participant.videoTracks));
17 | setAudioTracks(trackpubsToTracks(participant.audioTracks));
18 |
19 | const trackSubscribed = (track) => {
20 | if (track.kind === "video") {
21 | setVideoTracks((videoTracks) => [...videoTracks, track]);
22 | } else if (track.kind === "audio") {
23 | setAudioTracks((audioTracks) => [...audioTracks, track]);
24 | }
25 | };
26 |
27 | const trackUnsubscribed = (track) => {
28 | if (track.kind === "video") {
29 | setVideoTracks((videoTracks) => videoTracks.filter((v) => v !== track));
30 | } else if (track.kind === "audio") {
31 | setAudioTracks((audioTracks) => audioTracks.filter((a) => a !== track));
32 | }
33 | };
34 |
35 | participant.on("trackSubscribed", trackSubscribed);
36 | participant.on("trackUnsubscribed", trackUnsubscribed);
37 |
38 | return () => {
39 | setVideoTracks([]);
40 | setAudioTracks([]);
41 | participant.removeAllListeners();
42 | };
43 | }, [participant]);
44 |
45 | useEffect(() => {
46 | const videoTrack = videoTracks[0];
47 | if (videoTrack) {
48 | videoTrack.attach(videoRef.current);
49 | return () => {
50 | videoTrack.detach();
51 | };
52 | }
53 | }, [videoTracks]);
54 |
55 | useEffect(() => {
56 | const audioTrack = audioTracks[0];
57 | if (audioTrack) {
58 | audioTrack.attach(audioRef.current);
59 | return () => {
60 | audioTrack.detach();
61 | };
62 | }
63 | }, [audioTracks]);
64 |
65 | return (
66 |
67 |
{participant.identity}
68 |
69 |
70 |
71 | );
72 | };
73 |
74 | export default Participant;
75 |
--------------------------------------------------------------------------------
/src/Room.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import Participant from "./Participant";
3 |
4 | const Room = ({ roomName, room, handleLogout }) => {
5 | const [participants, setParticipants] = useState([]);
6 |
7 | useEffect(() => {
8 | const participantConnected = (participant) => {
9 | setParticipants((prevParticipants) => [...prevParticipants, participant]);
10 | };
11 |
12 | const participantDisconnected = (participant) => {
13 | setParticipants((prevParticipants) =>
14 | prevParticipants.filter((p) => p !== participant)
15 | );
16 | };
17 |
18 | room.on("participantConnected", participantConnected);
19 | room.on("participantDisconnected", participantDisconnected);
20 | room.participants.forEach(participantConnected);
21 | return () => {
22 | room.off("participantConnected", participantConnected);
23 | room.off("participantDisconnected", participantDisconnected);
24 | };
25 | }, [room]);
26 |
27 | const remoteParticipants = participants.map((participant) => (
28 |
29 | ));
30 |
31 | return (
32 |
33 |
Room: {roomName}
34 |
Log out
35 |
36 | {room ? (
37 |
41 | ) : (
42 | ""
43 | )}
44 |
45 |
Remote Participants
46 |
{remoteParticipants}
47 |
48 | );
49 | };
50 |
51 | export default Room;
52 |
--------------------------------------------------------------------------------
/src/VideoChat.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback, useEffect } from "react";
2 | import Video from "twilio-video";
3 | import Lobby from "./Lobby";
4 | import Room from "./Room";
5 |
6 | const VideoChat = () => {
7 | const [username, setUsername] = useState("");
8 | const [roomName, setRoomName] = useState("");
9 | const [room, setRoom] = useState(null);
10 | const [connecting, setConnecting] = useState(false);
11 |
12 | const handleUsernameChange = useCallback((event) => {
13 | setUsername(event.target.value);
14 | }, []);
15 |
16 | const handleRoomNameChange = useCallback((event) => {
17 | setRoomName(event.target.value);
18 | }, []);
19 |
20 | const handleSubmit = useCallback(
21 | async (event) => {
22 | event.preventDefault();
23 | setConnecting(true);
24 | const data = await fetch("/video/token", {
25 | method: "POST",
26 | body: JSON.stringify({
27 | identity: username,
28 | room: roomName,
29 | }),
30 | headers: {
31 | "Content-Type": "application/json",
32 | },
33 | }).then((res) => res.json());
34 | Video.connect(data.token, {
35 | name: roomName,
36 | })
37 | .then((room) => {
38 | setConnecting(false);
39 | setRoom(room);
40 | })
41 | .catch((err) => {
42 | console.error(err);
43 | setConnecting(false);
44 | });
45 | },
46 | [roomName, username]
47 | );
48 |
49 | const handleLogout = useCallback(() => {
50 | setRoom((prevRoom) => {
51 | if (prevRoom) {
52 | prevRoom.localParticipant.tracks.forEach((trackPub) => {
53 | trackPub.track.stop();
54 | });
55 | prevRoom.disconnect();
56 | }
57 | return null;
58 | });
59 | }, []);
60 |
61 | useEffect(() => {
62 | if (room) {
63 | const tidyUp = (event) => {
64 | if (event.persisted) {
65 | return;
66 | }
67 | if (room) {
68 | handleLogout();
69 | }
70 | };
71 | window.addEventListener("pagehide", tidyUp);
72 | window.addEventListener("beforeunload", tidyUp);
73 | return () => {
74 | window.removeEventListener("pagehide", tidyUp);
75 | window.removeEventListener("beforeunload", tidyUp);
76 | };
77 | }
78 | }, [room, handleLogout]);
79 |
80 | let render;
81 | if (room) {
82 | render = (
83 |
84 | );
85 | } else {
86 | render = (
87 |
95 | );
96 | }
97 | return render;
98 | };
99 |
100 | export default VideoChat;
101 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render( , document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: http://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export function register(config) {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Let's check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl, config);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl, config);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl, config) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 |
70 | // Execute callback
71 | if (config.onUpdate) {
72 | config.onUpdate(registration);
73 | }
74 | } else {
75 | // At this point, everything has been precached.
76 | // It's the perfect time to display a
77 | // "Content is cached for offline use." message.
78 | console.log('Content is cached for offline use.');
79 |
80 | // Execute callback
81 | if (config.onSuccess) {
82 | config.onSuccess(registration);
83 | }
84 | }
85 | }
86 | };
87 | };
88 | })
89 | .catch(error => {
90 | console.error('Error during service worker registration:', error);
91 | });
92 | }
93 |
94 | function checkValidServiceWorker(swUrl, config) {
95 | // Check if the service worker can be found. If it can't reload the page.
96 | fetch(swUrl)
97 | .then(response => {
98 | // Ensure service worker exists, and that we really are getting a JS file.
99 | if (
100 | response.status === 404 ||
101 | response.headers.get('content-type').indexOf('javascript') === -1
102 | ) {
103 | // No service worker found. Probably a different app. Reload the page.
104 | navigator.serviceWorker.ready.then(registration => {
105 | registration.unregister().then(() => {
106 | window.location.reload();
107 | });
108 | });
109 | } else {
110 | // Service worker found. Proceed as normal.
111 | registerValidSW(swUrl, config);
112 | }
113 | })
114 | .catch(() => {
115 | console.log(
116 | 'No internet connection found. App is running in offline mode.'
117 | );
118 | });
119 | }
120 |
121 | export function unregister() {
122 | if ('serviceWorker' in navigator) {
123 | navigator.serviceWorker.ready.then(registration => {
124 | registration.unregister();
125 | });
126 | }
127 | }
128 |
--------------------------------------------------------------------------------