diff options
Diffstat (limited to 'client/src')
-rw-r--r-- | client/src/App.css | 191 | ||||
-rw-r--r-- | client/src/App.tsx | 215 | ||||
-rw-r--r-- | client/src/components/ConnectWallet.tsx | 137 | ||||
-rw-r--r-- | client/src/components/DisconnectWallet.tsx | 50 | ||||
-rw-r--r-- | client/src/components/Records.tsx | 49 | ||||
-rw-r--r-- | client/src/components/Transfers.tsx | 70 | ||||
-rw-r--r-- | client/src/index.js | 16 | ||||
-rw-r--r-- | client/src/react-app-env.d.ts | 1 | ||||
-rw-r--r-- | client/src/serviceWorker.js | 141 |
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> 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> + <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> {userAddress} + </p> + <p> + <i className="fas fa-piggy-bank"></i> + {(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> Connect with wallet + </span> + </button> + <button className="button" disabled={loadingNano} onClick={connectNano}> + {loadingNano ? ( + <span> + <i className="fas fa-spinner fa-spin"></i> Loading, please + wait + </span> + ) : ( + <span> + <i className="fab fa-usb"></i> 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> 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> </div>; + return ( + <div className="buttons"> + <button className="button" disabled={loadingIncrement} onClick={addRecord}> + {loadingIncrement ? ( + <span> + <i className="fas fa-spinner fa-spin"></i> Please wait + </span> + ) : ( + <span> + <i className="fas fa-plus"></i> 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> Please wait + </span> + ) : ( + <span> + <i className="far fa-paper-plane"></i> 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); + }); + } +} |