summary refs log tree commit diff stats
path: root/client/src
diff options
context:
space:
mode:
Diffstat (limited to 'client/src')
-rw-r--r--client/src/App.css191
-rw-r--r--client/src/App.tsx215
-rw-r--r--client/src/components/ConnectWallet.tsx137
-rw-r--r--client/src/components/DisconnectWallet.tsx50
-rw-r--r--client/src/components/Records.tsx49
-rw-r--r--client/src/components/Transfers.tsx70
-rw-r--r--client/src/index.js16
-rw-r--r--client/src/react-app-env.d.ts1
-rw-r--r--client/src/serviceWorker.js141
9 files changed, 870 insertions, 0 deletions
diff --git a/client/src/App.css b/client/src/App.css
new file mode 100644
index 0000000..a73598f
--- /dev/null
+++ b/client/src/App.css
@@ -0,0 +1,191 @@
+html,
+body,
+#root {
+    height: 100%;
+    width: 100%;
+    margin: 0px;
+    padding: 0px;
+    font-size: 1rem;
+}
+
+#root {
+    display: grid;
+}
+
+.main-box {
+    margin: auto;
+}
+
+.title {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    align-items: center;
+}
+
+h1 {
+    color: #35636e;
+    text-align: left;
+}
+
+header {
+    font-size: 1.5rem;
+    padding: 20px;
+    background-color: #35636e;
+    color: white;
+}
+
+#dialog {
+    display: flex;
+    flex-direction: column;
+    width: 96vw;
+    max-width: 720px;
+    border: 1px solid #35636e;
+    border-radius: 3px;
+}
+
+#content {
+    display: flex;
+    flex-direction: column;
+    padding: 20px;
+}
+
+#footer {
+    display: flex;
+    justify-content: flex-end;
+    padding-top: 20px;
+}
+
+.buttons {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-around;
+    padding: 0px 10px 20px 10px;
+}
+
+.button {
+    margin-top: 0px;
+    margin-bottom: 5px;
+    color: #35636e;
+    font-size: 1rem;
+    padding: 15px;
+    background-color: white;
+    border-radius: 3px;
+    border: solid 2px #35636e;
+    outline: none;
+    cursor: pointer;
+    transition: 0.2s;
+}
+.button:hover {
+    color: white;
+    background-color: #35636e;
+}
+.button:active {
+    margin-top: 5px;
+    margin-bottom: 0px;
+}
+
+#public-token {
+    word-break: break-word;
+}
+#public-token-copy,
+#public-token-copy__copied {
+    float: right;
+    text-align: center;
+    margin: 15px 10px;
+    padding: 5px;
+    cursor: pointer;
+    transition: 0.2s;
+}
+#public-token-copy:hover {
+    border-radius: 3px;
+    color: white;
+    background-color: #35636e;
+}
+
+.text-align-center {
+    text-align: center;
+}
+
+a {
+    text-decoration: none;
+    color: royalblue;
+}
+a:hover {
+    font-style: italic;
+}
+
+#tabs {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-around;
+    border: none;
+    color: #35636e;
+    margin: 0px;
+    transition: 0.3s;
+}
+
+#tabs div {
+    width: 80%;
+    padding: 16px;
+    margin: 0px 10px;
+    border: solid 1px #35636e;
+    border-bottom: none;
+    text-align: center;
+    border-top-left-radius: 3px;
+    border-top-right-radius: 3px;
+    cursor: pointer;
+}
+
+#tabs div.active {
+    background-color: #35636e;
+    color: white;
+}
+
+#transfer-inputs {
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+    align-items: center;
+    padding: 0px 10px 20px 10px;
+}
+
+#transfer-inputs input[type="text"] {
+    padding: 15px;
+    border-top-left-radius: 3px;
+    border-bottom-left-radius: 3px;
+    border: solid 2px #35636e;
+    outline: none;
+    font-size: 1rem;
+    border-right: none;
+    margin-top: 0px;
+    margin-bottom: 5px;
+}
+
+#transfer-inputs input[type="number"] {
+    padding: 15px;
+    width: 60px;
+    border: solid 2px #35636e;
+    border-top-right-radius: 3px;
+    border-bottom-right-radius: 3px;
+    border-left: none;
+    outline: none;
+    margin-right: 5px;
+    font-size: 1rem;
+    margin-top: 0px;
+    margin-bottom: 5px;
+    text-align: right;
+}
+
+/* Removes arrows in input number */
+/* Chrome, Safari, Edge, Opera */
+input::-webkit-outer-spin-button,
+input::-webkit-inner-spin-button {
+    -webkit-appearance: none;
+    margin: 0;
+}
+
+/* Firefox */
+input[type="number"] {
+    -moz-appearance: textfield;
+}
diff --git a/client/src/App.tsx b/client/src/App.tsx
new file mode 100644
index 0000000..10a6568
--- /dev/null
+++ b/client/src/App.tsx
@@ -0,0 +1,215 @@
+import React, { useState } from "react";
+import { TezosToolkit } from "@taquito/taquito";
+import "./App.css";
+import ConnectButton from "./components/ConnectWallet";
+import DisconnectButton from "./components/DisconnectWallet";
+import qrcode from "qrcode-generator";
+import Records from "./components/Records";
+import Transfers from "./components/Transfers";
+
+enum BeaconConnection {
+  NONE = "",
+  LISTENING = "Listening to P2P channel",
+  CONNECTED = "Channel connected",
+  PERMISSION_REQUEST_SENT = "Permission request sent, waiting for response",
+  PERMISSION_REQUEST_SUCCESS = "Wallet is connected"
+}
+
+const App = () => {
+  const [Tezos, setTezos] = useState<TezosToolkit>(
+    new TezosToolkit("https://api.tez.ie/rpc/granadanet")
+  );
+  const [contract, setContract] = useState<any>(undefined);
+  const [publicToken, setPublicToken] = useState<string | null>("");
+  const [wallet, setWallet] = useState<any>(null);
+  const [userAddress, setUserAddress] = useState<string>("");
+  const [userBalance, setUserBalance] = useState<number>(0);
+  const [storage, setStorage] = useState<any>({dates: []});
+  const [copiedPublicToken, setCopiedPublicToken] = useState<boolean>(false);
+  const [beaconConnection, setBeaconConnection] = useState<boolean>(false);
+  const [activeTab, setActiveTab] = useState<string>("transfer");
+
+  // Granadanet Memoir contract
+  const contractAddress: string = "KT1T4idqQZpt1Ayn9SRNqCo7PsiBreT5YsMB";
+
+  const generateQrCode = (): { __html: string } => {
+    const qr = qrcode(0, "L");
+    qr.addData(publicToken || "");
+    qr.make();
+
+    return { __html: qr.createImgTag(4) };
+  };
+
+  if (publicToken && (!userAddress || isNaN(userBalance))) {
+    return (
+      <div className="main-box">
+        <h1>MyMedMemoir</h1>
+        <div id="dialog">
+          <header>Try the Taquito Boilerplate App!</header>
+          <div id="content">
+            <p className="text-align-center">
+              <i className="fas fa-broadcast-tower"></i>&nbsp; Connecting to
+              your wallet
+            </p>
+            <div
+              dangerouslySetInnerHTML={generateQrCode()}
+              className="text-align-center"
+            ></div>
+            <p id="public-token">
+              {copiedPublicToken ? (
+                <span id="public-token-copy__copied">
+                  <i className="far fa-thumbs-up"></i>
+                </span>
+              ) : (
+                <span
+                  id="public-token-copy"
+                  onClick={() => {
+                    if (publicToken) {
+                      navigator.clipboard.writeText(publicToken);
+                      setCopiedPublicToken(true);
+                      setTimeout(() => setCopiedPublicToken(false), 2000);
+                    }
+                  }}
+                >
+                  <i className="far fa-copy"></i>
+                </span>
+              )}
+
+              <span>
+                Public token: <span>{publicToken}</span>
+              </span>
+            </p>
+            <p className="text-align-center">
+              Status: {beaconConnection ? "Connected" : "Disconnected"}
+            </p>
+          </div>
+        </div>
+        <div id="footer">
+        </div>
+      </div>
+    );
+  } else if (userAddress && !isNaN(userBalance)) {
+    return (
+      <div className="main-box">
+        <h1>Memoir</h1>
+        <div id="tabs">
+          <div
+            id="transfer"
+            className={activeTab === "transfer" ? "active" : ""}
+            onClick={() => setActiveTab("transfer")}
+          >
+            Wallet
+          </div>
+          <div
+            id="contract"
+            className={activeTab === "contract" ? "active" : ""}
+            onClick={() => setActiveTab("contract")}
+          >
+            New Report
+          </div>
+          <div
+            id="reports"
+            className={activeTab === "reports" ? "active" : ""}
+            onClick={() => setActiveTab("reports")}
+          >
+            Reports
+          </div>
+        </div>
+        <div id="dialog">
+          <div id="content">
+            {activeTab === "transfer" && 
+              <div id="transfers">
+                <h3 className="text-align-center">Make a transfer</h3>
+                <Transfers
+                  Tezos={Tezos}
+                  setUserBalance={setUserBalance}
+                  userAddress={userAddress}
+                />
+                <p>
+                  <i className="far fa-file-code"></i>&nbsp;
+                  <a
+                    href={`https://better-call.dev/granadanet/${contractAddress}/operations`}
+                    target="_blank"
+                    rel="noopener noreferrer"
+                  >
+                  {contractAddress}
+                  </a>
+                </p>
+                <p>
+                  <i className="far fa-address-card"></i>&nbsp; {userAddress}
+                </p>
+                <p>
+                  <i className="fas fa-piggy-bank"></i>&nbsp;
+                  {(userBalance / 1000000).toLocaleString("en-US")} ęś©
+                </p>
+                <DisconnectButton
+                  wallet={wallet}
+                  setPublicToken={setPublicToken}
+                  setUserAddress={setUserAddress}
+                  setUserBalance={setUserBalance}
+                  setWallet={setWallet}
+                  setTezos={setTezos}
+                  setBeaconConnection={setBeaconConnection}
+                />
+              </div>
+            }
+            {activeTab === "contract" && 
+              <div id="increment-decrement">
+                <Records
+                   contract={contract}
+                   setUserBalance={setUserBalance}
+                  Tezos={Tezos}
+                   userAddress={userAddress}
+                   setStorage={setStorage}
+                />
+              </div>
+            }
+            {activeTab === "reports" && 
+              <div id="increment-decrement">
+                <h3 className="text-align-center">
+                 {storage.dates.map((val: boolean | React.ReactChild | React.ReactFragment | React.ReactPortal | null | undefined) => <div>{val}</div>)}
+                </h3>
+              </div>
+            }
+            
+          </div>
+        </div>
+        <div id="footer">
+        </div>
+      </div>
+    );
+  } else if (!publicToken && !userAddress && !userBalance) {
+    return (
+      <div className="main-box">
+        <div className="title">
+          <h1>MyMedMemoir</h1>
+        </div>
+        <div id="dialog">
+          <header>Welcome to MyMedMemoir!</header>
+          <div id="content">
+            <p>Hello!</p>
+            <p>Go forth and Tezos!</p>
+          </div>
+          <ConnectButton
+            Tezos={Tezos}
+            setContract={setContract}
+            setPublicToken={setPublicToken}
+            setWallet={setWallet}
+            setUserAddress={setUserAddress}
+            setUserBalance={setUserBalance}
+            setStorage={setStorage}
+            contractAddress={contractAddress}
+            setBeaconConnection={setBeaconConnection}
+            wallet={wallet}
+          />
+        </div>
+        <div id="footer">
+        </div>
+      </div>
+    );
+  } else {
+    return <div>An error has occurred</div>;
+  }
+};
+
+export default App;
diff --git a/client/src/components/ConnectWallet.tsx b/client/src/components/ConnectWallet.tsx
new file mode 100644
index 0000000..3a293c0
--- /dev/null
+++ b/client/src/components/ConnectWallet.tsx
@@ -0,0 +1,137 @@
+import React, { Dispatch, SetStateAction, useState, useEffect } from "react";
+import { TezosToolkit } from "@taquito/taquito";
+import { BeaconWallet } from "@taquito/beacon-wallet";
+import {
+  NetworkType,
+  BeaconEvent,
+  defaultEventCallbacks
+} from "@airgap/beacon-sdk";
+import TransportU2F from "@ledgerhq/hw-transport-u2f";
+import { LedgerSigner } from "@taquito/ledger-signer";
+
+type ButtonProps = {
+  Tezos: TezosToolkit;
+  setContract: Dispatch<SetStateAction<any>>;
+  setWallet: Dispatch<SetStateAction<any>>;
+  setUserAddress: Dispatch<SetStateAction<string>>;
+  setUserBalance: Dispatch<SetStateAction<number>>;
+  setStorage: Dispatch<SetStateAction<number>>;
+  contractAddress: string;
+  setBeaconConnection: Dispatch<SetStateAction<boolean>>;
+  setPublicToken: Dispatch<SetStateAction<string | null>>;
+  wallet: BeaconWallet;
+};
+
+const ConnectButton = ({
+  Tezos,
+  setContract,
+  setWallet,
+  setUserAddress,
+  setUserBalance,
+  setStorage,
+  contractAddress,
+  setBeaconConnection,
+  setPublicToken,
+  wallet
+}: ButtonProps): JSX.Element => {
+  const [loadingNano, setLoadingNano] = useState<boolean>(false);
+
+  const setup = async (userAddress: string): Promise<void> => {
+    setUserAddress(userAddress);
+    // updates balance
+    const balance = await Tezos.tz.getBalance(userAddress);
+    setUserBalance(balance.toNumber());
+    // creates contract instance
+    const contract = await Tezos.wallet.at(contractAddress);
+    const storage: any = await contract.storage();
+    setContract(contract);
+    setStorage(storage);
+  };
+
+  const connectWallet = async (): Promise<void> => {
+    try {
+      await wallet.requestPermissions({
+        network: {
+          type: NetworkType.GRANADANET,
+          rpcUrl: "https://api.tez.ie/rpc/granadanet"
+        }
+      });
+      // gets user's address
+      const userAddress = await wallet.getPKH();
+      await setup(userAddress);
+      setBeaconConnection(true);
+    } catch (error) {
+      console.log(error);
+    }
+  };
+
+  const connectNano = async (): Promise<void> => {
+    try {
+      setLoadingNano(true);
+      const transport = await TransportU2F.create();
+      const ledgerSigner = new LedgerSigner(transport, "44'/1729'/0'/0'", true);
+
+      Tezos.setSignerProvider(ledgerSigner);
+
+      //Get the public key and the public key hash from the Ledger
+      const userAddress = await Tezos.signer.publicKeyHash();
+      await setup(userAddress);
+    } catch (error) {
+      console.log("Error!", error);
+      setLoadingNano(false);
+    }
+  };
+
+  useEffect(() => {
+    (async () => {
+      // creates a wallet instance
+      const wallet = new BeaconWallet({
+        name: "Taquito Boilerplate",
+        preferredNetwork: NetworkType.GRANADANET,
+        disableDefaultEvents: true, // Disable all events / UI. This also disables the pairing alert.
+        eventHandlers: {
+          // To keep the pairing alert, we have to add the following default event handlers back
+          [BeaconEvent.PAIR_INIT]: {
+            handler: defaultEventCallbacks.PAIR_INIT
+          },
+          [BeaconEvent.PAIR_SUCCESS]: {
+            handler: data => setPublicToken(data.publicKey)
+          }
+        }
+      });
+      Tezos.setWalletProvider(wallet);
+      setWallet(wallet);
+      // checks if wallet was connected before
+      const activeAccount = await wallet.client.getActiveAccount();
+      if (activeAccount) {
+        const userAddress = await wallet.getPKH();
+        await setup(userAddress);
+        setBeaconConnection(true);
+      }
+    })();
+  }, []);
+
+  return (
+    <div className="buttons">
+      <button className="button" onClick={connectWallet}>
+        <span>
+          <i className="fas fa-wallet"></i>&nbsp; Connect with wallet
+        </span>
+      </button>
+      <button className="button" disabled={loadingNano} onClick={connectNano}>
+        {loadingNano ? (
+          <span>
+            <i className="fas fa-spinner fa-spin"></i>&nbsp; Loading, please
+            wait
+          </span>
+        ) : (
+          <span>
+            <i className="fab fa-usb"></i>&nbsp; Connect with Ledger Nano
+          </span>
+        )}
+      </button>
+    </div>
+  );
+};
+
+export default ConnectButton;
diff --git a/client/src/components/DisconnectWallet.tsx b/client/src/components/DisconnectWallet.tsx
new file mode 100644
index 0000000..45e6643
--- /dev/null
+++ b/client/src/components/DisconnectWallet.tsx
@@ -0,0 +1,50 @@
+import React, { Dispatch, SetStateAction } from "react";
+import { BeaconWallet } from "@taquito/beacon-wallet";
+import { TezosToolkit } from "@taquito/taquito";
+
+interface ButtonProps {
+  wallet: BeaconWallet | null;
+  setPublicToken: Dispatch<SetStateAction<string | null>>;
+  setUserAddress: Dispatch<SetStateAction<string>>;
+  setUserBalance: Dispatch<SetStateAction<number>>;
+  setWallet: Dispatch<SetStateAction<any>>;
+  setTezos: Dispatch<SetStateAction<TezosToolkit>>;
+  setBeaconConnection: Dispatch<SetStateAction<boolean>>;
+}
+
+const DisconnectButton = ({
+  wallet,
+  setPublicToken,
+  setUserAddress,
+  setUserBalance,
+  setWallet,
+  setTezos,
+  setBeaconConnection
+}: ButtonProps): JSX.Element => {
+  const disconnectWallet = async (): Promise<void> => {
+    //window.localStorage.clear();
+    setUserAddress("");
+    setUserBalance(0);
+    setWallet(null);
+    const tezosTK = new TezosToolkit("https://api.tez.ie/rpc/granadanet");
+    setTezos(tezosTK);
+    setBeaconConnection(false);
+    setPublicToken(null);
+    console.log("disconnecting wallet");
+    if (wallet) {
+      await wallet.client.removeAllAccounts();
+      await wallet.client.removeAllPeers();
+      await wallet.client.destroy();
+    }
+  };
+
+  return (
+    <div className="buttons">
+      <button className="button" onClick={disconnectWallet}>
+        <i className="fas fa-times"></i>&nbsp; Disconnect wallet
+      </button>
+    </div>
+  );
+};
+
+export default DisconnectButton;
diff --git a/client/src/components/Records.tsx b/client/src/components/Records.tsx
new file mode 100644
index 0000000..eec867d
--- /dev/null
+++ b/client/src/components/Records.tsx
@@ -0,0 +1,49 @@
+import React, { useState, Dispatch, SetStateAction } from "react";
+import { ContractMethod, TezosToolkit, WalletContract } from "@taquito/taquito";
+
+interface RecordsProps {
+  contract: WalletContract | any;
+  setUserBalance: Dispatch<SetStateAction<any>>;
+  Tezos: TezosToolkit;
+  userAddress: string;
+  setStorage: Dispatch<SetStateAction<number>>;
+}
+
+const Records = ({ contract, setUserBalance, Tezos, userAddress, setStorage }: RecordsProps) => {
+  const [loadingIncrement, setLoadingIncrement] = useState<boolean>(false);
+  const [loadingDecrement, setLoadingDecrement] = useState<boolean>(false);
+
+  const addRecord = async (): Promise<void> => {
+    setLoadingIncrement(true);
+    try {
+      const op = await contract.methods.default("tesds").send();
+      await op.confirmation();
+      const newStorage: any = await contract.storage();
+      if (newStorage) setStorage(newStorage);
+      setUserBalance(await Tezos.tz.getBalance(userAddress));
+    } catch (error) {
+      console.log(error);
+    } finally {
+      setLoadingIncrement(false);
+    }
+  };
+
+  if (!contract && !userAddress) return <div>&nbsp;</div>;
+  return (
+    <div className="buttons">
+      <button className="button" disabled={loadingIncrement} onClick={addRecord}>
+        {loadingIncrement ? (
+          <span>
+            <i className="fas fa-spinner fa-spin"></i>&nbsp; Please wait
+          </span>
+        ) : (
+          <span>
+            <i className="fas fa-plus"></i>&nbsp; Add Record
+          </span>
+        )}
+      </button>
+    </div>
+  );
+};
+
+export default Records;
diff --git a/client/src/components/Transfers.tsx b/client/src/components/Transfers.tsx
new file mode 100644
index 0000000..3bd1e65
--- /dev/null
+++ b/client/src/components/Transfers.tsx
@@ -0,0 +1,70 @@
+import React, { useState, Dispatch, SetStateAction } from "react";
+import { TezosToolkit } from "@taquito/taquito";
+
+const Transfers = ({
+  Tezos,
+  setUserBalance,
+  userAddress
+}: {
+  Tezos: TezosToolkit;
+  setUserBalance: Dispatch<SetStateAction<number>>;
+  userAddress: string;
+}): JSX.Element => {
+  const [recipient, setRecipient] = useState<string>("");
+  const [amount, setAmount] = useState<string>("");
+  const [loading, setLoading] = useState<boolean>(false);
+
+  const sendTransfer = async (): Promise<void> => {
+    if (recipient && amount) {
+      setLoading(true);
+      try {
+        const op = await Tezos.wallet
+          .transfer({ to: recipient, amount: parseInt(amount) })
+          .send();
+        await op.confirmation();
+        setRecipient("");
+        setAmount("");
+        const balance = await Tezos.tz.getBalance(userAddress);
+        setUserBalance(balance.toNumber());
+      } catch (error) {
+        console.log(error);
+      } finally {
+        setLoading(false);
+      }
+    }
+  };
+
+  return (
+    <div id="transfer-inputs">
+      <input
+        type="text"
+        placeholder="Recipient"
+        value={recipient}
+        onChange={e => setRecipient(e.target.value)}
+      />
+      <input
+        type="number"
+        placeholder="Amount"
+        value={amount}
+        onChange={e => setAmount(e.target.value)}
+      />
+      <button
+        className="button"
+        disabled={!recipient && !amount}
+        onClick={sendTransfer}
+      >
+        {loading ? (
+          <span>
+            <i className="fas fa-spinner fa-spin"></i>&nbsp; Please wait
+          </span>
+        ) : (
+          <span>
+            <i className="far fa-paper-plane"></i>&nbsp; Send
+          </span>
+        )}
+      </button>
+    </div>
+  );
+};
+
+export default Transfers;
diff --git a/client/src/index.js b/client/src/index.js
new file mode 100644
index 0000000..4522a95
--- /dev/null
+++ b/client/src/index.js
@@ -0,0 +1,16 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import App from "./App.tsx";
+import * as serviceWorker from "./serviceWorker";
+
+ReactDOM.render(
+    <React.StrictMode>
+        <App />
+    </React.StrictMode>,
+    document.getElementById("root")
+);
+
+// If you want your app to work offline and load faster, you can change
+// unregister() to register() below. Note this comes with some pitfalls.
+// Learn more about service workers: https://bit.ly/CRA-PWA
+serviceWorker.unregister();
diff --git a/client/src/react-app-env.d.ts b/client/src/react-app-env.d.ts
new file mode 100644
index 0000000..6431bc5
--- /dev/null
+++ b/client/src/react-app-env.d.ts
@@ -0,0 +1 @@
+/// <reference types="react-scripts" />
diff --git a/client/src/serviceWorker.js b/client/src/serviceWorker.js
new file mode 100644
index 0000000..61a319e
--- /dev/null
+++ b/client/src/serviceWorker.js
@@ -0,0 +1,141 @@
+// This optional code is used to register a service worker.
+// register() is not called by default.
+
+// This lets the app load faster on subsequent visits in production, and gives
+// it offline capabilities. However, it also means that developers (and users)
+// will only see deployed updates on subsequent visits to a page, after all the
+// existing tabs open on the page have been closed, since previously cached
+// resources are updated in the background.
+
+// To learn more about the benefits of this model and instructions on how to
+// opt-in, read https://bit.ly/CRA-PWA
+
+const isLocalhost = Boolean(
+    window.location.hostname === 'localhost' ||
+        // [::1] is the IPv6 localhost address.
+    window.location.hostname === '[::1]' ||
+        // 127.0.0.0/8 are considered localhost for IPv4.
+    window.location.hostname.match(
+        /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
+    )
+);
+
+export function register(config) {
+    if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
+        // The URL constructor is available in all browsers that support SW.
+        const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
+        if (publicUrl.origin !== window.location.origin) {
+            // Our service worker won't work if PUBLIC_URL is on a different origin
+            // from what our page is served on. This might happen if a CDN is used to
+            // serve assets; see https://github.com/facebook/create-react-app/issues/2374
+            return;
+        }
+
+        window.addEventListener('load', () => {
+            const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
+
+            if (isLocalhost) {
+                // This is running on localhost. Let's check if a service worker still exists or not.
+                checkValidServiceWorker(swUrl, config);
+
+                // Add some additional logging to localhost, pointing developers to the
+                // service worker/PWA documentation.
+                navigator.serviceWorker.ready.then(() => {
+                    console.log(
+                        'This web app is being served cache-first by a service ' +
+                            'worker. To learn more, visit https://bit.ly/CRA-PWA'
+                    );
+                });
+            } else {
+                // Is not localhost. Just register service worker
+                registerValidSW(swUrl, config);
+            }
+        });
+    }
+}
+
+function registerValidSW(swUrl, config) {
+    navigator.serviceWorker
+        .register(swUrl)
+        .then(registration => {
+            registration.onupdatefound = () => {
+                const installingWorker = registration.installing;
+                if (installingWorker == null) {
+                    return;
+                }
+                installingWorker.onstatechange = () => {
+                    if (installingWorker.state === 'installed') {
+                        if (navigator.serviceWorker.controller) {
+                            // At this point, the updated precached content has been fetched,
+                            // but the previous service worker will still serve the older
+                            // content until all client tabs are closed.
+                            console.log(
+                                'New content is available and will be used when all ' +
+                                    'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
+                            );
+
+                            // Execute callback
+                            if (config && config.onUpdate) {
+                                config.onUpdate(registration);
+                            }
+                        } else {
+                            // At this point, everything has been precached.
+                            // It's the perfect time to display a
+                            // "Content is cached for offline use." message.
+                            console.log('Content is cached for offline use.');
+
+                            // Execute callback
+                            if (config && config.onSuccess) {
+                                config.onSuccess(registration);
+                            }
+                        }
+                    }
+                };
+            };
+        })
+        .catch(error => {
+            console.error('Error during service worker registration:', error);
+        });
+}
+
+function checkValidServiceWorker(swUrl, config) {
+    // Check if the service worker can be found. If it can't reload the page.
+    fetch(swUrl, {
+        headers: { 'Service-Worker': 'script' },
+    })
+        .then(response => {
+            // Ensure service worker exists, and that we really are getting a JS file.
+            const contentType = response.headers.get('content-type');
+            if (
+                response.status === 404 ||
+                    (contentType != null && contentType.indexOf('javascript') === -1)
+            ) {
+                // No service worker found. Probably a different app. Reload the page.
+                navigator.serviceWorker.ready.then(registration => {
+                    registration.unregister().then(() => {
+                        window.location.reload();
+                    });
+                });
+            } else {
+                // Service worker found. Proceed as normal.
+                registerValidSW(swUrl, config);
+            }
+        })
+        .catch(() => {
+            console.log(
+                'No internet connection found. App is running in offline mode.'
+            );
+        });
+}
+
+export function unregister() {
+    if ('serviceWorker' in navigator) {
+        navigator.serviceWorker.ready
+            .then(registration => {
+                registration.unregister();
+            })
+            .catch(error => {
+                console.error(error.message);
+            });
+    }
+}