├── .env ├── .eslintrc.json ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── .yarn ├── plugins │ └── @yarnpkg │ │ └── plugin-interactive-tools.cjs └── releases │ └── yarn-3.0.2.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── netlify.toml ├── next-env.d.ts ├── next.config.js ├── package.json ├── public ├── favicon.ico ├── icon-192x192.png ├── img │ ├── app_screen.png │ └── twoblocks-app.png ├── lib │ └── otplib-browser-buffer.js ├── manifest.json └── undraw_authentication_fsn5.svg ├── scripts └── copy-lib.sh ├── src ├── App.tsx ├── components │ ├── AccountItem.tsx │ ├── AccountList.tsx │ ├── AddAccount.tsx │ ├── AddAccountScan.tsx │ ├── DeleteAccount.tsx │ ├── EditAccount.tsx │ ├── Home.tsx │ ├── Loader.tsx │ └── Login.tsx ├── config.ts ├── context │ ├── FileContext.tsx │ └── ThemeContext.ts ├── custom.d.ts ├── pages │ ├── _app.tsx │ └── index.tsx ├── types │ └── index.ts └── utils │ ├── accounts.ts │ ├── blockstack.ts │ ├── fathom.ts │ └── icons.ts ├── tsconfig.json └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_COMMIT_REF=$COMMIT_REF 2 | REACT_APP_FATHOM_SITE_ID=$FATHOM_SITE_ID 3 | REACT_APP_SENTRY_DSN=$SENTRY_DSN 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Use Node.js 12.x 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: 12.x 15 | - name: Get yarn cache directory path 16 | id: yarn-cache-dir-path 17 | run: echo "::set-output name=dir::$(yarn config get cacheFolder)" 18 | - uses: actions/cache@v2 19 | id: yarn-cache 20 | with: 21 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 22 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 23 | restore-keys: | 24 | ${{ runner.os }}-yarn- 25 | - name: Install Dependencies 26 | run: yarn install --immutable 27 | - name: Build 28 | run: yarn build 29 | -------------------------------------------------------------------------------- /.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 | .yarn/* 8 | !.yarn/releases 9 | !.yarn/plugins 10 | 11 | # testing 12 | /coverage 13 | 14 | # next.js 15 | /.next/ 16 | /out/ 17 | 18 | # production 19 | /build 20 | 21 | # misc 22 | .DS_Store 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # local env files 31 | .env.local 32 | .env.development.local 33 | .env.test.local 34 | .env.production.local 35 | 36 | # vercel 37 | .vercel 38 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | public/lib 3 | .yarn 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "arcanis.vscode-zipfs"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.prettierPath": ".yarn/sdks/prettier/index.js", 3 | "typescript.tsdk": ".yarn/sdks/typescript/lib", 4 | "typescript.enablePromptUseWorkspaceTsdk": true 5 | } 6 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 5 | spec: '@yarnpkg/plugin-interactive-tools' 6 | 7 | yarnPath: .yarn/releases/yarn-3.0.2.cjs 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Léo Pradel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Twoblocks

2 | 3 |

4 | 5 |

6 | 7 |

8 | Free and open source 2fa manager built with Stacks 9 |

10 | 11 |

12 | License 13 | Status 14 |

15 | 16 |

17 | App 18 |

19 | 20 | ## 🚀 Features 21 | 22 | - Codes are synced between devices 23 | - Use your phone / laptop camera to scan a qrcode 24 | - Add an account manually 25 | - Dark theme 26 | - Data stored encrypted on the storage of your choice using Stacks 27 | 28 | ## 📚 Setup 29 | 30 | First you need to clone the repository: 31 | 32 | ```sh 33 | git clone git@github.com:pradel/twoblocks.git 34 | ``` 35 | 36 | Then run the following command to install dependencies: 37 | 38 | ```sh 39 | yarn install 40 | ``` 41 | 42 | Finally to start the server run: 43 | 44 | ```sh 45 | yarn start 46 | ``` 47 | 48 | You can now open your browser and go to http://localhost:3000 to see the app. 49 | 50 | ## ☝️ Feature requests, suggest changes 51 | 52 | Open an issue on this repo :) 53 | 54 | ## 📖 License 55 | 56 | MIT © [Léo Pradel](https://www.leopradel.com/) 57 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "." 3 | command = "yarn build && yarn next export" 4 | publish = "out" 5 | 6 | [build.environment] 7 | NODE_VERSION = "14" 8 | YARN_VERSION = "1.22.11" 9 | YARN_FLAGS = "--immutable" 10 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twoblocks", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "prettier": "prettier --write '**/*.{json,md,js,ts,jsx,tsx,yml,css}'", 11 | "postinstall": "husky install" 12 | }, 13 | "prettier": { 14 | "singleQuote": true, 15 | "trailingComma": "es5" 16 | }, 17 | "lint-staged": { 18 | "*.{json,md,js,ts,jsx,tsx,yml,css}": [ 19 | "prettier --write", 20 | "git add" 21 | ] 22 | }, 23 | "dependencies": { 24 | "@blockstack/stacks-transactions": "0.7.0", 25 | "@material-ui/core": "4.12.3", 26 | "@material-ui/icons": "4.11.2", 27 | "@material-ui/lab": "4.0.0-alpha.60", 28 | "@material-ui/styles": "4.11.4", 29 | "@otplib/preset-browser": "12.0.1", 30 | "@sentry/browser": "5.28.0", 31 | "@sentry/react": "5.28.0", 32 | "@stacks/auth": "2.0.1", 33 | "@stacks/connect": "6.2.0", 34 | "@stacks/storage": "2.0.1", 35 | "fathom-client": "3.2.0", 36 | "next": "11.1.2", 37 | "query-string": "6.14.1", 38 | "react": "17.0.2", 39 | "react-dom": "17.0.2", 40 | "react-is": "17.0.2", 41 | "react-qr-reader": "2.2.1", 42 | "typeface-roboto": "1.1.13" 43 | }, 44 | "devDependencies": { 45 | "@types/node": "14.17.17", 46 | "@types/query-string": "6.3.0", 47 | "@types/react": "17.0.21", 48 | "@types/react-dom": "17.0.9", 49 | "@types/react-qr-reader": "2.1.4", 50 | "eslint": "7.32.0", 51 | "eslint-config-next": "11.1.2", 52 | "husky": "7.0.2", 53 | "lint-staged": "11.1.2", 54 | "prettier": "2.4.1", 55 | "typescript": "4.4.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pradel/twoblocks/8cfbae0632678ce87f8ccebe92f745a7e015d244/public/favicon.ico -------------------------------------------------------------------------------- /public/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pradel/twoblocks/8cfbae0632678ce87f8ccebe92f745a7e015d244/public/icon-192x192.png -------------------------------------------------------------------------------- /public/img/app_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pradel/twoblocks/8cfbae0632678ce87f8ccebe92f745a7e015d244/public/img/app_screen.png -------------------------------------------------------------------------------- /public/img/twoblocks-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pradel/twoblocks/8cfbae0632678ce87f8ccebe92f745a7e015d244/public/img/twoblocks-app.png -------------------------------------------------------------------------------- /public/lib/otplib-browser-buffer.js: -------------------------------------------------------------------------------- 1 | !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).buffer=t()}}(function(){return function(){return function t(r,e,n){function i(f,u){if(!e[f]){if(!r[f]){var s="function"==typeof require&&require;if(!u&&s)return s(f,!0);if(o)return o(f,!0);var h=new Error("Cannot find module '"+f+"'");throw h.code="MODULE_NOT_FOUND",h}var a=e[f]={exports:{}};r[f][0].call(a.exports,function(t){return i(r[f][1][t]||t)},a,a.exports,t,r,e,n)}return e[f].exports}for(var o="function"==typeof require&&require,f=0;ff)throw new RangeError('The value "'+t+'" is invalid for option "size"');var e=new Uint8Array(t);return Object.setPrototypeOf(e,r.prototype),e}function r(t,r,e){if("number"==typeof t){if("string"==typeof r)throw new TypeError('The "string" argument must be of type string. Received type number');return a(t)}return s(t,r,e)}function s(t,e,n){if("string"==typeof t)return function(t,e){"string"==typeof e&&""!==e||(e="utf8");if(!r.isEncoding(e))throw new TypeError("Unknown encoding: "+e);var n=0|l(t,e),i=u(n),o=i.write(t,e);o!==n&&(i=i.slice(0,o));return i}(t,e);if(ArrayBuffer.isView(t))return p(t);if(null==t)throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof t);if(z(t,ArrayBuffer)||t&&z(t.buffer,ArrayBuffer))return function(t,e,n){if(e<0||t.byteLength=f)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+f.toString(16)+" bytes");return 0|t}function l(t,e){if(r.isBuffer(t))return t.length;if(ArrayBuffer.isView(t)||z(t,ArrayBuffer))return t.byteLength;if("string"!=typeof t)throw new TypeError('The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof t);var n=t.length,i=arguments.length>2&&!0===arguments[2];if(!i&&0===n)return 0;for(var o=!1;;)switch(e){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":return P(t).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return j(t).length;default:if(o)return i?-1:P(t).length;e=(""+e).toLowerCase(),o=!0}}function y(t,r,e){var n=t[r];t[r]=t[e],t[e]=n}function g(t,e,n,i,o){if(0===t.length)return-1;if("string"==typeof n?(i=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),D(n=+n)&&(n=o?0:t.length-1),n<0&&(n=t.length+n),n>=t.length){if(o)return-1;n=t.length-1}else if(n<0){if(!o)return-1;n=0}if("string"==typeof e&&(e=r.from(e,i)),r.isBuffer(e))return 0===e.length?-1:w(t,e,n,i,o);if("number"==typeof e)return e&=255,"function"==typeof Uint8Array.prototype.indexOf?o?Uint8Array.prototype.indexOf.call(t,e,n):Uint8Array.prototype.lastIndexOf.call(t,e,n):w(t,[e],n,i,o);throw new TypeError("val must be string, number or Buffer")}function w(t,r,e,n,i){var o,f=1,u=t.length,s=r.length;if(void 0!==n&&("ucs2"===(n=String(n).toLowerCase())||"ucs-2"===n||"utf16le"===n||"utf-16le"===n)){if(t.length<2||r.length<2)return-1;f=2,u/=2,s/=2,e/=2}function h(t,r){return 1===f?t[r]:t.readUInt16BE(r*f)}if(i){var a=-1;for(o=e;ou&&(e=u-s),o=e;o>=0;o--){for(var p=!0,c=0;ci&&(n=i):n=i;var o=r.length;n>o/2&&(n=o/2);for(var f=0;f>8,i=e%256,o.push(i),o.push(n);return o}(r,t.length-e),t,e,n)}function A(t,r,e){return 0===r&&e===t.length?n.fromByteArray(t):n.fromByteArray(t.slice(r,e))}function U(t,r,e){e=Math.min(t.length,e);for(var n=[],i=r;i239?4:h>223?3:h>191?2:1;if(i+p<=e)switch(p){case 1:h<128&&(a=h);break;case 2:128==(192&(o=t[i+1]))&&(s=(31&h)<<6|63&o)>127&&(a=s);break;case 3:o=t[i+1],f=t[i+2],128==(192&o)&&128==(192&f)&&(s=(15&h)<<12|(63&o)<<6|63&f)>2047&&(s<55296||s>57343)&&(a=s);break;case 4:o=t[i+1],f=t[i+2],u=t[i+3],128==(192&o)&&128==(192&f)&&128==(192&u)&&(s=(15&h)<<18|(63&o)<<12|(63&f)<<6|63&u)>65535&&s<1114112&&(a=s)}null===a?(a=65533,p=1):a>65535&&(a-=65536,n.push(a>>>10&1023|55296),a=56320|1023&a),n.push(a),i+=p}return function(t){var r=t.length;if(r<=T)return String.fromCharCode.apply(String,t);var e="",n=0;for(;nthis.length)return"";if((void 0===e||e>this.length)&&(e=this.length),e<=0)return"";if((e>>>=0)<=(r>>>=0))return"";for(t||(t="utf8");;)switch(t){case"hex":return L(this,r,e);case"utf8":case"utf-8":return U(this,r,e);case"ascii":return I(this,r,e);case"latin1":case"binary":return S(this,r,e);case"base64":return A(this,r,e);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return R(this,r,e);default:if(n)throw new TypeError("Unknown encoding: "+t);t=(t+"").toLowerCase(),n=!0}}.apply(this,arguments)},r.prototype.toLocaleString=r.prototype.toString,r.prototype.equals=function(t){if(!r.isBuffer(t))throw new TypeError("Argument must be a Buffer");return this===t||0===r.compare(this,t)},r.prototype.inspect=function(){var t="",r=e.INSPECT_MAX_BYTES;return t=this.toString("hex",0,r).replace(/(.{2})/g,"$1 ").trim(),this.length>r&&(t+=" ... "),""},o&&(r.prototype[o]=r.prototype.inspect),r.prototype.compare=function(t,e,n,i,o){if(z(t,Uint8Array)&&(t=r.from(t,t.offset,t.byteLength)),!r.isBuffer(t))throw new TypeError('The "target" argument must be one of type Buffer or Uint8Array. Received type '+typeof t);if(void 0===e&&(e=0),void 0===n&&(n=t?t.length:0),void 0===i&&(i=0),void 0===o&&(o=this.length),e<0||n>t.length||i<0||o>this.length)throw new RangeError("out of range index");if(i>=o&&e>=n)return 0;if(i>=o)return-1;if(e>=n)return 1;if(this===t)return 0;for(var f=(o>>>=0)-(i>>>=0),u=(n>>>=0)-(e>>>=0),s=Math.min(f,u),h=this.slice(i,o),a=t.slice(e,n),p=0;p>>=0,isFinite(e)?(e>>>=0,void 0===n&&(n="utf8")):(n=e,e=void 0)}var i=this.length-r;if((void 0===e||e>i)&&(e=i),t.length>0&&(e<0||r<0)||r>this.length)throw new RangeError("Attempt to write outside buffer bounds");n||(n="utf8");for(var o=!1;;)switch(n){case"hex":return d(this,t,r,e);case"utf8":case"utf-8":return v(this,t,r,e);case"ascii":return b(this,t,r,e);case"latin1":case"binary":return m(this,t,r,e);case"base64":return E(this,t,r,e);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return B(this,t,r,e);default:if(o)throw new TypeError("Unknown encoding: "+n);n=(""+n).toLowerCase(),o=!0}},r.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var T=4096;function I(t,r,e){var n="";e=Math.min(t.length,e);for(var i=r;in)&&(e=n);for(var i="",o=r;oe)throw new RangeError("Trying to access beyond buffer length")}function O(t,e,n,i,o,f){if(!r.isBuffer(t))throw new TypeError('"buffer" argument must be a Buffer instance');if(e>o||et.length)throw new RangeError("Index out of range")}function _(t,r,e,n,i,o){if(e+n>t.length)throw new RangeError("Index out of range");if(e<0)throw new RangeError("Index out of range")}function x(t,r,e,n,o){return r=+r,e>>>=0,o||_(t,0,e,4),i.write(t,r,e,n,23,4),e+4}function M(t,r,e,n,o){return r=+r,e>>>=0,o||_(t,0,e,8),i.write(t,r,e,n,52,8),e+8}r.prototype.slice=function(t,e){var n=this.length;(t=~~t)<0?(t+=n)<0&&(t=0):t>n&&(t=n),(e=void 0===e?n:~~e)<0?(e+=n)<0&&(e=0):e>n&&(e=n),e>>=0,r>>>=0,e||C(t,r,this.length);for(var n=this[t],i=1,o=0;++o>>=0,r>>>=0,e||C(t,r,this.length);for(var n=this[t+--r],i=1;r>0&&(i*=256);)n+=this[t+--r]*i;return n},r.prototype.readUInt8=function(t,r){return t>>>=0,r||C(t,1,this.length),this[t]},r.prototype.readUInt16LE=function(t,r){return t>>>=0,r||C(t,2,this.length),this[t]|this[t+1]<<8},r.prototype.readUInt16BE=function(t,r){return t>>>=0,r||C(t,2,this.length),this[t]<<8|this[t+1]},r.prototype.readUInt32LE=function(t,r){return t>>>=0,r||C(t,4,this.length),(this[t]|this[t+1]<<8|this[t+2]<<16)+16777216*this[t+3]},r.prototype.readUInt32BE=function(t,r){return t>>>=0,r||C(t,4,this.length),16777216*this[t]+(this[t+1]<<16|this[t+2]<<8|this[t+3])},r.prototype.readIntLE=function(t,r,e){t>>>=0,r>>>=0,e||C(t,r,this.length);for(var n=this[t],i=1,o=0;++o=(i*=128)&&(n-=Math.pow(2,8*r)),n},r.prototype.readIntBE=function(t,r,e){t>>>=0,r>>>=0,e||C(t,r,this.length);for(var n=r,i=1,o=this[t+--n];n>0&&(i*=256);)o+=this[t+--n]*i;return o>=(i*=128)&&(o-=Math.pow(2,8*r)),o},r.prototype.readInt8=function(t,r){return t>>>=0,r||C(t,1,this.length),128&this[t]?-1*(255-this[t]+1):this[t]},r.prototype.readInt16LE=function(t,r){t>>>=0,r||C(t,2,this.length);var e=this[t]|this[t+1]<<8;return 32768&e?4294901760|e:e},r.prototype.readInt16BE=function(t,r){t>>>=0,r||C(t,2,this.length);var e=this[t+1]|this[t]<<8;return 32768&e?4294901760|e:e},r.prototype.readInt32LE=function(t,r){return t>>>=0,r||C(t,4,this.length),this[t]|this[t+1]<<8|this[t+2]<<16|this[t+3]<<24},r.prototype.readInt32BE=function(t,r){return t>>>=0,r||C(t,4,this.length),this[t]<<24|this[t+1]<<16|this[t+2]<<8|this[t+3]},r.prototype.readFloatLE=function(t,r){return t>>>=0,r||C(t,4,this.length),i.read(this,t,!0,23,4)},r.prototype.readFloatBE=function(t,r){return t>>>=0,r||C(t,4,this.length),i.read(this,t,!1,23,4)},r.prototype.readDoubleLE=function(t,r){return t>>>=0,r||C(t,8,this.length),i.read(this,t,!0,52,8)},r.prototype.readDoubleBE=function(t,r){return t>>>=0,r||C(t,8,this.length),i.read(this,t,!1,52,8)},r.prototype.writeUIntLE=function(t,r,e,n){(t=+t,r>>>=0,e>>>=0,n)||O(this,t,r,e,Math.pow(2,8*e)-1,0);var i=1,o=0;for(this[r]=255&t;++o>>=0,e>>>=0,n)||O(this,t,r,e,Math.pow(2,8*e)-1,0);var i=e-1,o=1;for(this[r+i]=255&t;--i>=0&&(o*=256);)this[r+i]=t/o&255;return r+e},r.prototype.writeUInt8=function(t,r,e){return t=+t,r>>>=0,e||O(this,t,r,1,255,0),this[r]=255&t,r+1},r.prototype.writeUInt16LE=function(t,r,e){return t=+t,r>>>=0,e||O(this,t,r,2,65535,0),this[r]=255&t,this[r+1]=t>>>8,r+2},r.prototype.writeUInt16BE=function(t,r,e){return t=+t,r>>>=0,e||O(this,t,r,2,65535,0),this[r]=t>>>8,this[r+1]=255&t,r+2},r.prototype.writeUInt32LE=function(t,r,e){return t=+t,r>>>=0,e||O(this,t,r,4,4294967295,0),this[r+3]=t>>>24,this[r+2]=t>>>16,this[r+1]=t>>>8,this[r]=255&t,r+4},r.prototype.writeUInt32BE=function(t,r,e){return t=+t,r>>>=0,e||O(this,t,r,4,4294967295,0),this[r]=t>>>24,this[r+1]=t>>>16,this[r+2]=t>>>8,this[r+3]=255&t,r+4},r.prototype.writeIntLE=function(t,r,e,n){if(t=+t,r>>>=0,!n){var i=Math.pow(2,8*e-1);O(this,t,r,e,i-1,-i)}var o=0,f=1,u=0;for(this[r]=255&t;++o>0)-u&255;return r+e},r.prototype.writeIntBE=function(t,r,e,n){if(t=+t,r>>>=0,!n){var i=Math.pow(2,8*e-1);O(this,t,r,e,i-1,-i)}var o=e-1,f=1,u=0;for(this[r+o]=255&t;--o>=0&&(f*=256);)t<0&&0===u&&0!==this[r+o+1]&&(u=1),this[r+o]=(t/f>>0)-u&255;return r+e},r.prototype.writeInt8=function(t,r,e){return t=+t,r>>>=0,e||O(this,t,r,1,127,-128),t<0&&(t=255+t+1),this[r]=255&t,r+1},r.prototype.writeInt16LE=function(t,r,e){return t=+t,r>>>=0,e||O(this,t,r,2,32767,-32768),this[r]=255&t,this[r+1]=t>>>8,r+2},r.prototype.writeInt16BE=function(t,r,e){return t=+t,r>>>=0,e||O(this,t,r,2,32767,-32768),this[r]=t>>>8,this[r+1]=255&t,r+2},r.prototype.writeInt32LE=function(t,r,e){return t=+t,r>>>=0,e||O(this,t,r,4,2147483647,-2147483648),this[r]=255&t,this[r+1]=t>>>8,this[r+2]=t>>>16,this[r+3]=t>>>24,r+4},r.prototype.writeInt32BE=function(t,r,e){return t=+t,r>>>=0,e||O(this,t,r,4,2147483647,-2147483648),t<0&&(t=4294967295+t+1),this[r]=t>>>24,this[r+1]=t>>>16,this[r+2]=t>>>8,this[r+3]=255&t,r+4},r.prototype.writeFloatLE=function(t,r,e){return x(this,t,r,!0,e)},r.prototype.writeFloatBE=function(t,r,e){return x(this,t,r,!1,e)},r.prototype.writeDoubleLE=function(t,r,e){return M(this,t,r,!0,e)},r.prototype.writeDoubleBE=function(t,r,e){return M(this,t,r,!1,e)},r.prototype.copy=function(t,e,n,i){if(!r.isBuffer(t))throw new TypeError("argument should be a Buffer");if(n||(n=0),i||0===i||(i=this.length),e>=t.length&&(e=t.length),e||(e=0),i>0&&i=this.length)throw new RangeError("Index out of range");if(i<0)throw new RangeError("sourceEnd out of bounds");i>this.length&&(i=this.length),t.length-e=0;--f)t[f+e]=this[f+n];else Uint8Array.prototype.set.call(t,this.subarray(n,i),e);return o},r.prototype.fill=function(t,e,n,i){if("string"==typeof t){if("string"==typeof e?(i=e,e=0,n=this.length):"string"==typeof n&&(i=n,n=this.length),void 0!==i&&"string"!=typeof i)throw new TypeError("encoding must be a string");if("string"==typeof i&&!r.isEncoding(i))throw new TypeError("Unknown encoding: "+i);if(1===t.length){var o=t.charCodeAt(0);("utf8"===i&&o<128||"latin1"===i)&&(t=o)}}else"number"==typeof t?t&=255:"boolean"==typeof t&&(t=Number(t));if(e<0||this.length>>=0,n=void 0===n?this.length:n>>>0,t||(t=0),"number"==typeof t)for(f=e;f55295&&e<57344){if(!i){if(e>56319){(r-=3)>-1&&o.push(239,191,189);continue}if(f+1===n){(r-=3)>-1&&o.push(239,191,189);continue}i=e;continue}if(e<56320){(r-=3)>-1&&o.push(239,191,189),i=e;continue}e=65536+(i-55296<<10|e-56320)}else i&&(r-=3)>-1&&o.push(239,191,189);if(i=null,e<128){if((r-=1)<0)break;o.push(e)}else if(e<2048){if((r-=2)<0)break;o.push(e>>6|192,63&e|128)}else if(e<65536){if((r-=3)<0)break;o.push(e>>12|224,e>>6&63|128,63&e|128)}else{if(!(e<1114112))throw new Error("Invalid code point");if((r-=4)<0)break;o.push(e>>18|240,e>>12&63|128,e>>6&63|128,63&e|128)}}return o}function j(t){return n.toByteArray(function(t){if((t=(t=t.split("=")[0]).trim().replace(k,"")).length<2)return"";for(;t.length%4!=0;)t+="=";return t}(t))}function N(t,r,e,n){for(var i=0;i=r.length||i>=t.length);++i)r[i+e]=t[i];return i}function z(t,r){return t instanceof r||null!=t&&null!=t.constructor&&null!=t.constructor.name&&t.constructor.name===r.name}function D(t){return t!=t}var F=function(){for(var t=new Array(256),r=0;r<16;++r)for(var e=16*r,n=0;n<16;++n)t[e+n]="0123456789abcdef"[r]+"0123456789abcdef"[n];return t}()}).call(this,t("buffer").Buffer)},{"base64-js":2,buffer:5,ieee754:3}],2:[function(t,r,e){"use strict";e.byteLength=function(t){var r=h(t),e=r[0],n=r[1];return 3*(e+n)/4-n},e.toByteArray=function(t){var r,e,n=h(t),f=n[0],u=n[1],s=new o(function(t,r,e){return 3*(r+e)/4-e}(0,f,u)),a=0,p=u>0?f-4:f;for(e=0;e>16&255,s[a++]=r>>8&255,s[a++]=255&r;2===u&&(r=i[t.charCodeAt(e)]<<2|i[t.charCodeAt(e+1)]>>4,s[a++]=255&r);1===u&&(r=i[t.charCodeAt(e)]<<10|i[t.charCodeAt(e+1)]<<4|i[t.charCodeAt(e+2)]>>2,s[a++]=r>>8&255,s[a++]=255&r);return s},e.fromByteArray=function(t){for(var r,e=t.length,i=e%3,o=[],f=0,u=e-i;fu?u:f+16383));1===i?(r=t[e-1],o.push(n[r>>2]+n[r<<4&63]+"==")):2===i&&(r=(t[e-2]<<8)+t[e-1],o.push(n[r>>10]+n[r>>4&63]+n[r<<2&63]+"="));return o.join("")};for(var n=[],i=[],o="undefined"!=typeof Uint8Array?Uint8Array:Array,f="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",u=0,s=f.length;u0)throw new Error("Invalid string. Length must be a multiple of 4");var e=t.indexOf("=");return-1===e&&(e=r),[e,e===r?0:4-e%4]}function a(t,r,e){for(var i,o,f=[],u=r;u>18&63]+n[o>>12&63]+n[o>>6&63]+n[63&o]);return f.join("")}i["-".charCodeAt(0)]=62,i["_".charCodeAt(0)]=63},{}],3:[function(t,r,e){e.read=function(t,r,e,n,i){var o,f,u=8*i-n-1,s=(1<>1,a=-7,p=e?i-1:0,c=e?-1:1,l=t[r+p];for(p+=c,o=l&(1<<-a)-1,l>>=-a,a+=u;a>0;o=256*o+t[r+p],p+=c,a-=8);for(f=o&(1<<-a)-1,o>>=-a,a+=n;a>0;f=256*f+t[r+p],p+=c,a-=8);if(0===o)o=1-h;else{if(o===s)return f?NaN:1/0*(l?-1:1);f+=Math.pow(2,n),o-=h}return(l?-1:1)*f*Math.pow(2,o-n)},e.write=function(t,r,e,n,i,o){var f,u,s,h=8*o-i-1,a=(1<>1,c=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,l=n?0:o-1,y=n?1:-1,g=r<0||0===r&&1/r<0?1:0;for(r=Math.abs(r),isNaN(r)||r===1/0?(u=isNaN(r)?1:0,f=a):(f=Math.floor(Math.log(r)/Math.LN2),r*(s=Math.pow(2,-f))<1&&(f--,s*=2),(r+=f+p>=1?c/s:c*Math.pow(2,1-p))*s>=2&&(f++,s/=2),f+p>=a?(u=0,f=a):f+p>=1?(u=(r*s-1)*Math.pow(2,i),f+=p):(u=r*Math.pow(2,p-1)*Math.pow(2,i),f=0));i>=8;t[e+l]=255&u,l+=y,u/=256,i-=8);for(f=f<0;t[e+l]=255&f,l+=y,f/=256,h-=8);t[e+l-y]|=128*g}},{}],4:[function(t,r,e){arguments[4][2][0].apply(e,arguments)},{dup:2}],5:[function(t,r,e){(function(r){"use strict";var n=t("base64-js"),i=t("ieee754");e.Buffer=r,e.SlowBuffer=function(t){+t!=t&&(t=0);return r.alloc(+t)},e.INSPECT_MAX_BYTES=50;var o=2147483647;function f(t){if(t>o)throw new RangeError('The value "'+t+'" is invalid for option "size"');var e=new Uint8Array(t);return e.__proto__=r.prototype,e}function r(t,r,e){if("number"==typeof t){if("string"==typeof r)throw new TypeError('The "string" argument must be of type string. Received type number');return h(t)}return u(t,r,e)}function u(t,e,n){if("string"==typeof t)return function(t,e){"string"==typeof e&&""!==e||(e="utf8");if(!r.isEncoding(e))throw new TypeError("Unknown encoding: "+e);var n=0|c(t,e),i=f(n),o=i.write(t,e);o!==n&&(i=i.slice(0,o));return i}(t,e);if(ArrayBuffer.isView(t))return a(t);if(null==t)throw TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof t);if(z(t,ArrayBuffer)||t&&z(t.buffer,ArrayBuffer))return function(t,e,n){if(e<0||t.byteLength=o)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+o.toString(16)+" bytes");return 0|t}function c(t,e){if(r.isBuffer(t))return t.length;if(ArrayBuffer.isView(t)||z(t,ArrayBuffer))return t.byteLength;if("string"!=typeof t)throw new TypeError('The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof t);var n=t.length,i=arguments.length>2&&!0===arguments[2];if(!i&&0===n)return 0;for(var o=!1;;)switch(e){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":return P(t).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return j(t).length;default:if(o)return i?-1:P(t).length;e=(""+e).toLowerCase(),o=!0}}function l(t,r,e){var n=t[r];t[r]=t[e],t[e]=n}function y(t,e,n,i,o){if(0===t.length)return-1;if("string"==typeof n?(i=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),D(n=+n)&&(n=o?0:t.length-1),n<0&&(n=t.length+n),n>=t.length){if(o)return-1;n=t.length-1}else if(n<0){if(!o)return-1;n=0}if("string"==typeof e&&(e=r.from(e,i)),r.isBuffer(e))return 0===e.length?-1:g(t,e,n,i,o);if("number"==typeof e)return e&=255,"function"==typeof Uint8Array.prototype.indexOf?o?Uint8Array.prototype.indexOf.call(t,e,n):Uint8Array.prototype.lastIndexOf.call(t,e,n):g(t,[e],n,i,o);throw new TypeError("val must be string, number or Buffer")}function g(t,r,e,n,i){var o,f=1,u=t.length,s=r.length;if(void 0!==n&&("ucs2"===(n=String(n).toLowerCase())||"ucs-2"===n||"utf16le"===n||"utf-16le"===n)){if(t.length<2||r.length<2)return-1;f=2,u/=2,s/=2,e/=2}function h(t,r){return 1===f?t[r]:t.readUInt16BE(r*f)}if(i){var a=-1;for(o=e;ou&&(e=u-s),o=e;o>=0;o--){for(var p=!0,c=0;ci&&(n=i):n=i;var o=r.length;n>o/2&&(n=o/2);for(var f=0;f>8,i=e%256,o.push(i),o.push(n);return o}(r,t.length-e),t,e,n)}function B(t,r,e){return 0===r&&e===t.length?n.fromByteArray(t):n.fromByteArray(t.slice(r,e))}function A(t,r,e){e=Math.min(t.length,e);for(var n=[],i=r;i239?4:h>223?3:h>191?2:1;if(i+p<=e)switch(p){case 1:h<128&&(a=h);break;case 2:128==(192&(o=t[i+1]))&&(s=(31&h)<<6|63&o)>127&&(a=s);break;case 3:o=t[i+1],f=t[i+2],128==(192&o)&&128==(192&f)&&(s=(15&h)<<12|(63&o)<<6|63&f)>2047&&(s<55296||s>57343)&&(a=s);break;case 4:o=t[i+1],f=t[i+2],u=t[i+3],128==(192&o)&&128==(192&f)&&128==(192&u)&&(s=(15&h)<<18|(63&o)<<12|(63&f)<<6|63&u)>65535&&s<1114112&&(a=s)}null===a?(a=65533,p=1):a>65535&&(a-=65536,n.push(a>>>10&1023|55296),a=56320|1023&a),n.push(a),i+=p}return function(t){var r=t.length;if(r<=U)return String.fromCharCode.apply(String,t);var e="",n=0;for(;nthis.length)return"";if((void 0===e||e>this.length)&&(e=this.length),e<=0)return"";if((e>>>=0)<=(r>>>=0))return"";for(t||(t="utf8");;)switch(t){case"hex":return S(this,r,e);case"utf8":case"utf-8":return A(this,r,e);case"ascii":return T(this,r,e);case"latin1":case"binary":return I(this,r,e);case"base64":return B(this,r,e);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return L(this,r,e);default:if(n)throw new TypeError("Unknown encoding: "+t);t=(t+"").toLowerCase(),n=!0}}.apply(this,arguments)},r.prototype.toLocaleString=r.prototype.toString,r.prototype.equals=function(t){if(!r.isBuffer(t))throw new TypeError("Argument must be a Buffer");return this===t||0===r.compare(this,t)},r.prototype.inspect=function(){var t="",r=e.INSPECT_MAX_BYTES;return t=this.toString("hex",0,r).replace(/(.{2})/g,"$1 ").trim(),this.length>r&&(t+=" ... "),""},r.prototype.compare=function(t,e,n,i,o){if(z(t,Uint8Array)&&(t=r.from(t,t.offset,t.byteLength)),!r.isBuffer(t))throw new TypeError('The "target" argument must be one of type Buffer or Uint8Array. Received type '+typeof t);if(void 0===e&&(e=0),void 0===n&&(n=t?t.length:0),void 0===i&&(i=0),void 0===o&&(o=this.length),e<0||n>t.length||i<0||o>this.length)throw new RangeError("out of range index");if(i>=o&&e>=n)return 0;if(i>=o)return-1;if(e>=n)return 1;if(this===t)return 0;for(var f=(o>>>=0)-(i>>>=0),u=(n>>>=0)-(e>>>=0),s=Math.min(f,u),h=this.slice(i,o),a=t.slice(e,n),p=0;p>>=0,isFinite(e)?(e>>>=0,void 0===n&&(n="utf8")):(n=e,e=void 0)}var i=this.length-r;if((void 0===e||e>i)&&(e=i),t.length>0&&(e<0||r<0)||r>this.length)throw new RangeError("Attempt to write outside buffer bounds");n||(n="utf8");for(var o=!1;;)switch(n){case"hex":return w(this,t,r,e);case"utf8":case"utf-8":return d(this,t,r,e);case"ascii":return v(this,t,r,e);case"latin1":case"binary":return b(this,t,r,e);case"base64":return m(this,t,r,e);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return E(this,t,r,e);default:if(o)throw new TypeError("Unknown encoding: "+n);n=(""+n).toLowerCase(),o=!0}},r.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var U=4096;function T(t,r,e){var n="";e=Math.min(t.length,e);for(var i=r;in)&&(e=n);for(var i="",o=r;oe)throw new RangeError("Trying to access beyond buffer length")}function C(t,e,n,i,o,f){if(!r.isBuffer(t))throw new TypeError('"buffer" argument must be a Buffer instance');if(e>o||et.length)throw new RangeError("Index out of range")}function O(t,r,e,n,i,o){if(e+n>t.length)throw new RangeError("Index out of range");if(e<0)throw new RangeError("Index out of range")}function _(t,r,e,n,o){return r=+r,e>>>=0,o||O(t,0,e,4),i.write(t,r,e,n,23,4),e+4}function x(t,r,e,n,o){return r=+r,e>>>=0,o||O(t,0,e,8),i.write(t,r,e,n,52,8),e+8}r.prototype.slice=function(t,e){var n=this.length;(t=~~t)<0?(t+=n)<0&&(t=0):t>n&&(t=n),(e=void 0===e?n:~~e)<0?(e+=n)<0&&(e=0):e>n&&(e=n),e>>=0,r>>>=0,e||R(t,r,this.length);for(var n=this[t],i=1,o=0;++o>>=0,r>>>=0,e||R(t,r,this.length);for(var n=this[t+--r],i=1;r>0&&(i*=256);)n+=this[t+--r]*i;return n},r.prototype.readUInt8=function(t,r){return t>>>=0,r||R(t,1,this.length),this[t]},r.prototype.readUInt16LE=function(t,r){return t>>>=0,r||R(t,2,this.length),this[t]|this[t+1]<<8},r.prototype.readUInt16BE=function(t,r){return t>>>=0,r||R(t,2,this.length),this[t]<<8|this[t+1]},r.prototype.readUInt32LE=function(t,r){return t>>>=0,r||R(t,4,this.length),(this[t]|this[t+1]<<8|this[t+2]<<16)+16777216*this[t+3]},r.prototype.readUInt32BE=function(t,r){return t>>>=0,r||R(t,4,this.length),16777216*this[t]+(this[t+1]<<16|this[t+2]<<8|this[t+3])},r.prototype.readIntLE=function(t,r,e){t>>>=0,r>>>=0,e||R(t,r,this.length);for(var n=this[t],i=1,o=0;++o=(i*=128)&&(n-=Math.pow(2,8*r)),n},r.prototype.readIntBE=function(t,r,e){t>>>=0,r>>>=0,e||R(t,r,this.length);for(var n=r,i=1,o=this[t+--n];n>0&&(i*=256);)o+=this[t+--n]*i;return o>=(i*=128)&&(o-=Math.pow(2,8*r)),o},r.prototype.readInt8=function(t,r){return t>>>=0,r||R(t,1,this.length),128&this[t]?-1*(255-this[t]+1):this[t]},r.prototype.readInt16LE=function(t,r){t>>>=0,r||R(t,2,this.length);var e=this[t]|this[t+1]<<8;return 32768&e?4294901760|e:e},r.prototype.readInt16BE=function(t,r){t>>>=0,r||R(t,2,this.length);var e=this[t+1]|this[t]<<8;return 32768&e?4294901760|e:e},r.prototype.readInt32LE=function(t,r){return t>>>=0,r||R(t,4,this.length),this[t]|this[t+1]<<8|this[t+2]<<16|this[t+3]<<24},r.prototype.readInt32BE=function(t,r){return t>>>=0,r||R(t,4,this.length),this[t]<<24|this[t+1]<<16|this[t+2]<<8|this[t+3]},r.prototype.readFloatLE=function(t,r){return t>>>=0,r||R(t,4,this.length),i.read(this,t,!0,23,4)},r.prototype.readFloatBE=function(t,r){return t>>>=0,r||R(t,4,this.length),i.read(this,t,!1,23,4)},r.prototype.readDoubleLE=function(t,r){return t>>>=0,r||R(t,8,this.length),i.read(this,t,!0,52,8)},r.prototype.readDoubleBE=function(t,r){return t>>>=0,r||R(t,8,this.length),i.read(this,t,!1,52,8)},r.prototype.writeUIntLE=function(t,r,e,n){(t=+t,r>>>=0,e>>>=0,n)||C(this,t,r,e,Math.pow(2,8*e)-1,0);var i=1,o=0;for(this[r]=255&t;++o>>=0,e>>>=0,n)||C(this,t,r,e,Math.pow(2,8*e)-1,0);var i=e-1,o=1;for(this[r+i]=255&t;--i>=0&&(o*=256);)this[r+i]=t/o&255;return r+e},r.prototype.writeUInt8=function(t,r,e){return t=+t,r>>>=0,e||C(this,t,r,1,255,0),this[r]=255&t,r+1},r.prototype.writeUInt16LE=function(t,r,e){return t=+t,r>>>=0,e||C(this,t,r,2,65535,0),this[r]=255&t,this[r+1]=t>>>8,r+2},r.prototype.writeUInt16BE=function(t,r,e){return t=+t,r>>>=0,e||C(this,t,r,2,65535,0),this[r]=t>>>8,this[r+1]=255&t,r+2},r.prototype.writeUInt32LE=function(t,r,e){return t=+t,r>>>=0,e||C(this,t,r,4,4294967295,0),this[r+3]=t>>>24,this[r+2]=t>>>16,this[r+1]=t>>>8,this[r]=255&t,r+4},r.prototype.writeUInt32BE=function(t,r,e){return t=+t,r>>>=0,e||C(this,t,r,4,4294967295,0),this[r]=t>>>24,this[r+1]=t>>>16,this[r+2]=t>>>8,this[r+3]=255&t,r+4},r.prototype.writeIntLE=function(t,r,e,n){if(t=+t,r>>>=0,!n){var i=Math.pow(2,8*e-1);C(this,t,r,e,i-1,-i)}var o=0,f=1,u=0;for(this[r]=255&t;++o>0)-u&255;return r+e},r.prototype.writeIntBE=function(t,r,e,n){if(t=+t,r>>>=0,!n){var i=Math.pow(2,8*e-1);C(this,t,r,e,i-1,-i)}var o=e-1,f=1,u=0;for(this[r+o]=255&t;--o>=0&&(f*=256);)t<0&&0===u&&0!==this[r+o+1]&&(u=1),this[r+o]=(t/f>>0)-u&255;return r+e},r.prototype.writeInt8=function(t,r,e){return t=+t,r>>>=0,e||C(this,t,r,1,127,-128),t<0&&(t=255+t+1),this[r]=255&t,r+1},r.prototype.writeInt16LE=function(t,r,e){return t=+t,r>>>=0,e||C(this,t,r,2,32767,-32768),this[r]=255&t,this[r+1]=t>>>8,r+2},r.prototype.writeInt16BE=function(t,r,e){return t=+t,r>>>=0,e||C(this,t,r,2,32767,-32768),this[r]=t>>>8,this[r+1]=255&t,r+2},r.prototype.writeInt32LE=function(t,r,e){return t=+t,r>>>=0,e||C(this,t,r,4,2147483647,-2147483648),this[r]=255&t,this[r+1]=t>>>8,this[r+2]=t>>>16,this[r+3]=t>>>24,r+4},r.prototype.writeInt32BE=function(t,r,e){return t=+t,r>>>=0,e||C(this,t,r,4,2147483647,-2147483648),t<0&&(t=4294967295+t+1),this[r]=t>>>24,this[r+1]=t>>>16,this[r+2]=t>>>8,this[r+3]=255&t,r+4},r.prototype.writeFloatLE=function(t,r,e){return _(this,t,r,!0,e)},r.prototype.writeFloatBE=function(t,r,e){return _(this,t,r,!1,e)},r.prototype.writeDoubleLE=function(t,r,e){return x(this,t,r,!0,e)},r.prototype.writeDoubleBE=function(t,r,e){return x(this,t,r,!1,e)},r.prototype.copy=function(t,e,n,i){if(!r.isBuffer(t))throw new TypeError("argument should be a Buffer");if(n||(n=0),i||0===i||(i=this.length),e>=t.length&&(e=t.length),e||(e=0),i>0&&i=this.length)throw new RangeError("Index out of range");if(i<0)throw new RangeError("sourceEnd out of bounds");i>this.length&&(i=this.length),t.length-e=0;--f)t[f+e]=this[f+n];else Uint8Array.prototype.set.call(t,this.subarray(n,i),e);return o},r.prototype.fill=function(t,e,n,i){if("string"==typeof t){if("string"==typeof e?(i=e,e=0,n=this.length):"string"==typeof n&&(i=n,n=this.length),void 0!==i&&"string"!=typeof i)throw new TypeError("encoding must be a string");if("string"==typeof i&&!r.isEncoding(i))throw new TypeError("Unknown encoding: "+i);if(1===t.length){var o=t.charCodeAt(0);("utf8"===i&&o<128||"latin1"===i)&&(t=o)}}else"number"==typeof t&&(t&=255);if(e<0||this.length>>=0,n=void 0===n?this.length:n>>>0,t||(t=0),"number"==typeof t)for(f=e;f55295&&e<57344){if(!i){if(e>56319){(r-=3)>-1&&o.push(239,191,189);continue}if(f+1===n){(r-=3)>-1&&o.push(239,191,189);continue}i=e;continue}if(e<56320){(r-=3)>-1&&o.push(239,191,189),i=e;continue}e=65536+(i-55296<<10|e-56320)}else i&&(r-=3)>-1&&o.push(239,191,189);if(i=null,e<128){if((r-=1)<0)break;o.push(e)}else if(e<2048){if((r-=2)<0)break;o.push(e>>6|192,63&e|128)}else if(e<65536){if((r-=3)<0)break;o.push(e>>12|224,e>>6&63|128,63&e|128)}else{if(!(e<1114112))throw new Error("Invalid code point");if((r-=4)<0)break;o.push(e>>18|240,e>>12&63|128,e>>6&63|128,63&e|128)}}return o}function j(t){return n.toByteArray(function(t){if((t=(t=t.split("=")[0]).trim().replace(M,"")).length<2)return"";for(;t.length%4!=0;)t+="=";return t}(t))}function N(t,r,e,n){for(var i=0;i=r.length||i>=t.length);++i)r[i+e]=t[i];return i}function z(t,r){return t instanceof r||null!=t&&null!=t.constructor&&null!=t.constructor.name&&t.constructor.name===r.name}function D(t){return t!=t}}).call(this,t("buffer").Buffer)},{"base64-js":4,buffer:5,ieee754:6}],6:[function(t,r,e){arguments[4][3][0].apply(e,arguments)},{dup:3}]},{},[1])(1)}); -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Twoblocks", 3 | "name": "Twoblocks", 4 | "icons": [ 5 | { 6 | "src": "https://twoblocks.leopradel.com/icon-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/undraw_authentication_fsn5.svg: -------------------------------------------------------------------------------- 1 | authentication -------------------------------------------------------------------------------- /scripts/copy-lib.sh: -------------------------------------------------------------------------------- 1 | cp ./node_modules/@otplib/preset-browser/buffer.js public/lib/otplib-browser-buffer.js 2 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo, useCallback } from 'react'; 2 | import * as Sentry from '@sentry/react'; 3 | import { ThemeProvider } from '@material-ui/styles'; 4 | import { createTheme } from '@material-ui/core/styles'; 5 | import { CssBaseline } from '@material-ui/core'; 6 | import * as Fathom from 'fathom-client'; 7 | import { showConnect } from '@stacks/connect'; 8 | import { Login } from './components/Login'; 9 | import { Home } from './components/Home'; 10 | import { Loader } from './components/Loader'; 11 | import { ThemeContext, themeStorageKey } from './context/ThemeContext'; 12 | import { FileContextProvider } from './context/FileContext'; 13 | import { userSession } from './utils/blockstack'; 14 | import { Goals } from './utils/fathom'; 15 | import { config } from './config'; 16 | 17 | // Track when page is loaded 18 | const FathomTrack = () => { 19 | useEffect(() => { 20 | if (config.fathomSiteId) { 21 | Fathom.load(config.fathomSiteId); 22 | Fathom.trackPageview(); 23 | } 24 | }, []); 25 | 26 | return ; 27 | }; 28 | 29 | const App = () => { 30 | const localTheme = localStorage.getItem('theme'); 31 | const [theme, setTheme] = useState<'light' | 'dark'>( 32 | localTheme === 'dark' ? 'dark' : 'light' 33 | ); 34 | const [loggedIn, setLoggedIn] = useState(!!userSession.isUserSignedIn()); 35 | const [loggingIn, setLoggingIn] = useState(!!userSession.isSignInPending()); 36 | 37 | const muiTheme = useMemo( 38 | () => 39 | createTheme({ 40 | palette: { 41 | type: theme, 42 | }, 43 | }), 44 | [theme] 45 | ); 46 | 47 | const handleChangeTheme = (data: 'light' | 'dark') => { 48 | setTheme(data); 49 | localStorage.setItem(themeStorageKey, data); 50 | }; 51 | 52 | const handleLogin = useCallback(() => { 53 | Fathom.trackGoal(Goals.LOGIN, 0); 54 | showConnect({ 55 | redirectTo: '/', 56 | appDetails: { 57 | name: 'Twoblocks', 58 | icon: 'https://twoblocks.leopradel.com/icon-192x192.png', 59 | }, 60 | onFinish: () => { 61 | setLoggingIn(false); 62 | setLoggedIn(true); 63 | }, 64 | }); 65 | }, [setLoggingIn]); 66 | 67 | useEffect(() => { 68 | if (userSession.isSignInPending()) { 69 | userSession 70 | .handlePendingSignIn() 71 | .then(() => { 72 | setLoggingIn(false); 73 | setLoggedIn(true); 74 | }) 75 | .catch((error: any) => { 76 | setLoggingIn(false); 77 | alert(error.message); 78 | }); 79 | } 80 | }, []); 81 | 82 | return ( 83 | 84 | 85 | 86 | 87 | {!loggingIn && !loggedIn && } 88 | {!loggingIn && loggedIn && ( 89 | 90 | 91 | 92 | )} 93 | {loggingIn && } 94 | 95 | 96 | ); 97 | }; 98 | 99 | const FallbackComponent = () => { 100 | return
An error has occurred
; 101 | }; 102 | 103 | const AppWithError = () => { 104 | return ( 105 | 106 | 107 | 108 | ); 109 | }; 110 | 111 | export default AppWithError; 112 | -------------------------------------------------------------------------------- /src/components/AccountItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react'; 2 | import { 3 | Typography, 4 | IconButton, 5 | Menu, 6 | MenuItem, 7 | Theme, 8 | } from '@material-ui/core'; 9 | import { makeStyles } from '@material-ui/styles'; 10 | import { MoreVert } from '@material-ui/icons'; 11 | import { authenticator } from '@otplib/preset-browser'; 12 | import { Account } from '../types'; 13 | import { EditAccount } from './EditAccount'; 14 | import { DeleteAccount } from './DeleteAccount'; 15 | import { ThemeContext } from '../context/ThemeContext'; 16 | import { icons } from '../utils/icons'; 17 | 18 | const useStyles = makeStyles((theme: Theme) => ({ 19 | iconContainer: { 20 | display: 'flex', 21 | alignItems: 'center', 22 | marginRight: theme.spacing(2), 23 | }, 24 | leftContainer: { 25 | flex: 1, 26 | marginTop: theme.spacing(), 27 | }, 28 | name: { 29 | marginBottom: theme.spacing(), 30 | }, 31 | container: { 32 | marginTop: theme.spacing(2), 33 | marginBottom: theme.spacing(2), 34 | paddingTop: theme.spacing(), 35 | paddingBottom: theme.spacing(), 36 | paddingLeft: theme.spacing(2), 37 | paddingRight: theme.spacing(2), 38 | display: 'flex', 39 | flexDirection: 'row', 40 | backgroundColor: theme.palette.background.paper, 41 | }, 42 | timer: { 43 | display: 'flex', 44 | alignItems: 'center', 45 | flexDirection: 'column', 46 | }, 47 | })); 48 | 49 | interface Props { 50 | index: number; 51 | account: Account; 52 | remainingSeconds: number; 53 | } 54 | 55 | export const AccountItem = (props: Props) => { 56 | const classes = useStyles(); 57 | const [anchorEl, setAnchorEl] = useState(null); 58 | const open = Boolean(anchorEl); 59 | const [editModalOpen, setEditModalOpen] = useState(false); 60 | const [deleteModalOpen, setDeleteModalOpen] = useState(false); 61 | const theme = useContext(ThemeContext); 62 | 63 | const handleRequestDelete = () => { 64 | setAnchorEl(null); 65 | setDeleteModalOpen(true); 66 | }; 67 | 68 | const handleRequestEdit = () => { 69 | setAnchorEl(null); 70 | setEditModalOpen(true); 71 | }; 72 | 73 | let code; 74 | try { 75 | code = authenticator.generate(props.account.secret); // eslint-disable-line 76 | // Insert a space in the middle of the code for better readability 77 | code = [code.slice(0, 3), ' ', code.slice(3)].join(''); 78 | } catch (error) { 79 | code = error.message; 80 | } 81 | 82 | return ( 83 | 84 |
85 | {props.account.icon && ( 86 |
87 | {props.account.icon} 92 |
93 | )} 94 |
95 | 96 | {props.account.name} 97 | 98 | 102 | {code} 103 | 104 |
105 |
106 | setAnchorEl(event.currentTarget)} 110 | > 111 | 112 | 113 | setAnchorEl(null)} 118 | > 119 | Edit 120 | Delete 121 | 122 | {props.remainingSeconds} 123 |
124 |
125 | 126 | setEditModalOpen(false)} 129 | accountIndex={props.index} 130 | account={props.account} 131 | /> 132 | 133 | setDeleteModalOpen(false)} 136 | accountIndex={props.index} 137 | account={props.account} 138 | /> 139 |
140 | ); 141 | }; 142 | -------------------------------------------------------------------------------- /src/components/AccountList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { authenticator } from '@otplib/preset-browser'; 3 | import { File } from '../utils/accounts'; 4 | import { AccountItem } from './AccountItem'; 5 | 6 | interface Props { 7 | file: File; 8 | } 9 | 10 | export const AccountList = (props: Props) => { 11 | const [seconds, setSeconds] = useState(0); 12 | 13 | useEffect(() => { 14 | const intervalId = setInterval(() => { 15 | setSeconds(authenticator.timeRemaining()); // eslint-disable-line 16 | }, 1000); 17 | 18 | return function cleanup() { 19 | clearInterval(intervalId); 20 | }; 21 | }, []); 22 | 23 | return ( 24 | 25 | {props.file.accounts.map((account, index) => ( 26 | 32 | ))} 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/AddAccount.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useContext } from 'react'; 2 | import { 3 | Dialog, 4 | Slide, 5 | AppBar, 6 | Toolbar, 7 | IconButton, 8 | Typography, 9 | Grid, 10 | Button, 11 | TextField, 12 | Theme, 13 | FormControl, 14 | InputLabel, 15 | Select, 16 | MenuItem, 17 | } from '@material-ui/core'; 18 | import { ArrowBack } from '@material-ui/icons'; 19 | import { makeStyles } from '@material-ui/styles'; 20 | import { Loader } from './Loader'; 21 | import { icons } from '../utils/icons'; 22 | import { FileContext } from '../context/FileContext'; 23 | 24 | const useStyles = makeStyles((theme: Theme) => ({ 25 | flex: { 26 | flex: 1, 27 | }, 28 | loadingContainer: { 29 | marginTop: 56 + theme.spacing(2), 30 | }, 31 | container: { 32 | marginTop: 56, 33 | }, 34 | formContainer: { 35 | marginLeft: theme.spacing(2), 36 | marginRight: theme.spacing(2), 37 | display: 'flex', 38 | flexDirection: 'column', 39 | }, 40 | formControlIcon: { 41 | marginTop: theme.spacing(2), 42 | marginBottom: theme.spacing(1), 43 | }, 44 | selectMenuIcon: { 45 | display: 'flex', 46 | alignItems: 'center', 47 | }, 48 | iconImage: { 49 | marginRight: theme.spacing(2), 50 | }, 51 | })); 52 | 53 | interface Props { 54 | open: boolean; 55 | onClose: () => void; 56 | } 57 | 58 | function Transition(props: any) { 59 | return ; 60 | } 61 | 62 | export const AddAccount = ({ open, onClose }: Props) => { 63 | const classes = useStyles(); 64 | 65 | const { addAccount } = useContext(FileContext); 66 | 67 | const [values, setValues] = useState({ 68 | name: '', 69 | secret: '', 70 | icon: '', 71 | }); 72 | const [errors, setErrors] = useState({ 73 | name: false, 74 | secret: false, 75 | }); 76 | const [loading, setLoading] = useState(false); 77 | 78 | const reset = () => { 79 | setValues({ 80 | name: '', 81 | secret: '', 82 | icon: '', 83 | }); 84 | setErrors({ name: false, secret: false }); 85 | setLoading(false); 86 | }; 87 | 88 | const handleChange = (name: string) => (event: any) => { 89 | setValues({ ...values, [name]: event.target.value }); 90 | }; 91 | 92 | const handleSubmit = async (e: React.FormEvent) => { 93 | e.preventDefault(); 94 | 95 | setErrors({ name: false, secret: false }); 96 | 97 | if (!values.name || values.name === '') { 98 | setErrors({ ...errors, name: true }); 99 | return; 100 | } 101 | if (!values.secret || values.secret === '') { 102 | setErrors({ ...errors, secret: true }); 103 | return; 104 | } 105 | try { 106 | setLoading(true); 107 | await addAccount(values); 108 | 109 | reset(); 110 | onClose(); 111 | } catch (error) { 112 | setLoading(false); 113 | alert(error.message); 114 | } 115 | }; 116 | 117 | return ( 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | Enter account details 126 | 127 | 130 | 131 | 132 | 133 | {loading && } 134 | 135 | {!loading && ( 136 | 137 | 138 |
144 | 152 | 153 | 161 | 162 | 163 | Icon 164 | 186 | 187 | 188 |
189 |
190 | )} 191 |
192 | ); 193 | }; 194 | -------------------------------------------------------------------------------- /src/components/AddAccountScan.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useContext } from 'react'; 2 | import { 3 | Dialog, 4 | Slide, 5 | AppBar, 6 | Toolbar, 7 | IconButton, 8 | Typography, 9 | Grid, 10 | Theme, 11 | } from '@material-ui/core'; 12 | import { ArrowBack } from '@material-ui/icons'; 13 | import { makeStyles } from '@material-ui/styles'; 14 | import QrReader from 'react-qr-reader'; 15 | import * as queryString from 'query-string'; 16 | import { Loader } from './Loader'; 17 | import { FileContext } from '../context/FileContext'; 18 | 19 | const useStyles = makeStyles((theme: Theme) => ({ 20 | flex: { 21 | flex: 1, 22 | }, 23 | loadingContainer: { 24 | marginTop: 56 + theme.spacing(2), 25 | }, 26 | errorContainer: { 27 | marginTop: 56 + theme.spacing(2), 28 | paddingLeft: theme.spacing(2), 29 | paddingRight: theme.spacing(2), 30 | }, 31 | container: { 32 | marginTop: 56, 33 | }, 34 | })); 35 | 36 | interface Props { 37 | open: boolean; 38 | onClose: () => void; 39 | } 40 | 41 | function Transition(props: any) { 42 | return ; 43 | } 44 | 45 | export const AddAccountScan = ({ open, onClose }: Props) => { 46 | const classes = useStyles(); 47 | 48 | const { addAccount } = useContext(FileContext); 49 | 50 | const [loading, setLoading] = useState(false); 51 | const [error, setError] = useState(null); 52 | 53 | const handleClose = () => { 54 | onClose(); 55 | setLoading(false); 56 | setError(null); 57 | }; 58 | 59 | const handleError = (error: any) => { 60 | console.error(error); 61 | setError(error.message); 62 | setLoading(false); 63 | }; 64 | 65 | const handleScan = async (scanned: string | null) => { 66 | if (scanned) { 67 | setLoading(true); 68 | const parsed = queryString.parseUrl(scanned); 69 | // Verify the url is valid 70 | if (!parsed.url.startsWith('otpauth://') && !parsed.query.secret) { 71 | alert('Invalid code'); 72 | console.error(parsed); 73 | setLoading(false); 74 | return; 75 | } 76 | 77 | try { 78 | const parsedUrl = parsed.url.split('/'); 79 | const accountName = parsedUrl[parsedUrl.length - 1]; 80 | 81 | await addAccount({ 82 | name: accountName, 83 | secret: parsed.query.secret as string, 84 | }); 85 | 86 | onClose(); 87 | } catch (error) { 88 | handleError(error); 89 | } 90 | } 91 | }; 92 | 93 | return ( 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | Scan qrcode 102 | 103 | 104 | 105 | 106 | {loading && } 107 | 108 | {!loading && error && ( 109 | 110 | 111 | Error: {error} 112 | Known issues: 113 | 114 | - On IOS 11 it is only supported on Safari and not on Chrome or 115 | Firefox due to Apple making the API not available to 3rd party 116 | browsers. 117 | 118 | 119 | 120 | )} 121 | 122 | {!loading && !error && ( 123 | 124 | 125 | 130 | 131 | 132 | )} 133 | 134 | ); 135 | }; 136 | -------------------------------------------------------------------------------- /src/components/DeleteAccount.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useContext } from 'react'; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | DialogContentText, 7 | DialogActions, 8 | Button, 9 | } from '@material-ui/core'; 10 | import { Account } from '../types'; 11 | import { Loader } from './Loader'; 12 | import { FileContext } from '../context/FileContext'; 13 | 14 | interface Props { 15 | open: boolean; 16 | onClose: () => void; 17 | account: Account; 18 | accountIndex: number; 19 | } 20 | 21 | export const DeleteAccount = ({ 22 | open, 23 | onClose, 24 | account, 25 | accountIndex, 26 | }: Props) => { 27 | const { removeAccount } = useContext(FileContext); 28 | 29 | const [loading, setLoading] = useState(false); 30 | 31 | const reset = () => { 32 | setLoading(false); 33 | }; 34 | 35 | const handleDelete = async () => { 36 | try { 37 | setLoading(true); 38 | await removeAccount(accountIndex); 39 | onClose(); 40 | reset(); 41 | } catch (error) { 42 | setLoading(false); 43 | alert(error.message); 44 | } 45 | }; 46 | 47 | return ( 48 | 49 | Delete {account.name}? 50 | {loading && } 51 | {!loading && ( 52 | 53 | 54 | Do you really want to delete {account.name}? 55 | 56 | 57 | )} 58 | 59 | 62 | 70 | 71 | 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/components/EditAccount.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useContext } from 'react'; 2 | import { 3 | Dialog, 4 | Slide, 5 | AppBar, 6 | Toolbar, 7 | IconButton, 8 | Typography, 9 | Grid, 10 | Button, 11 | TextField, 12 | Theme, 13 | FormControl, 14 | InputLabel, 15 | Select, 16 | MenuItem, 17 | } from '@material-ui/core'; 18 | import { ArrowBack } from '@material-ui/icons'; 19 | import { makeStyles } from '@material-ui/styles'; 20 | import { Account } from '../types'; 21 | import { Loader } from './Loader'; 22 | import { icons } from '../utils/icons'; 23 | import { FileContext } from '../context/FileContext'; 24 | 25 | const useStyles = makeStyles((theme: Theme) => ({ 26 | flex: { 27 | flex: 1, 28 | }, 29 | loadingContainer: { 30 | marginTop: 56 + theme.spacing(2), 31 | }, 32 | container: { 33 | marginTop: 56, 34 | }, 35 | formContainer: { 36 | marginLeft: theme.spacing(2), 37 | marginRight: theme.spacing(2), 38 | display: 'flex', 39 | flexDirection: 'column', 40 | }, 41 | formControlIcon: { 42 | marginTop: theme.spacing(2), 43 | marginBottom: theme.spacing(1), 44 | }, 45 | selectMenuIcon: { 46 | display: 'flex', 47 | alignItems: 'center', 48 | }, 49 | iconImage: { 50 | marginRight: theme.spacing(2), 51 | }, 52 | })); 53 | 54 | interface Props { 55 | open: boolean; 56 | onClose: () => void; 57 | account: Account; 58 | accountIndex: number; 59 | } 60 | 61 | function Transition(props: any) { 62 | return ; 63 | } 64 | 65 | export const EditAccount = ({ 66 | open, 67 | onClose, 68 | accountIndex, 69 | account, 70 | }: Props) => { 71 | const classes = useStyles(); 72 | 73 | const { editAccount } = useContext(FileContext); 74 | 75 | const [values, setValues] = useState({ 76 | name: account.name, 77 | icon: account.icon, 78 | }); 79 | const [errors, setErrors] = useState({ 80 | name: false, 81 | }); 82 | const [loading, setLoading] = useState(false); 83 | 84 | const reset = () => { 85 | setErrors({ name: false }); 86 | setLoading(false); 87 | }; 88 | 89 | const handleChange = (name: string) => (event: any) => { 90 | setValues({ ...values, [name]: event.target.value }); 91 | }; 92 | 93 | const handleSubmit = async (e: React.FormEvent) => { 94 | e.preventDefault(); 95 | 96 | setErrors({ name: false }); 97 | 98 | if (!values.name || values.name === '') { 99 | setErrors({ ...errors, name: true }); 100 | return; 101 | } 102 | 103 | try { 104 | setLoading(true); 105 | await editAccount(accountIndex, { ...account, ...values }); 106 | 107 | reset(); 108 | onClose(); 109 | } catch (error) { 110 | setLoading(false); 111 | alert(error.message); 112 | } 113 | }; 114 | 115 | return ( 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | Edit account details 124 | 125 | 128 | 129 | 130 | 131 | {loading && } 132 | 133 | {!loading && ( 134 | 135 | 136 |
142 | 150 | 151 | 152 | Icon 153 | 175 | 176 | 177 |
178 |
179 | )} 180 |
181 | ); 182 | }; 183 | -------------------------------------------------------------------------------- /src/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react'; 2 | import { 3 | AppBar, 4 | Toolbar, 5 | Typography, 6 | CircularProgress, 7 | Grid, 8 | IconButton, 9 | Menu, 10 | MenuItem, 11 | Theme, 12 | Link, 13 | } from '@material-ui/core'; 14 | import { SpeedDial, SpeedDialAction, SpeedDialIcon } from '@material-ui/lab'; 15 | import { makeStyles } from '@material-ui/styles'; 16 | import { MoreVert, Keyboard, PhotoCamera } from '@material-ui/icons'; 17 | import { AccountList } from './AccountList'; 18 | import { AddAccount } from './AddAccount'; 19 | import { AddAccountScan } from './AddAccountScan'; 20 | import { ThemeContext } from '../context/ThemeContext'; 21 | import { FileContext } from '../context/FileContext'; 22 | import { userSession } from '../utils/blockstack'; 23 | 24 | const useStyles = makeStyles((theme: Theme) => ({ 25 | flex: { 26 | flex: 1, 27 | }, 28 | fab: { 29 | position: 'fixed', 30 | bottom: theme.spacing(2), 31 | right: theme.spacing(2), 32 | }, 33 | container: { 34 | marginTop: 56, 35 | }, 36 | loadingContainer: { 37 | display: 'flex', 38 | justifyContent: 'center', 39 | marginTop: theme.spacing(2), 40 | }, 41 | emptyContainer: { 42 | display: 'flex', 43 | justifyContent: 'center', 44 | marginTop: theme.spacing(2), 45 | flexDirection: 'column', 46 | }, 47 | emptyImage: { 48 | height: 130, 49 | maxWidth: '100%', 50 | marginTop: theme.spacing(2), 51 | marginBottom: theme.spacing(2), 52 | }, 53 | links: { 54 | marginTop: theme.spacing(1), 55 | marginBottom: theme.spacing(1), 56 | }, 57 | linksDivider: { 58 | marginLeft: theme.spacing(1), 59 | marginRight: theme.spacing(1), 60 | }, 61 | })); 62 | 63 | interface Props { 64 | setTheme: (theme: 'light' | 'dark') => void; 65 | } 66 | 67 | export const Home = ({ setTheme }: Props) => { 68 | const classes = useStyles(); 69 | 70 | const theme = useContext(ThemeContext); 71 | const { file } = useContext(FileContext); 72 | 73 | const [speedDialOpen, setSpeedDialOpen] = useState(false); 74 | const [addAccountScanOpen, setAddAccountScanOpen] = useState(false); 75 | const [addAccountModalOpen, setAddAccountModalOpen] = useState(false); 76 | const [anchorEl, setAnchorEl] = useState(null); 77 | const menuOpen = Boolean(anchorEl); 78 | 79 | const handleSpeedDialClick = () => { 80 | setSpeedDialOpen(!speedDialOpen); 81 | }; 82 | 83 | const handleSpeedDialClose = () => { 84 | setSpeedDialOpen(false); 85 | }; 86 | 87 | const handleSelectCamera = () => { 88 | handleSpeedDialClose(); 89 | setAddAccountScanOpen(true); 90 | }; 91 | 92 | const handleSelectManual = () => { 93 | handleSpeedDialClose(); 94 | setAddAccountModalOpen(true); 95 | }; 96 | 97 | const handleSelectLightTheme = () => { 98 | setTheme('light'); 99 | setAnchorEl(null); 100 | }; 101 | 102 | const handleSelectDarkTheme = () => { 103 | setTheme('dark'); 104 | setAnchorEl(null); 105 | }; 106 | 107 | const handleLogout = () => { 108 | userSession.signUserOut(window.location.origin); 109 | }; 110 | 111 | return ( 112 | 113 | 114 | 115 | 116 | Twoblocks 117 | 118 | 119 | setAnchorEl(event.currentTarget)} 123 | color="inherit" 124 | > 125 | 126 | 127 | setAnchorEl(null)} 132 | > 133 | {theme === 'dark' && ( 134 | Light theme 135 | )} 136 | {theme === 'light' && ( 137 | Dark theme 138 | )} 139 | Logout 140 | 141 | 142 | 143 | 144 | 145 | {!file && ( 146 | 147 | 148 | 149 | )} 150 | 151 | {file && file.accounts.length === 0 && ( 152 | 153 | authentication illustration 158 | 159 | Empty account list 160 | 161 | 162 | Use the + button to add a new account 163 | 164 | 165 | )} 166 | 167 | {file && ( 168 | 169 | 170 | 171 | )} 172 | 173 | 179 | Twitter 180 | | 181 | Github 182 | | 183 | 186 | {process.env.NODE_ENV === 'development' 187 | ? 'Development' 188 | : `${ 189 | process.env.REACT_APP_COMMIT_REF && 190 | process.env.REACT_APP_COMMIT_REF.substring(0, 6) 191 | }...`} 192 | 193 | 194 | 195 | 196 | 197 | setAddAccountModalOpen(false)} 200 | /> 201 | 202 | setAddAccountScanOpen(false)} 205 | /> 206 | 207 | {file && ( 208 | } 212 | open={speedDialOpen} 213 | onClick={handleSpeedDialClick} 214 | onClose={handleSpeedDialClose} 215 | > 216 | } 218 | tooltipTitle="Scan a barcode" 219 | tooltipOpen 220 | onClick={handleSelectCamera} 221 | /> 222 | } 224 | tooltipTitle="Enter manually" 225 | tooltipOpen 226 | onClick={handleSelectManual} 227 | /> 228 | 229 | )} 230 | 231 | ); 232 | }; 233 | -------------------------------------------------------------------------------- /src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, CircularProgress, Theme } from '@material-ui/core'; 2 | import { makeStyles } from '@material-ui/styles'; 3 | 4 | const useStyles = makeStyles((theme: Theme) => ({ 5 | container: { 6 | display: 'flex', 7 | justifyContent: 'center', 8 | marginTop: theme.spacing(2), 9 | marginBottom: theme.spacing(2), 10 | }, 11 | })); 12 | 13 | interface Props { 14 | className?: string; 15 | } 16 | 17 | export const Loader = ({ className }: Props) => { 18 | const classes = useStyles(); 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/Login.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Theme, Typography, Link, Button } from '@material-ui/core'; 3 | import { makeStyles } from '@material-ui/styles'; 4 | 5 | const useStyles = makeStyles((theme: Theme) => ({ 6 | hero: { 7 | maxWidth: '760px', 8 | margin: '0 auto', 9 | marginTop: theme.spacing(8), 10 | marginBottom: theme.spacing(4), 11 | }, 12 | container: { 13 | marginLeft: theme.spacing(4), 14 | marginRight: theme.spacing(4), 15 | [theme.breakpoints.up('sm')]: { 16 | display: 'flex', 17 | flexDirection: 'row', 18 | }, 19 | }, 20 | brand: { 21 | display: 'flex', 22 | flexDirection: 'column', 23 | justifyContent: 'center', 24 | [theme.breakpoints.up('sm')]: { 25 | flex: 1.2, 26 | paddingRight: theme.spacing(8), 27 | paddingBottom: theme.spacing(10), 28 | }, 29 | }, 30 | brandContent: { 31 | marginBottom: theme.spacing(2), 32 | }, 33 | screen: { 34 | flex: 1, 35 | marginTop: theme.spacing(4), 36 | [theme.breakpoints.up('sm')]: { 37 | marginTop: 0, 38 | }, 39 | }, 40 | screenImage: { 41 | width: '100%', 42 | maxWidth: '100%', 43 | }, 44 | })); 45 | 46 | interface LoginProps { 47 | onLogin: () => void; 48 | } 49 | 50 | export const Login = ({ onLogin }: LoginProps) => { 51 | const classes = useStyles(); 52 | 53 | return ( 54 | 55 |
56 |
57 |
58 | 59 | Twoblocks 60 | 61 | 62 | Free and{' '} 63 | 68 | open source 69 | {' '} 70 | 2fa manager built with{' '} 71 | 76 | Stacks 77 | 78 | . Protect yourself online! 79 | 80 | 83 |
84 |
85 | Screenshot of the app 90 |
91 |
92 |
93 |
94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | fathomSiteId: process.env.REACT_APP_FATHOM_SITE_ID, 3 | sentryDsn: process.env.REACT_APP_SENTRY_DSN, 4 | }; 5 | -------------------------------------------------------------------------------- /src/context/FileContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useReducer, useEffect } from 'react'; 2 | import * as Fathom from 'fathom-client'; 3 | import { File, getFile, putFile } from '../utils/accounts'; 4 | import { Account } from '../types'; 5 | import { Goals } from '../utils/fathom'; 6 | 7 | export const FileContext = createContext<{ 8 | file?: File; 9 | loading?: boolean; 10 | setFile(file: File): void; 11 | addAccount(account: Account): Promise; 12 | editAccount(index: number, account: Account): Promise; 13 | removeAccount(index: number): Promise; 14 | }>(undefined as any); 15 | 16 | interface Props { 17 | children: React.ReactNode; 18 | } 19 | 20 | type Action = 21 | | { type: 'success'; file?: File } 22 | | { type: 'error'; error: string }; 23 | 24 | interface State { 25 | file?: File; 26 | loading?: boolean; 27 | error?: string; 28 | } 29 | 30 | const initialState: State = { 31 | file: undefined, 32 | loading: true, 33 | error: undefined, 34 | }; 35 | 36 | const reducer = (state: State, action: Action): State => { 37 | switch (action.type) { 38 | case 'success': 39 | return { 40 | ...state, 41 | loading: false, 42 | file: action.file, 43 | }; 44 | case 'error': 45 | return { ...state, loading: false, error: action.error }; 46 | default: 47 | throw new Error(); 48 | } 49 | }; 50 | 51 | export const FileContextProvider = ({ children }: Props) => { 52 | const [state, dispatch] = useReducer(reducer, initialState); 53 | 54 | const fetchFile = async () => { 55 | try { 56 | const file = await getFile(); 57 | dispatch({ type: 'success', file }); 58 | } catch (error) { 59 | dispatch({ type: 'error', error: error.message }); 60 | alert(error.message); 61 | } 62 | }; 63 | 64 | const setFile = (file: File) => { 65 | dispatch({ type: 'success', file }); 66 | }; 67 | 68 | /** 69 | * Add an account to the file and save it 70 | */ 71 | const addAccount = async (account: Account) => { 72 | if (!state.file) return; 73 | Fathom.trackGoal(Goals.ADD_NEW_ACCOUNT, 0); 74 | const file = state.file; 75 | file.accounts.push(account); 76 | await putFile(file); 77 | dispatch({ type: 'success', file }); 78 | }; 79 | 80 | /** 81 | * Edit an account and save it 82 | */ 83 | const editAccount = async (index: number, account: Account) => { 84 | if (!state.file) return; 85 | const file = state.file; 86 | file.accounts[index] = account; 87 | await putFile(file); 88 | dispatch({ type: 'success', file }); 89 | }; 90 | 91 | /** 92 | * Remove an account at a given index and save it 93 | */ 94 | const removeAccount = async (index: number) => { 95 | if (!state.file) return; 96 | const file = state.file; 97 | file.accounts.splice(index, 1); 98 | await putFile(file); 99 | dispatch({ type: 'success', file }); 100 | }; 101 | 102 | useEffect(() => { 103 | fetchFile(); 104 | }, []); 105 | 106 | return ( 107 | 117 | {children} 118 | 119 | ); 120 | }; 121 | -------------------------------------------------------------------------------- /src/context/ThemeContext.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const themeStorageKey = 'theme'; 4 | 5 | export const ThemeContext = React.createContext<'light' | 'dark'>('light'); 6 | -------------------------------------------------------------------------------- /src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@otplib/preset-browser'; 2 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from 'next/app'; 2 | import Head from 'next/head'; 3 | 4 | function MyApp({ Component, pageProps }: AppProps) { 5 | return ( 6 | <> 7 | 8 | 12 | 13 | Twoblocks 14 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | export default MyApp; 25 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import type { NextPage } from 'next'; 3 | import dynamic from 'next/dynamic'; 4 | import Script from 'next/script'; 5 | import * as Sentry from '@sentry/react'; 6 | import 'typeface-roboto'; 7 | import { config } from '../config'; 8 | 9 | Sentry.init({ 10 | dsn: config.sentryDsn, 11 | }); 12 | 13 | const DynamicComponent = dynamic(() => import('../App'), { ssr: false }); 14 | 15 | const Home: NextPage = () => { 16 | const [mounted, setMounted] = useState(false); 17 | 18 | useEffect(() => { 19 | setMounted(true); 20 | }, []); 21 | 22 | if (!mounted) { 23 | return null; 24 | } 25 | 26 | return ( 27 | <> 28 |