├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.js ├── App.test.js ├── components │ ├── Chat.js │ ├── EncryptedMessage.js │ ├── EncryptedMessageInput.js │ ├── KeyDeriver.js │ ├── Recipient.js │ └── Sender.js ├── index.css ├── index.js ├── lib │ ├── chatClient.js │ ├── decrypt.js │ ├── deriveKey.js │ ├── encrypt.js │ ├── generateKeyPair.js │ └── setUser.js ├── logo.svg ├── serviceWorker.js └── setupTests.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔐💬 Encrypted Web Chat [![](https://img.shields.io/twitter/url?url=https%3A%2F%2Fgithub.com%2FGetStream%2Fencrypted-web-chat)](https://twitter.com/intent/tweet?text=Want%20to%20secure%20your%20web%20chat%20app%20with%20the%20Web%20Crypto%20API%3F%20Learn%20how%3A&url=https%3A%2F%2Fgithub.com%2FGetStream%2Fencrypted-web-chat) 2 | 3 | 4 | 5 | ## 📚 Tutorial 6 | 7 | This repository contains the completed React project following the [End-to-End Encrypted Chat with the Web Crypto API](https://getstream.io/blog/web-crypto-api-chat/) tutorial. You should read it before trying to run this project as it contains it may contain useful information not present in this README. 8 | 9 | ## ⚙️ Setup 10 | 11 | ## Requirements 12 | - [Yarn](https://yarnpkg.com/) 13 | - A [Stream](https://getstream.io/accounts/signup/) account. 14 | 15 | ### Configuration 16 | 17 | You should place your [Stream Chat](https://getstream.io/chat) credentials in [`src/lib/chatClient.js`](src/lib/chatClient.js). 18 | 19 | ### Dependencies 20 | 21 | To install the dependencies, use Yarn in the root folder: 22 | 23 | ```bash 24 | $ yarn install 25 | ``` 26 | 27 | ### Running 28 | 29 | To run the web app, use Yarn in the root folder: 30 | 31 | ```bash 32 | $ yarn start 33 | ``` 34 | 35 | ## 🔗 Helpful Links 36 | 37 | - [W3C: Web Crypto API](https://getstream.io/blog/build-imessage-clone/) 38 | - [MDN: Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) 39 | - [Stream Chat React Tutorial](https://getstream.io/tutorials/react-chat/) 40 | - [Stream Chat React Repo](https://github.com/GetStream/stream-chat-react/) 41 | - [Stream Chat React Docs](https://getstream.io/chat/docs/introduction/?language=js) 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encrypted-web-chat", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "express": "^4.17.1", 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1", 12 | "react-scripts": "3.4.3", 13 | "stream-chat": "^2.4.0", 14 | "stream-chat-react": "^3.0.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/encrypted-web-chat/f2b14a29bffc837b6e05353c91cd8d30b1d4fafe/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/encrypted-web-chat/f2b14a29bffc837b6e05353c91cd8d30b1d4fafe/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/encrypted-web-chat/f2b14a29bffc837b6e05353c91cd8d30b1d4fafe/public/logo512.png -------------------------------------------------------------------------------- /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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import "./App.css"; 3 | import Chat from "./components/Chat"; 4 | import Sender from "./components/Sender"; 5 | import Recipient from "./components/Recipient"; 6 | import KeyDeriver from "./components/KeyDeriver"; 7 | 8 | function App() { 9 | const [sender, setSender] = useState(null); 10 | const [recipient, setRecipient] = useState(null); 11 | const [derivedKey, setDerivedKey] = useState(null); 12 | 13 | if (!sender) return ; 14 | 15 | if (!recipient) return ; 16 | 17 | if (!derivedKey) 18 | return ( 19 | 24 | ); 25 | 26 | return ; 27 | } 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react"; 3 | import App from "./App"; 4 | 5 | test("renders learn react link", () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/Chat.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, memo } from "react"; 2 | import { 3 | Chat, 4 | Channel, 5 | ChannelHeader, 6 | Thread, 7 | Window, 8 | Message, 9 | } from "stream-chat-react"; 10 | import { MessageList, MessageInput, SendButton } from "stream-chat-react"; 11 | 12 | import chatClient from "../lib/chatClient"; 13 | 14 | import "stream-chat-react/dist/css/index.css"; 15 | import EncryptedMessage from "./EncryptedMessage"; 16 | import EncryptedMessageInput from "./EncryptedMessageInput"; 17 | 18 | export default (props) => { 19 | const [channel, setChannel] = useState(null); 20 | 21 | useEffect(() => { 22 | setChannel( 23 | chatClient.channel("messaging", { 24 | members: [props.sender.sender, props.recipient.recipient], 25 | }) 26 | ); 27 | }, [props]); 28 | 29 | if (!channel) return
Loading...
; 30 | 31 | return ( 32 | 33 | 34 | 35 | 36 | ( 38 | 39 | )} 40 | /> 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/EncryptedMessage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react" 2 | import { MessageSimple } from "stream-chat-react" 3 | import decrypt from "../lib/decrypt"; 4 | 5 | // builds a message component that decrypts the message with derivedKey 6 | export default props => { 7 | const [message, setMessage] = useState(props.message); 8 | 9 | useEffect(() => { 10 | const work = async () => { 11 | setMessage({ 12 | ...message, 13 | text: await decrypt(props.message.text, props.derivedKey) 14 | }) 15 | } 16 | 17 | work() 18 | }, [props]) 19 | 20 | return ( 21 | 22 | ) 23 | } -------------------------------------------------------------------------------- /src/components/EncryptedMessageInput.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { MessageInputLarge, ChannelContext } from "stream-chat-react"; 3 | import encrypt from "../lib/encrypt"; 4 | 5 | export default (props) => { 6 | const channelContext = useContext(ChannelContext); 7 | const sendMessage = async (message, channelCid) => { 8 | const newMessage = { 9 | ...message, 10 | text: await encrypt(message.text, props.derivedKey), 11 | }; 12 | 13 | await channelContext.channel.sendMessage(newMessage); 14 | }; 15 | 16 | return ; 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/KeyDeriver.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import deriveKey from "../lib/deriveKey"; 3 | 4 | export default (props) => { 5 | const { sender, recipient, onSubmit } = props; 6 | const [error, setError] = useState(""); 7 | 8 | useEffect(() => { 9 | const derive = async () => { 10 | try { 11 | const derivedKey = await deriveKey( 12 | recipient.publicKeyJwk, 13 | sender.keyPair.privateKeyJwk 14 | ); 15 | onSubmit(derivedKey); 16 | } catch (e) { 17 | setError(e.message); 18 | } 19 | }; 20 | 21 | derive(); 22 | }, [sender, recipient]); 23 | 24 | return ( 25 |
26 |

Deriving key...

27 |

{error}

28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/Recipient.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import chatClient from "../lib/chatClient"; 3 | 4 | export default (props) => { 5 | const [recipient, setRecipient] = useState(""); 6 | const [error, setError] = useState(""); 7 | 8 | const handleClick = () => { 9 | const logIn = async () => { 10 | try { 11 | const usersQuery = await chatClient.queryUsers({ id: recipient }); 12 | 13 | if (usersQuery.users.length > 0) { 14 | const publicKeyJwk = JSON.parse(usersQuery.users[0].publicKeyJwk); 15 | props.onSubmit({ recipient, publicKeyJwk }); 16 | } else { 17 | setError( 18 | "This user is not registered. Open a new tab and create it? :)" 19 | ); 20 | } 21 | } catch (e) { 22 | setError(`Error setting recipient: ${e.message}`); 23 | } 24 | }; 25 | 26 | logIn(); 27 | }; 28 | 29 | return ( 30 |
31 |

Who do you want to chat with?

32 | setRecipient(e.target.value)} /> 33 |
34 |
35 | 36 |

{error}

37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/Sender.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import setUser from "../lib/setUser"; 3 | import generateKeyPair from "../lib/generateKeyPair"; 4 | 5 | export default (props) => { 6 | const [keyPair, setKeyPair] = useState(null); 7 | const [sender, setSender] = useState(""); 8 | const [error, setError] = useState(""); 9 | 10 | const handleKeyPairInputChange = (e) => { 11 | try { 12 | const keyPair = JSON.parse(e.target.value); 13 | setKeyPair(keyPair); 14 | } catch (e) { 15 | setError(`Error reading key pair: ${e}`); 16 | setKeyPair(keyPair); 17 | } 18 | }; 19 | 20 | const handleGenerateClick = async () => { 21 | setKeyPair(await generateKeyPair()); 22 | }; 23 | 24 | const handleSubmit = () => { 25 | const work = async () => { 26 | try { 27 | await setUser(sender, keyPair); 28 | props.onSubmit({ sender, keyPair }); 29 | } catch (e) { 30 | setError(`Error logging in: ${e}`); 31 | } 32 | }; 33 | 34 | work(); 35 | }; 36 | 37 | return ( 38 |
39 |

What is your id?

40 | setSender(e.target.value)} /> 41 |

Avoid spaces and special characters.

42 |

43 | Paste your key pair below or a new one. 44 |

45 | 49 |

50 | You need to save this key pair somewhere safe if you want to log in with 51 | the same user id later. 52 |

53 | 54 |

{error}

55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | padding-left: 10px; 9 | max-width: 600px; 10 | } 11 | 12 | code { 13 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 14 | monospace; 15 | } 16 | -------------------------------------------------------------------------------- /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( 8 | 9 | 10 | , 11 | document.getElementById("root") 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /src/lib/chatClient.js: -------------------------------------------------------------------------------- 1 | import { StreamChat } from "stream-chat"; 2 | 3 | export default new StreamChat("[api_key]"); 4 | -------------------------------------------------------------------------------- /src/lib/decrypt.js: -------------------------------------------------------------------------------- 1 | export default async (text, derivedKey) => { 2 | try { 3 | const string = atob(text); 4 | const uintArray = new Uint8Array( 5 | [...string].map((char) => char.charCodeAt(0)) 6 | ); 7 | const algorithm = { 8 | name: "AES-GCM", 9 | iv: new TextEncoder().encode("Initialization Vector"), 10 | }; 11 | const decryptedData = await window.crypto.subtle.decrypt( 12 | algorithm, 13 | derivedKey, 14 | uintArray 15 | ); 16 | 17 | return new TextDecoder().decode(decryptedData); 18 | } catch (e) { 19 | return `error decrypting message: ${e}`; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/lib/deriveKey.js: -------------------------------------------------------------------------------- 1 | export default async (publicKeyJwk, privateKeyJwk) => { 2 | const publicKey = await window.crypto.subtle.importKey( 3 | "jwk", 4 | publicKeyJwk, 5 | { 6 | name: "ECDH", 7 | namedCurve: "P-256", 8 | }, 9 | true, 10 | [] 11 | ); 12 | 13 | const privateKey = await window.crypto.subtle.importKey( 14 | "jwk", 15 | privateKeyJwk, 16 | { 17 | name: "ECDH", 18 | namedCurve: "P-256", 19 | }, 20 | true, 21 | ["deriveKey", "deriveBits"] 22 | ); 23 | 24 | return await window.crypto.subtle.deriveKey( 25 | { name: "ECDH", public: publicKey }, 26 | privateKey, 27 | { name: "AES-GCM", length: 256 }, 28 | true, 29 | ["encrypt", "decrypt"] 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/lib/encrypt.js: -------------------------------------------------------------------------------- 1 | export default async (text, derivedKey) => { 2 | const encodedText = new TextEncoder().encode(text); 3 | 4 | const encryptedData = await window.crypto.subtle.encrypt( 5 | { name: "AES-GCM", iv: new TextEncoder().encode("Initialization Vector") }, 6 | derivedKey, 7 | encodedText 8 | ); 9 | 10 | const uintArray = new Uint8Array(encryptedData); 11 | 12 | const string = String.fromCharCode.apply(null, uintArray); 13 | 14 | const base64Data = btoa(string); 15 | 16 | return base64Data; 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/generateKeyPair.js: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | const keyPair = await window.crypto.subtle.generateKey( 3 | { 4 | name: "ECDH", 5 | namedCurve: "P-256", 6 | }, 7 | true, 8 | ["deriveKey", "deriveBits"] 9 | ); 10 | 11 | const publicKeyJwk = await window.crypto.subtle.exportKey( 12 | "jwk", 13 | keyPair.publicKey 14 | ); 15 | 16 | const privateKeyJwk = await window.crypto.subtle.exportKey( 17 | "jwk", 18 | keyPair.privateKey 19 | ); 20 | 21 | return { publicKeyJwk, privateKeyJwk }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/setUser.js: -------------------------------------------------------------------------------- 1 | import chatClient from "./chatClient"; 2 | 3 | export default async (id, keyPair) => { 4 | const response = await chatClient.setUser( 5 | { 6 | id, 7 | name: id, 8 | image: `https://getstream.io/random_png/?id=cool-recipe-9&name=${id}`, 9 | }, 10 | chatClient.devToken(id) 11 | ); 12 | 13 | if ( 14 | response.me?.publicKeyJwk && 15 | response.me.publicKeyJwk != JSON.stringify(keyPair.publicKeyJwk) 16 | ) { 17 | await chatClient.disconnect(); 18 | throw "This user id already exists with a different key pair. Choose a new user id or paste the correct key pair."; 19 | } 20 | 21 | await chatClient.upsertUsers([ 22 | { id, publicKeyJwk: JSON.stringify(keyPair.publicKeyJwk) }, 23 | ]); 24 | }; 25 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === "localhost" || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === "[::1]" || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener("load", () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | "This web app is being served cache-first by a service " + 46 | "worker. To learn more, visit https://bit.ly/CRA-PWA" 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then((registration) => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === "installed") { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | "New content is available and will be used when all " + 74 | "tabs for this page are closed. See https://bit.ly/CRA-PWA." 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log("Content is cached for offline use."); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch((error) => { 97 | console.error("Error during service worker registration:", error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { "Service-Worker": "script" }, 105 | }) 106 | .then((response) => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get("content-type"); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf("javascript") === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then((registration) => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | "No internet connection found. App is running in offline mode." 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ("serviceWorker" in navigator) { 133 | navigator.serviceWorker.ready 134 | .then((registration) => { 135 | registration.unregister(); 136 | }) 137 | .catch((error) => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom/extend-expect"; 6 | --------------------------------------------------------------------------------