diff --git a/web/cashtab/.gitignore b/web/cashtab/.gitignore --- a/web/cashtab/.gitignore +++ b/web/cashtab/.gitignore @@ -10,8 +10,6 @@ # production /build -/dist -/extension # misc .DS_Store @@ -27,4 +25,5 @@ # Chrome Extension testing *.crx *.zip -*.pem \ No newline at end of file +*.pem +dist/ \ No newline at end of file diff --git a/web/cashtab/README.md b/web/cashtab/README.md --- a/web/cashtab/README.md +++ b/web/cashtab/README.md @@ -42,6 +42,15 @@ See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. +## Browser Extension + +1. `npm run extension` +2. Open Chrome or Brave +3. Navigate to `chrome://extensions/` (or `brave://extensions/`) +4. Enable Developer Mode +5. Click "Load unpacked" +6. Select the `extension/dist` folder that was created with `npm run extension` + ## Docker deployment ``` diff --git a/web/cashtab/extension/README.md b/web/cashtab/extension/README.md new file mode 100644 --- /dev/null +++ b/web/cashtab/extension/README.md @@ -0,0 +1,9 @@ +# CashTab extension + +Some minor but important code changes are required to build CashTab as a browser extension. + +1. Add option to open extension in tab +2. Unique format of manifest.json with sha256 hash of any external scripts +3. CSS rules for extension pop-up sizing + +Source files unique to the browser extension are kept in the `extension/` directory. diff --git a/web/cashtab/extension/public/manifest.json b/web/cashtab/extension/public/manifest.json new file mode 100644 --- /dev/null +++ b/web/cashtab/extension/public/manifest.json @@ -0,0 +1,20 @@ +{ + "manifest_version": 2, + + "name": "CashTab", + "description": "A browser-integrated BCHA wallet from Bitcoin ABC", + "version": "0.0.1", + + "browser_action": { + "default_popup": "index.html", + "default_title": "CashTab" + }, + "icons": { + "16": "bch16.png", + "48": "bch48.png", + "128": "bch128.png", + "192": "bch192.png", + "512": "bch512.png" + }, + "content_security_policy": "script-src 'self' https://unpkg.com/minimal-slp-wallet-web-joey 'sha256-03cee7e881f6cdd32ce620d5787d7e6a0eaef8acc55557bded61dd6ad82ff0e6'; object-src 'self'" +} diff --git a/web/cashtab/extension/src/assets/popout.svg b/web/cashtab/extension/src/assets/popout.svg new file mode 100644 --- /dev/null +++ b/web/cashtab/extension/src/assets/popout.svg @@ -0,0 +1,21 @@ + + + + popout + Created with Sketch. + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/cashtab/extension/src/components/App.css b/web/cashtab/extension/src/components/App.css new file mode 100644 --- /dev/null +++ b/web/cashtab/extension/src/components/App.css @@ -0,0 +1,411 @@ +@import '~antd/dist/antd.less'; +@import '~@fortawesome/fontawesome-free/css/all.css'; +@import url('https://fonts.googleapis.com/css?family=Khula&display=swap&.css'); + +@font-face { + font-family: 'Roboto Mono'; + src: local('Roboto Mono'), + url(../assets/fonts/RobotoMono-Regular.ttf) format('truetype'); + font-weight: normal; +} + +aside::-webkit-scrollbar { + width: 0.3em; +} +aside::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px #13171f; +} +aside::-webkit-scrollbar-thumb { + background-color: darkgrey; + outline: 1px solid slategrey; +} + +/* Hide up and down arros on input type="number" */ +/* Chrome, Safari, Edge, Opera */ +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Hide up and down arros on input type="number" */ +/* Firefox */ +input[type='number'] { + -moz-appearance: textfield; +} + +html, +body { + min-width: 320px; + min-height: 600px; + max-width: 100%; + overflow-x: hidden; +} + +.ant-modal-wrap.ant-modal-centered::-webkit-scrollbar { + display: none; +} + +.App { + text-align: center; + font-family: 'Gilroy', sans-serif; + background-color: #fbfbfd; +} +.App-logo { + width: 100%; + display: block; +} + +.logo img { + width: 100%; + min-width: 193px; + display: block; + padding-left: 24px; + padding-right: 34px; + padding-top: 24px; + max-width: 200px; +} +.ant-list-item-meta .ant-list-item-meta-content { + display: flex; +} + +#react-qrcode-logo { + border-radius: 8px; +} +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #f59332; +} +.ant-menu-item-group-title { + padding-left: 30px; + font-size: 20px !important; + font-weight: 500 !important; +} + +.ant-menu-item > span { + font-size: 14px !important; + font-weight: 500 !important; +} + +.ant-card-actions > li > span:hover, +.ant-btn:hover, +.ant-btn:focus { + color: #f59332; + transition: color 0.3s; + background-color: white; +} + +.ant-card-actions > li { + color: #3e3f42; +} +.anticon { + color: #3e3f42; +} +.ant-list-item-meta-description, +.ant-list-item-meta-title { + color: #3e3f42; +} + +.ant-list-item-meta-description > :first-child { + right: 20px !important; + position: absolute; +} + +.ant-modal-body .ant-list-item-meta { + height: 85px; + width: 85px; + padding-left: 10px; + padding-top: 10px; + padding-bottom: 20px; + overflow: visible !important; +} + +/* .ant-radio-group-solid .ant-radio-button-wrapper { + margin-top: 0px; +} + +.ant-radio-group-solid .ant-radio-button-wrapper-checked { + border: none !important; + box-shadow: none !important; +} */ +.identicon { + border-radius: 50%; + width: 200px; + height: 200px; + margin-top: -75px; + margin-left: -75px; + margin-bottom: 20px; + box-shadow: 1px 1px 2px 1px #444; +} +.ant-list-item-meta { + width: 40px; + height: 40px; +} + +/* .ant-radio-group-solid .ant-radio-button-wrapper-checked { + background: #ff8d00 !important; +} + +.ant-radio-group.ant-radio-group-solid.ant-radio-group-small { + font-size: 14px !important; + font-weight: 600 !important; + vertical-align: middle; + border-radius: 100px; + overflow: auto; + background: rgba(255, 255, 255, 0.5) !important; + margin-top: 14px; + margin-bottom: 10px; + cursor: pointer; +} */ + +input.ant-input, +.ant-select-selection { + background-color: #fff !important; + box-shadow: none !important; + border: 1px solid #eaedf3 !important; + border-radius: 4px; + font-weight: bold; + color: rgb(62, 63, 66); + opacity: 1; + padding: 11px 5px; + height: 50px; +} + +.ant-select-selection:hover { + border: 1px solid #eaedf3; +} + +.ant-select-selection-selected-value { + color: rgb(62, 63, 66); +} + +.ant-select-dropdown-menu-item { + color: #444; + background-color: #fff; +} + +.ant-select-dropdown-menu-item-active, +.ant-select-dropdown-menu-item:hover { + color: #fff; + background-color: #ff8d00 !important; +} + +.ant-checkbox-inner { + border: 1px solid #eaedf3 !important; + background: white; +} + +.ant-checkbox-inner::after { + border-color: white !important; +} + +.ant-card-bordered { + border: 1px solid rgb(234, 237, 243); + border-radius: 8px; +} + +.ant-card-actions { + border-top: 1px solid rgb(234, 237, 243); + border-bottom: 1px solid rgb(234, 237, 243); + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + box-shadow: 0px 5px 8px rgba(0, 0, 0, 0.35); +} + +.ant-input-group-addon { + background-color: #f4f4f4 !important; + border: 1px solid rgb(234, 237, 243); + color: #3e3f42 !important; + + * { + color: #3e3f42 !important; + } +} + +.ant-menu-item.ant-menu-item-selected > * { + color: #fff !important; +} + +.ant-menu-item.ant-menu-item-selected { + border: 0; + overflow: hidden; + text-align: left; + padding-left: 28px; + background-color: rgba(255, 255, 255, 0.2) !important; +} + +.ant-btn { + border-radius: 8px; + background-color: #fff; + color: rgb(62, 63, 66); + font-weight: bold; +} + +.ant-card-actions > li:not(:last-child) { + border-right: 0; +} +.ant-list-item-meta-avatar > img { + margin-left: -12px; + transform: translate(0, -6px); +} + +.ant-list-item-meta-avatar > svg { + margin-right: -70px; +} + +/* Removing these for ABC SLP warning +.ant-alert-warning { + background-color: #20242d; + border: 1px solid #17171f; + border-radius: 0; +} + +.ant-alert-message { + color: #fff; +} +*/ + +.ant-layout-sider-dark { + background: linear-gradient(0deg, #040c3c, #212c6e); +} + +.ant-menu-dark { + background: none; +} + +.ant-layout-sider-zero-width-trigger.ant-layout-sider-zero-width-trigger-left + .anticon.anticon-bars { + color: #fff; + transform: scale(1.3); +} + +.ant-layout-sider-zero-width-trigger.ant-layout-sider-zero-width-trigger-left { + background: #3e3f42; + border-radius: 0 8px 8px 0; +} + +.ant-btn-group .ant-btn-primary:first-child:not(:last-child) { + border-right-color: transparent !important; +} + +.ant-btn-group .ant-btn-primary:last-child:not(:first-child), +.ant-btn-group .ant-btn-primary + .ant-btn-primary { + border-left-color: #20242d !important; +} + +.audit { + a, + a:active { + color: #46464a; + } + + a:hover { + color: #111117; + } +} + +.dividends { + a, + a:active { + color: #111117; + } + + a:hover { + color: #46464a; + } +} + +.ant-popover-inner-content { + color: white; +} + +.ant-modal-body .ant-card { + max-width: 100%; +} + +.ant-upload.ant-upload-drag { + border: 1px solid #eaedf3; + border-radius: 8px; + background: #d3d3d3; +} + +.ant-upload-list-item:hover .ant-upload-list-item-info { + background-color: #ffffff; +} + +/* .ant-radio-button-wrapper { + border: none; +} + +.ant-radio-button-wrapper-checked { + border-radius: none !important; +} */ + +/* .ant-radio-button-wrapper:first-child, .ant-radio-button-wrapper:last-child { + border-radius: 0 0 0 0; +} */ + +.ant-radio-group { + width: 100%; + margin-top: 10px; +} + +.ant-radio-button-wrapper { + background: rgba(255, 255, 255, 0.2); + width: 104px; + border: none; + text-align: center; + color: #fff; +} + +.ant-radio-button-wrapper:hover { + color: #fff; + background: rgba(255, 255, 255, 0.3); +} + +.ant-radio-group-small .ant-radio-button-wrapper { + height: 35px; + line-height: 35px; +} + +.ant-radio-button-wrapper-checked { + background: #ff8d00 !important; + border: none !important; +} + +.ant-radio-button-wrapper:first-child { + border-radius: 100px 0 0 100px; +} + +.ant-radio-button-wrapper:last-child { + border-radius: 0 100px 100px 0; +} + +::selection { + background-color: #ff8d00; +} + +@media (max-width: 768px) { + .ant-notification { + width: 100%; + top: 20px !important; + max-width: unset; + margin-right: 0; + } +} + +@media (max-width: 350px) { + .ant-select-selection-selected-value { + font-size: 10px; + } +} diff --git a/web/cashtab/extension/src/components/App.js b/web/cashtab/extension/src/components/App.js new file mode 100644 --- /dev/null +++ b/web/cashtab/extension/src/components/App.js @@ -0,0 +1,278 @@ +import React from 'react'; +import 'antd/dist/antd.less'; +import '../index.css'; +import styled from 'styled-components'; +import { Layout, Tabs, Icon } from 'antd'; +import Wallet from './Wallet/Wallet'; +import Send from './Send/Send'; +import SendToken from './Send/SendToken'; +import Configure from './Configure/Configure'; +import NotFound from './NotFound'; +import CashTab from '../assets/cashtab.png'; +import PopOut from '../assets/popout.svg'; +import './App.css'; +import { WalletContext } from '../utils/context'; +import { + Route, + Redirect, + Switch, + useLocation, + useHistory, +} from 'react-router-dom'; + +const { Footer } = Layout; +const { TabPane } = Tabs; + +const OpenInTabBtn = styled.button` + background: none; + border: none; +`; +const ExtTabImg = styled.img` + max-width: 20px; +`; + +const StyledTabsMenu = styled.div` + .ant-layout-footer { + position: absolute; + bottom: 0; + width: 100%; + padding: 0; + background-color: #fff; + left: 0; + border-radius: 20px; + border-top: 1px solid #e2e2e2; + @media (max-width: 768px) { + position: fixed; + } + } + .ant-tabs-nav .ant-tabs-tab { + padding: 30px 0 20px 0; + } + .ant-tabs-bar.ant-tabs-bottom-bar { + margin-top: 0; + border-top: none; + } + .ant-tabs-tab { + span { + font-size: 12px; + display: grid; + font-weight: bold; + } + .anticon { + color: rgb(148, 148, 148); + font-size: 24px; + margin-left: 8px; + margin-bottom: 3px; + } + } + .ant-tabs-tab:hover { + color: #ff8d00 !important; + .anticon { + color: #ff8d00; + } + } + .ant-tabs-tab-active.ant-tabs-tab { + color: #ff8d00; + .anticon { + color: #ff8d00; + } + } + .ant-tabs-tab-active.ant-tabs-tab { + color: #ff8d00; + .anticon { + color: #ff8d00; + } + } + .ant-tabs-tab-active:active { + color: #ff8d00 !important; + } + .ant-tabs-ink-bar { + display: none !important; + } + .ant-tabs-nav { + margin: -3.5px 0 0 0; + } +`; + +export const WalletBody = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 100%; + min-height: 100vh; + background: linear-gradient(270deg, #040c3c, #212c6e); +`; + +export const WalletCtn = styled.div` + position: relative; + width: 500px; + background-color: #fff; + min-height: 100vh; + padding-top: 30px; + padding: 10px 30px 100px 30px; + background: #fff; + -webkit-box-shadow: 0px 0px 24px 1px rgba(0, 0, 0, 1); + -moz-box-shadow: 0px 0px 24px 1px rgba(0, 0, 0, 1); + box-shadow: 0px 0px 24px 1px rgba(0, 0, 0, 1); + @media (max-width: 768px) { + width: 100%; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + } +`; + +export const HeaderCtn = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 100%; + padding: 20px 0 30px; + margin-bottom: 20px; + justify-content: space-between; + border-bottom: 1px solid #e2e2e2; + + a { + color: #848484; + + :hover { + color: #ff8d00; + } + } + + @media (max-width: 768px) { + a { + font-size: 12px; + } + padding: 10px 0 20px; + } +`; + +export const CashTabLogo = styled.img` + width: 120px; + @media (max-width: 768px) { + width: 110px; + } +`; +export const AbcLogo = styled.img` + width: 150px; + @media (max-width: 768px) { + width: 120px; + } +`; + +const App = () => { + const ContextValue = React.useContext(WalletContext); + const { wallet } = ContextValue; + const location = useLocation(); + const history = useHistory(); + const selectedKey = + location && location.pathname ? location.pathname.substr(1) : ''; + const openInTab = () => { + window.open(`index.html#/${selectedKey}`); + }; + + return ( +
+ + + + + openInTab()} + > + + + + + + + + + + + ( + + )} + /> + + + + + + + + {wallet ? ( + + + + ) : null} + + +
+ ); +}; + +export default App; diff --git a/web/cashtab/package.json b/web/cashtab/package.json --- a/web/cashtab/package.json +++ b/web/cashtab/package.json @@ -96,6 +96,7 @@ "scripts": { "start": "node scripts/start.js", "build": "node scripts/build.js", + "extension": "./scripts/extension.sh", "predeploy": "npm run build", "test": "node scripts/test.js", "test:coverage": "yarn test --coverage --watchAll=false", diff --git a/web/cashtab/scripts/extension.sh b/web/cashtab/scripts/extension.sh new file mode 100755 --- /dev/null +++ b/web/cashtab/scripts/extension.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +export LC_ALL=C +set -euo pipefail + +# Build CashTab as a Chrome/Brave browser extension + +# Create a working directory for stashing non-extension files +WORKDIR=$(mktemp -d) +echo Using workdir ${WORKDIR} + +# Delete workdir on script finish +function cleanup { + echo Deleting workdir ${WORKDIR} + rm -rf "${WORKDIR}" +} +trap cleanup EXIT + +# Stash web files that require extension changes in workdir +mv public/manifest.json ${WORKDIR} +mv src/components/App.js ${WORKDIR} +mv src/components/App.css ${WORKDIR} + +# Move extension src files into place for npm build +cp extension/src/assets/popout.svg src/assets/ +cp extension/public/manifest.json public/ +cp extension/src/components/App.js src/components/ +cp extension/src/components/App.css src/components/ + +# Delete the last extension build +if [ -d "extension/dist/" ]; then rm -Rf extension/dist/; fi + +# Build the extension +mkdir extension/dist/ +echo 'Building Extension...' + +# Required for extension build rules +export INLINE_RUNTIME_CHUNK=false +export GENERATE_SOURCEMAP=false + +npm run build + +# Copy extension build files to extension/ folder +cp -r build/* extension/dist + +# Delete extension build from build/ folder (reserved for web app builds) +rm -Rf build + +# Replace original web files +rm src/assets/popout.svg +rm public/manifest.json +rm src/components/App.js +rm src/components/App.css + +# Note, src/assets/popout.svg does not need to be replaced, not used by web app +mv ${WORKDIR}/manifest.json public/ +mv ${WORKDIR}/App.js src/components/ +mv ${WORKDIR}/App.css src/components/ + +echo 'Extension built and web files replaced!' \ No newline at end of file