Account improvements (#302)

* begin account migration

* prep for styling

* improve multi-network support

allow deprecated window.web3 providers

* kill old modal

* remove tests

* clean up css

* Style account modal

* kill scss

* bug fixes

* use suspense for code-splitting and i18n

move abis

add ENS support to Web3Status

* Account modal mobile styles

* style tweaks

* finalize migration

* fix account styling

* fix ethereum svg

* fixes

* Split wallet modal into components

* refactor wallet modal and add connect button styles

* removing tap highlights

fix spacing on some mobile browsers

* Styling fixes for account and warning

* clean up injected connector logic

* remove console

* add wrong network copy

* restore border radius

change wallet logo
This commit is contained in:
Noah Zinsmeister 2019-05-24 16:53:01 -04:00 committed by GitHub
parent 6541f3582f
commit 6579e17f7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 2907 additions and 5050 deletions

@ -1,3 +1,2 @@
REACT_APP_NETWORK_ID="1"
REACT_APP_NETWORK_URL=""
REACT_APP_NETWORK_NAME="Main Ethereum Network"

@ -11,4 +11,3 @@ install: yarn
script:
- yarn check:all
- yarn build
- yarn test

@ -24,17 +24,15 @@ yarn
### Configure Environment
Rename `.env.example` to `.env` and fill in the appropriate variables.
Rename `.env.local.example` to `.env.local` and fill in the appropriate variables.
### Run
```bash
yarn start
# or
yarn start:rinkeby
```
More robust support for other testnets is in the works!
To run on a testnet, simply update the `.env.local` file appropriately.
## Contributions

@ -5,32 +5,35 @@
"homepage": ".",
"private": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.18",
"@fortawesome/free-brands-svg-icons": "^5.8.2",
"@fortawesome/free-regular-svg-icons": "^5.8.2",
"@fortawesome/free-solid-svg-icons": "^5.8.2",
"@fortawesome/react-fontawesome": "^0.1.4",
"@reach/dialog": "^0.2.8",
"@reach/tooltip": "^0.2.0",
"classnames": "^2.2.6",
"copy-to-clipboard": "^3.2.0",
"escape-string-regexp": "^2.0.0",
"ethers": "^4.0.27",
"i18next": "^15.0.9",
"i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^2.0.1",
"jazzicon": "^1.5.0",
"node-sass": "^4.11.0",
"polished": "^3.3.2",
"react": "^16.8.6",
"react-aria-modal": "^4.0.0",
"react-device-detect": "^1.6.2",
"react-dom": "^16.8.6",
"react-ga": "^2.5.7",
"react-i18next": "^10.7.0",
"react-router-dom": "^5.0.0",
"react-scripts": "^3.0.1",
"react-transition-group": "1.x",
"react-spring": "^8.0.20",
"styled-components": "^4.2.0",
"ua-parser-js": "^0.7.18",
"web3-react": "^5.0.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"start:rinkeby": "REACT_APP_NETWORK_ID=4 REACT_APP_NETWORK_NAME='Rinkeby Test Network' yarn start",
"build:rinkeby": "REACT_APP_NETWORK_ID=4 REACT_APP_NETWORK_NAME='Rinkeby Test Network' yarn build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"lint:base": "yarn eslint './src/**/*.{js,jsx}'",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 31 KiB

@ -1,9 +1,9 @@
<!DOCTYPE html>
<!DOCTYPE html />
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is installed on a
@ -23,8 +23,7 @@
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="modal-root"></div>
<div id="root" />
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.

@ -1,67 +0,0 @@
(function(){
'use strict';var aa="function"==typeof Object.defineProperties?Object.defineProperty:function(d,c,a){d!=Array.prototype&&d!=Object.prototype&&(d[c]=a.value)},e="undefined"!=typeof window&&window===this?this:"undefined"!=typeof global&&null!=global?global:this;function ba(){ba=function(){};e.Symbol||(e.Symbol=da)}var da=function(){var d=0;return function(c){return"jscomp_symbol_"+(c||"")+d++}}();
function sa(){ba();var d=e.Symbol.iterator;d||(d=e.Symbol.iterator=e.Symbol("iterator"));"function"!=typeof Array.prototype[d]&&aa(Array.prototype,d,{configurable:!0,writable:!0,value:function(){return ta(this)}});sa=function(){}}function ta(d){var c=0;return ua(function(){return c<d.length?{done:!1,value:d[c++]}:{done:!0}})}function ua(d){sa();d={next:d};d[e.Symbol.iterator]=function(){return this};return d}function va(d){sa();ba();sa();var c=d[Symbol.iterator];return c?c.call(d):ta(d)}
function Oa(d,c){if(c){var a=e;d=d.split(".");for(var b=0;b<d.length-1;b++){var g=d[b];g in a||(a[g]={});a=a[g]}d=d[d.length-1];b=a[d];c=c(b);c!=b&&null!=c&&aa(a,d,{configurable:!0,writable:!0,value:c})}}
Oa("String.prototype.startsWith",function(d){return d?d:function(c,a){if(null==this)throw new TypeError("The 'this' value for String.prototype.startsWith must not be null or undefined");if(c instanceof RegExp)throw new TypeError("First argument to String.prototype.startsWith must not be a regular expression");var b=this+"";c+="";var g=b.length,d=c.length;a=Math.max(0,Math.min(a|0,b.length));for(var h=0;h<d&&a<g;)if(b[a++]!=c[h++])return!1;return h>=d}});
Oa("Array.prototype.find",function(d){return d?d:function(c,a){a:{var b=this;b instanceof String&&(b=String(b));for(var g=b.length,d=0;d<g;d++){var h=b[d];if(c.call(a,h,d,b)){c=h;break a}}c=void 0}return c}});
self.onmessage=function(d){var c=d.data.data;switch(d.data.type){case "setDebug":Pa=c;break;case "decode":var a=null;try{Qa=c;f=c.width;n=c.height;var b=Qa.data,g=0,k=new Uint8ClampedArray(b.buffer,g,f*n);g+=f*n;var h=new Uint8ClampedArray(b.buffer,g,f*n);g+=f*n;var l=va(Ra(f,n));l.next();for(var m=l.next().value,p=l.next().value,w=new Uint8ClampedArray(b.buffer,g,m*p),q=f,u=n,t=0;t<u;t++)for(var x=0;x<q;x++){var v=t*q+x,ca=4*v;k[v]=Sa.red*b[ca]+Sa.blue*b[ca+1]+Sa.green*b[ca+2]+128>>8}var ea=h,Q=
w,R=f,E=n;ea=void 0===ea?k:ea;Q=void 0===Q?null:Q;var S=va(Ra(R,E)),z=S.next().value,C=S.next().value,X=S.next().value;if(Q){if(!(Q instanceof Uint8ClampedArray)||Q.byteLength!==C*X)throw Error("QR Error: Illegal Buffer.");var Y=Q}else Y=new Uint8ClampedArray(C*X);for(var U=0;U<X;++U)for(var V=0;V<C;++V){for(var M=255,Z=0,Va=Math.min(U*z,E-z)*R+Math.min(V*z,R-z),Wa=0;Wa<z;++Wa){for(var wa=0;wa<z;++wa){var fa=k[Va+wa];fa<M&&(M=fa);fa>Z&&(Z=fa)}Va+=R}if(Z-M>Ta){var Xa=(M+Z)/2;var xa=Math.min(255,Xa+
(M+Z)/4,1.1*Xa)}else if(0===V||0===U)xa=M-1;else{var ya=U*C+V,Ya=(Y[ya-1]+Y[ya-C]+C[ya-C-1])/3;xa=Ya>M?Ya:M-1}Y[U*C+V]=xa}for(var ha=0;ha<X;++ha)for(var ia=0;ia<C;++ia){for(var Za=0,za=-2;2>=za;++za)for(var Aa=-2;2>=Aa;++Aa)Za+=Y[Math.max(0,Math.min(X-1,ha+Aa))*C+Math.max(0,Math.min(C-1,ia+za))];var $a=k,Ba=R,Kb=Za/25,ja=ea;ja=void 0===ja?$a:ja;for(var ab=Math.min(ha*z,E-z)*Ba+Math.min(ia*z,Ba-z),bb=0;bb<z;++bb){for(var Ca=0;Ca<z;++Ca){var cb=ab+Ca;ja[cb]=$a[cb]<=Kb}ab+=Ba}}if(Pa){var F=new ImageData(new Uint8ClampedArray(f*
n*4),f,n);for(var G=0;G<n;G++)for(var H=0;H<f;H++){var N=4*H+G*f*4,Da=h[G*f+H]?0:255;F.data[N]=Da;F.data[N+1]=Da;F.data[N+2]=Da;F.data[N+3]=255}}try{var ka=(new Ua(h)).Oa();if(Pa)for(G=0;G<ka.i.height;G++)for(H=0;H<ka.i.width;H++){N=8*H+2*G*f*4;var db=ka.i.ga(H,G);F.data[N]=db?0:255;F.data[N+1]=db?0:255;F.data[N+2]=255}}finally{Pa&&self.postMessage({type:"debugImage",data:F},[F.data.buffer])}var Ea=new tb(ka.i),Fa=Ea.Ba(),eb=Ea.Aa().Pa,la=Ea.jb();if(la.length!=Fa.na)throw Error("QR Error: ArgumentException");
for(var Ga=Fa.Za(eb),fb=0,ma=Ga.da,B=0;B<ma.length;B++)fb+=ma[B].count;for(var D=Array(fb),na=0,A=0;A<ma.length;A++){var gb=ma[A];for(B=0;B<gb.count;B++){var hb=gb.ra,Nb=Ga.ka+hb;D[na++]=new ub(hb,Array(Nb))}}for(var ib=D[0].T.length,W=D.length-1;0<=W&&D[W].T.length!=ib;)W--;W++;var Ha=ib-Ga.ka,Ia=0;for(B=0;B<Ha;B++)for(A=0;A<na;A++)D[A].T[B]=la[Ia++];for(A=W;A<na;A++)D[A].T[Ha]=la[Ia++];var Pb=D[0].T.length;for(B=Ha;B<Pb;B++)for(A=0;A<na;A++)D[A].T[A<W?B:B+1]=la[Ia++];for(var jb=0,T=0;T<D.length;T++)jb+=
D[T].ya;for(var kb=Array(jb),Qb=0,Ja=0;Ja<D.length;Ja++){for(var lb=D[Ja],mb=lb.T,nb=lb.ya,oa=mb,ob=nb,pb=oa.length,Ka=Array(pb),I=0;I<pb;I++)Ka[I]=oa[I]&255;var Rb=oa.length-ob;try{vb.decode(Ka,Rb)}catch(J){throw J;}for(I=0;I<ob;I++)oa[I]=Ka[I];for(T=0;T<nb;T++)kb[Qb++]=mb[T]}for(var La=(new wb(kb,Fa.oa,eb.i)).Xa(),qb="",pa=0;pa<La.length;pa++)for(var Ma=0;Ma<La[pa].length;Ma++)qb+=String.fromCharCode(La[pa][Ma]);var qa=qb;try{new URL(qa);var rb=!0}catch(J){rb=!1}if(rb){var ra="";try{ra=escape(qa)}catch(J){console.log(J),
ra=qa}var Na="";try{Na=decodeURIComponent(ra)}catch(J){console.log(J),Na=ra}var sb=Na}else sb=qa;a=xb=sb}catch(J){if(!J.message.startsWith("QR Error"))throw J;}finally{self.postMessage({type:"qrResult",data:a})}break;case "grayscaleWeights":if(256!==c.red+c.green+c.blue)throw Error("Weights have to sum up to 256");Sa=c}};function Ra(d,c){var a=Math.max(Math.floor(Math.min(d,c)/yb),zb);return[a,Math.ceil(d/a),Math.ceil(c/a)]}var yb=40,zb=16,Ta=12;function r(d,c){this.count=d;this.ra=c}function y(d,c,a){this.ka=d;this.da=a?[c,a]:Array(c)}
function K(d,c,a,b,g,k){this.oa=d;this.ca=c;this.da=[a,b,g,k];d=0;c=a.ka;a=a.da;for(b=0;b<a.length;b++)g=a[b],d+=g.count*(g.ra+c);this.na=d;this.fa=function(){return 17+4*this.oa};this.Ia=function(){var b=this.fa(),a=new Ab(b);a.P(0,0,9,9);a.P(b-8,0,8,9);a.P(0,b-8,9,8);for(var g=this.ca.length,c=0;c<g;c++)for(var d=this.ca[c]-2,k=0;k<g;k++)0==c&&(0==k||k==g-1)||c==g-1&&0==k||a.P(this.ca[k]-2,d,5,5);a.P(6,9,1,b-17);a.P(9,6,b-17,1);6<this.oa&&(a.P(b-11,0,3,6),a.P(0,b-11,6,3));return a};this.Za=function(b){return this.da[b.gb]}}
var Bb=[31892,34236,39577,42195,48118,51042,55367,58893,63784,68472,70749,76311,79154,84390,87683,92361,96236,102084,102881,110507,110734,117786,119615,126325,127568,133589,136944,141498,145311,150283,152622,158308,161089,167017],Cb=[new K(1,[],new y(7,new r(1,19)),new y(10,new r(1,16)),new y(13,new r(1,13)),new y(17,new r(1,9))),new K(2,[6,18],new y(10,new r(1,34)),new y(16,new r(1,28)),new y(22,new r(1,22)),new y(28,new r(1,16))),new K(3,[6,22],new y(15,new r(1,55)),new y(26,new r(1,44)),new y(18,
new r(2,17)),new y(22,new r(2,13))),new K(4,[6,26],new y(20,new r(1,80)),new y(18,new r(2,32)),new y(26,new r(2,24)),new y(16,new r(4,9))),new K(5,[6,30],new y(26,new r(1,108)),new y(24,new r(2,43)),new y(18,new r(2,15),new r(2,16)),new y(22,new r(2,11),new r(2,12))),new K(6,[6,34],new y(18,new r(2,68)),new y(16,new r(4,27)),new y(24,new r(4,19)),new y(28,new r(4,15))),new K(7,[6,22,38],new y(20,new r(2,78)),new y(18,new r(4,31)),new y(18,new r(2,14),new r(4,15)),new y(26,new r(4,13),new r(1,14))),
new K(8,[6,24,42],new y(24,new r(2,97)),new y(22,new r(2,38),new r(2,39)),new y(22,new r(4,18),new r(2,19)),new y(26,new r(4,14),new r(2,15))),new K(9,[6,26,46],new y(30,new r(2,116)),new y(22,new r(3,36),new r(2,37)),new y(20,new r(4,16),new r(4,17)),new y(24,new r(4,12),new r(4,13))),new K(10,[6,28,50],new y(18,new r(2,68),new r(2,69)),new y(26,new r(4,43),new r(1,44)),new y(24,new r(6,19),new r(2,20)),new y(28,new r(6,15),new r(2,16))),new K(11,[6,30,54],new y(20,new r(4,81)),new y(30,new r(1,
50),new r(4,51)),new y(28,new r(4,22),new r(4,23)),new y(24,new r(3,12),new r(8,13))),new K(12,[6,32,58],new y(24,new r(2,92),new r(2,93)),new y(22,new r(6,36),new r(2,37)),new y(26,new r(4,20),new r(6,21)),new y(28,new r(7,14),new r(4,15))),new K(13,[6,34,62],new y(26,new r(4,107)),new y(22,new r(8,37),new r(1,38)),new y(24,new r(8,20),new r(4,21)),new y(22,new r(12,11),new r(4,12))),new K(14,[6,26,46,66],new y(30,new r(3,115),new r(1,116)),new y(24,new r(4,40),new r(5,41)),new y(20,new r(11,16),
new r(5,17)),new y(24,new r(11,12),new r(5,13))),new K(15,[6,26,48,70],new y(22,new r(5,87),new r(1,88)),new y(24,new r(5,41),new r(5,42)),new y(30,new r(5,24),new r(7,25)),new y(24,new r(11,12),new r(7,13))),new K(16,[6,26,50,74],new y(24,new r(5,98),new r(1,99)),new y(28,new r(7,45),new r(3,46)),new y(24,new r(15,19),new r(2,20)),new y(30,new r(3,15),new r(13,16))),new K(17,[6,30,54,78],new y(28,new r(1,107),new r(5,108)),new y(28,new r(10,46),new r(1,47)),new y(28,new r(1,22),new r(15,23)),new y(28,
new r(2,14),new r(17,15))),new K(18,[6,30,56,82],new y(30,new r(5,120),new r(1,121)),new y(26,new r(9,43),new r(4,44)),new y(28,new r(17,22),new r(1,23)),new y(28,new r(2,14),new r(19,15))),new K(19,[6,30,58,86],new y(28,new r(3,113),new r(4,114)),new y(26,new r(3,44),new r(11,45)),new y(26,new r(17,21),new r(4,22)),new y(26,new r(9,13),new r(16,14))),new K(20,[6,34,62,90],new y(28,new r(3,107),new r(5,108)),new y(26,new r(3,41),new r(13,42)),new y(30,new r(15,24),new r(5,25)),new y(28,new r(15,15),
new r(10,16))),new K(21,[6,28,50,72,94],new y(28,new r(4,116),new r(4,117)),new y(26,new r(17,42)),new y(28,new r(17,22),new r(6,23)),new y(30,new r(19,16),new r(6,17))),new K(22,[6,26,50,74,98],new y(28,new r(2,111),new r(7,112)),new y(28,new r(17,46)),new y(30,new r(7,24),new r(16,25)),new y(24,new r(34,13))),new K(23,[6,30,54,74,102],new y(30,new r(4,121),new r(5,122)),new y(28,new r(4,47),new r(14,48)),new y(30,new r(11,24),new r(14,25)),new y(30,new r(16,15),new r(14,16))),new K(24,[6,28,54,
80,106],new y(30,new r(6,117),new r(4,118)),new y(28,new r(6,45),new r(14,46)),new y(30,new r(11,24),new r(16,25)),new y(30,new r(30,16),new r(2,17))),new K(25,[6,32,58,84,110],new y(26,new r(8,106),new r(4,107)),new y(28,new r(8,47),new r(13,48)),new y(30,new r(7,24),new r(22,25)),new y(30,new r(22,15),new r(13,16))),new K(26,[6,30,58,86,114],new y(28,new r(10,114),new r(2,115)),new y(28,new r(19,46),new r(4,47)),new y(28,new r(28,22),new r(6,23)),new y(30,new r(33,16),new r(4,17))),new K(27,[6,
34,62,90,118],new y(30,new r(8,122),new r(4,123)),new y(28,new r(22,45),new r(3,46)),new y(30,new r(8,23),new r(26,24)),new y(30,new r(12,15),new r(28,16))),new K(28,[6,26,50,74,98,122],new y(30,new r(3,117),new r(10,118)),new y(28,new r(3,45),new r(23,46)),new y(30,new r(4,24),new r(31,25)),new y(30,new r(11,15),new r(31,16))),new K(29,[6,30,54,78,102,126],new y(30,new r(7,116),new r(7,117)),new y(28,new r(21,45),new r(7,46)),new y(30,new r(1,23),new r(37,24)),new y(30,new r(19,15),new r(26,16))),
new K(30,[6,26,52,78,104,130],new y(30,new r(5,115),new r(10,116)),new y(28,new r(19,47),new r(10,48)),new y(30,new r(15,24),new r(25,25)),new y(30,new r(23,15),new r(25,16))),new K(31,[6,30,56,82,108,134],new y(30,new r(13,115),new r(3,116)),new y(28,new r(2,46),new r(29,47)),new y(30,new r(42,24),new r(1,25)),new y(30,new r(23,15),new r(28,16))),new K(32,[6,34,60,86,112,138],new y(30,new r(17,115)),new y(28,new r(10,46),new r(23,47)),new y(30,new r(10,24),new r(35,25)),new y(30,new r(19,15),new r(35,
16))),new K(33,[6,30,58,86,114,142],new y(30,new r(17,115),new r(1,116)),new y(28,new r(14,46),new r(21,47)),new y(30,new r(29,24),new r(19,25)),new y(30,new r(11,15),new r(46,16))),new K(34,[6,34,62,90,118,146],new y(30,new r(13,115),new r(6,116)),new y(28,new r(14,46),new r(23,47)),new y(30,new r(44,24),new r(7,25)),new y(30,new r(59,16),new r(1,17))),new K(35,[6,30,54,78,102,126,150],new y(30,new r(12,121),new r(7,122)),new y(28,new r(12,47),new r(26,48)),new y(30,new r(39,24),new r(14,25)),new y(30,
new r(22,15),new r(41,16))),new K(36,[6,24,50,76,102,128,154],new y(30,new r(6,121),new r(14,122)),new y(28,new r(6,47),new r(34,48)),new y(30,new r(46,24),new r(10,25)),new y(30,new r(2,15),new r(64,16))),new K(37,[6,28,54,80,106,132,158],new y(30,new r(17,122),new r(4,123)),new y(28,new r(29,46),new r(14,47)),new y(30,new r(49,24),new r(10,25)),new y(30,new r(24,15),new r(46,16))),new K(38,[6,32,58,84,110,136,162],new y(30,new r(4,122),new r(18,123)),new y(28,new r(13,46),new r(32,47)),new y(30,
new r(48,24),new r(14,25)),new y(30,new r(42,15),new r(32,16))),new K(39,[6,26,54,82,110,138,166],new y(30,new r(20,117),new r(4,118)),new y(28,new r(40,47),new r(7,48)),new y(30,new r(43,24),new r(22,25)),new y(30,new r(10,15),new r(67,16))),new K(40,[6,30,58,86,114,142,170],new y(30,new r(19,118),new r(6,119)),new y(28,new r(18,47),new r(31,48)),new y(30,new r(34,24),new r(34,25)),new y(30,new r(20,15),new r(61,16)))];
function Db(d){if(1>d||40<d)throw Error("QR Error: ArgumentException");return Cb[d-1]}function Eb(d){for(var c=4294967295,a=0,b=0;b<Bb.length;b++){var g=Bb[b];if(g==d)return Db(b+7);g=Fb(d,g);g<c&&(a=b+7,c=g)}return 3>=c?Db(a):null};function Gb(d,c,a,b,g,k,h,l,m){this.o=d;this.s=b;this.u=h;this.v=c;this.w=g;this.A=l;this.B=a;this.C=k;this.D=m;this.sb=function(b){for(var a=b.length,c=this.o,g=this.s,d=this.u,k=this.v,h=this.w,l=this.A,m=this.B,p=this.C,R=this.D,E=0;E<a;E+=2){var S=b[E],z=b[E+1],C=d*S+l*z+R;b[E]=(c*S+k*z+m)/C;b[E+1]=(g*S+h*z+p)/C}};this.Ha=function(){return new Gb(this.w*this.D-this.A*this.C,this.A*this.B-this.v*this.D,this.v*this.C-this.w*this.B,this.u*this.C-this.s*this.D,this.o*this.D-this.u*this.B,this.s*this.B-
this.o*this.C,this.s*this.A-this.u*this.w,this.u*this.v-this.o*this.A,this.o*this.w-this.s*this.v)};this.pb=function(b){return new Gb(this.o*b.o+this.v*b.s+this.B*b.u,this.o*b.v+this.v*b.w+this.B*b.A,this.o*b.B+this.v*b.C+this.B*b.D,this.s*b.o+this.w*b.s+this.C*b.u,this.s*b.v+this.w*b.w+this.C*b.A,this.s*b.B+this.w*b.C+this.C*b.D,this.u*b.o+this.A*b.s+this.D*b.u,this.u*b.v+this.A*b.w+this.D*b.A,this.u*b.B+this.A*b.C+this.D*b.D)}}
function Hb(d,c,a,b,g,k,h,l){var m=l-k,p=c-b+k-l;if(0==m&&0==p)return new Gb(a-d,g-a,d,b-c,k-b,c,0,0,1);var w=a-g,q=h-g;g=d-a+g-h;k=b-k;var u=w*m-q*k;m=(g*m-q*p)/u;p=(w*p-g*k)/u;return new Gb(a-d+m*a,h-d+p*h,d,b-c+m*b,l-c+p*l,c,m,p,1)}function Ib(d){this.i=d}
function Ua(d){this.H=d;this.U=null;this.Ca=function(c,a,b,g){var d=Math.abs(g-a)>Math.abs(b-c);if(d){var h=c;c=a;a=h;h=b;b=g;g=h}var l=Math.abs(b-c),m=Math.abs(g-a),p=-l>>1,w=a<g?1:-1,q=c<b?1:-1,u=0,t=c;for(h=a;t!=b;t+=q){var x=d?h:t,v=d?t:h;1==u?this.H[x+v*f]&&u++:this.H[x+v*f]||u++;if(3==u)return g=t-c,a=h-a,Math.sqrt(g*g+a*a);p+=m;if(0<p){if(h==g)break;h+=w;p-=l}}c=b-c;a=g-a;return Math.sqrt(c*c+a*a)};this.Da=function(c,a,b,g){var d=this.Ca(c,a,b,g),h=1;b=c-(b-c);0>b?(h=c/(c-b),b=0):b>=f&&(h=
(f-1-c)/(b-c),b=f-1);g=Math.floor(a-(g-a)*h);h=1;0>g?(h=a/(a-g),g=0):g>=n&&(h=(n-1-a)/(g-a),g=n-1);b=Math.floor(c+(b-c)*h);d+=this.Ca(c,a,b,g);return d-1};this.qa=function(c,a){var b=this.Da(Math.floor(c.g()),Math.floor(c.h()),Math.floor(a.g()),Math.floor(a.h()));c=this.Da(Math.floor(a.g()),Math.floor(a.h()),Math.floor(c.g()),Math.floor(c.h()));return isNaN(b)?c/7:isNaN(c)?b/7:(b+c)/14};this.Ja=function(c,a,b){return(this.qa(c,a)+this.qa(c,b))/2};this.sa=function(c,a){var b=c.g()-a.g();c=c.h()-a.h();
return Math.sqrt(b*b+c*c)};this.Ka=function(c,a,b,g){c=(Math.round(this.sa(c,a)/g)+Math.round(this.sa(c,b)/g)>>1)+7;switch(c&3){case 0:c++;break;case 2:c--;break;case 3:throw Error("QR Error: in detector");}return c};this.Qa=function(c,a,b,g){g=Math.floor(g*c);var d=Math.max(0,a-g);a=Math.min(f-1,a+g);if(a-d<3*c)throw Error("QR Error: in detector");var h=Math.max(0,b-g);return(new Jb(this.H,d,h,a-d,Math.min(n-1,b+g)-h,c,this.U)).find()};this.La=function(c,a,b,g,d){d-=3.5;var k;if(null!=g){var l=g.g();
g=g.h();var m=k=d-3}else l=a.g()-c.g()+b.g(),g=a.h()-c.h()+b.h(),m=k=d;return Hb(c.g(),c.h(),a.g(),a.h(),l,g,b.g(),b.h()).pb(Hb(3.5,3.5,d,3.5,m,k,3.5,d).Ha())};this.lb=function(c,a,b){for(var g=new Ab(b),d=Array(b<<1),h=0;h<b;h++){for(var l=d.length,m=h+.5,p=0;p<l;p+=2)d[p]=(p>>1)+.5,d[p+1]=m;a.sb(d);m=d;for(var w=f,q=n,u=!0,t=0;t<m.length&&u;t+=2){var x=Math.floor(m[t]),v=Math.floor(m[t+1]);if(-1>x||x>w||-1>v||v>q)throw Error("QR Error: Error.checkAndNudgePoints");u=!1;-1==x?(m[t]=0,u=!0):x==w&&
(m[t]=w-1,u=!0);-1==v?(m[t+1]=0,u=!0):v==q&&(m[t+1]=q-1,u=!0)}u=!0;for(t=m.length-2;0<=t&&u;t-=2){x=Math.floor(m[t]);v=Math.floor(m[t+1]);if(-1>x||x>w||-1>v||v>q)throw Error("QR Error: Error.checkAndNudgePoints");u=!1;-1==x?(m[t]=0,u=!0):x==w&&(m[t]=w-1,u=!0);-1==v?(m[t+1]=0,u=!0):v==q&&(m[t+1]=q-1,u=!0)}try{for(p=0;p<l;p+=2)c[Math.floor(d[p])+f*Math.floor(d[p+1])]&&g.nb(p>>1,h)}catch(ca){throw Error("QR Error: Error.checkAndNudgePoints");}}return g};this.ib=function(c){var a=c.qb,b=c.rb;c=c.Ga;var g=
this.Ja(a,b,c);if(1>g)throw Error("QR Error: in detector");var d=this.Ka(a,b,c,g);if(1!=d%4)throw Error("QR Error: Error getProvisionalVersionForDimension");try{var h=Db(d-17>>2)}catch(w){throw Error("QR Error: Error getVersionForNumber");}var l=h.fa()-7,m=null;if(0<h.ca.length){l=1-3/l;h=Math.floor(a.g()+l*(b.g()-a.g()+c.g()-a.g()));l=Math.floor(a.h()+l*(b.h()-a.h()+c.h()-a.h()));for(var p=4;16>=p;p<<=1)try{m=this.Qa(g,h,l,p);break}catch(w){}}a=this.lb(this.H,this.La(a,b,c,m,d),d);return new Ib(a)};
this.Oa=function(){var c=(new Lb).Ta(this.H);return this.ib(c)}};var Mb=[[21522,0],[20773,1],[24188,2],[23371,3],[17913,4],[16590,5],[20375,6],[19104,7],[30660,8],[29427,9],[32170,10],[30877,11],[26159,12],[25368,13],[27713,14],[26998,15],[5769,16],[5054,17],[7399,18],[6608,19],[1890,20],[597,21],[3340,22],[2107,23],[13663,24],[12392,25],[16177,26],[14854,27],[9396,28],[8579,29],[11994,30],[11245,31]],L=[0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4];function Ob(d){var c=d>>3&3;if(0>c||c>=Sb.length)throw Error("QR Error: ArgumentException");this.Pa=Sb[c];this.Na=d&7}
function Fb(d,c){d^=c;return L[d&15]+L[O(d,4)&15]+L[O(d,8)&15]+L[O(d,12)&15]+L[O(d,16)&15]+L[O(d,20)&15]+L[O(d,24)&15]+L[O(d,28)&15]}function Tb(d){var c=Ub(d);return null!=c?c:Ub(d^21522)}function Ub(d){for(var c=4294967295,a=0,b=0;b<Mb.length;b++){var g=Mb[b],k=g[0];if(k==d)return new Ob(g[1]);k=Fb(d,k);k<c&&(a=g[1],c=k)}return 3>=c?new Ob(a):null};function Vb(d,c,a){this.gb=d;this.i=c;this.name=a;this.getName=function(){return this.name}}var Sb=[new Vb(1,0,"M"),new Vb(0,1,"L"),new Vb(3,2,"H"),new Vb(2,3,"Q")];function Ab(d){var c;c||(c=d);if(1>d||1>c)throw Error("QR Error: Both dimensions must be greater than 0");this.width=d;this.height=c;var a=d>>5;0!=(d&31)&&a++;this.X=a;this.i=Array(a*c);for(d=0;d<this.i.length;d++)this.i[d]=0;this.ea=function(){if(this.width!=this.height)throw Error("QR Error: Can't call getDimension() on a non-square matrix");return this.width};this.ga=function(b,a){return 0!=(O(this.i[a*this.X+(b>>5)],b&31)&1)};this.nb=function(b,a){this.i[a*this.X+(b>>5)]|=1<<(b&31)};this.M=function(b,
a){this.i[a*this.X+(b>>5)]^=1<<(b&31)};this.clear=function(){for(var b=this.i.length,a=0;a<b;a++)this.i[a]=0};this.P=function(b,a,c,d){if(0>a||0>b)throw Error("QR Error: Left and top must be nonnegative");if(1>d||1>c)throw Error("QR Error: Height and width must be at least 1");c=b+c;d=a+d;if(d>this.height||c>this.width)throw Error("QR Error: The region must fit inside the matrix");for(;a<d;a++)for(var g=a*this.X,k=b;k<c;k++)this.i[g+(k>>5)]|=1<<(k&31)}};function ub(d,c){this.ya=d;this.T=c};function tb(d){var c=d.ea();if(21>c||1!=(c&3))throw Error("QR Error: Error BitMatrixParser");this.V=d;this.O=this.J=null;this.K=function(a,b,c){return this.V.ga(a,b)?c<<1|1:c<<1};this.Aa=function(){if(null!=this.O)return this.O;for(var a=0,b=0;6>b;b++)a=this.K(b,8,a);a=this.K(7,8,a);a=this.K(8,8,a);a=this.K(8,7,a);for(b=5;0<=b;b--)a=this.K(8,b,a);this.O=Tb(a);if(null!=this.O)return this.O;var c=this.V.ea();a=0;var d=c-8;for(b=c-1;b>=d;b--)a=this.K(b,8,a);for(b=c-7;b<c;b++)a=this.K(8,b,a);this.O=Tb(a);
if(null!=this.O)return this.O;throw Error("QR Error: Error readFormatInformation");};this.Ba=function(){if(null!=this.J)return this.J;var a=this.V.ea(),b=a-17>>2;if(6>=b)return Db(b);b=0;for(var c=a-11,d=5;0<=d;d--)for(var h=a-9;h>=c;h--)b=this.K(h,d,b);this.J=Eb(b);if(null!=this.J&&this.J.fa()==a)return this.J;b=0;for(h=5;0<=h;h--)for(d=a-9;d>=c;d--)b=this.K(h,d,b);this.J=Eb(b);if(null!=this.J&&this.J.fa()==a)return this.J;throw Error("QR Error: Error readVersion");};this.jb=function(){var a=this.Aa(),
b=this.Ba();a=a.Na;if(0>a||7<a)throw Error("QR Error: System.ArgumentException");var c=Wb[a];a=this.V.ea();c.R(this.V,a);c=b.Ia();for(var d=!0,h=Array(b.na),l=0,m=0,p=0,w=a-1;0<w;w-=2){6==w&&w--;for(var q=0;q<a;q++)for(var u=d?a-1-q:q,t=0;2>t;t++)c.ga(w-t,u)||(p++,m<<=1,this.V.ga(w-t,u)&&(m|=1),8==p&&(h[l++]=m,m=p=0));d^=1}if(l!=b.na)throw Error("QR Error: Error readCodewords");return h}};var Wb=[new function(){this.R=function(d,c){for(var a=0;a<c;a++)for(var b=0;b<c;b++)this.j(a,b)&&d.M(b,a)};this.j=function(d,c){return 0==(d+c&1)}},new function(){this.R=function(d,c){for(var a=0;a<c;a++)for(var b=0;b<c;b++)this.j(a,b)&&d.M(b,a)};this.j=function(d){return 0==(d&1)}},new function(){this.R=function(d,c){for(var a=0;a<c;a++)for(var b=0;b<c;b++)this.j(a,b)&&d.M(b,a)};this.j=function(d,c){return 0==c%3}},new function(){this.R=function(d,c){for(var a=0;a<c;a++)for(var b=0;b<c;b++)this.j(a,
b)&&d.M(b,a)};this.j=function(d,c){return 0==(d+c)%3}},new function(){this.R=function(d,c){for(var a=0;a<c;a++)for(var b=0;b<c;b++)this.j(a,b)&&d.M(b,a)};this.j=function(d,c){return 0==(O(d,1)+c/3&1)}},new function(){this.R=function(d,c){for(var a=0;a<c;a++)for(var b=0;b<c;b++)this.j(a,b)&&d.M(b,a)};this.j=function(d,c){d*=c;return 0==(d&1)+d%3}},new function(){this.R=function(d,c){for(var a=0;a<c;a++)for(var b=0;b<c;b++)this.j(a,b)&&d.M(b,a)};this.j=function(d,c){d*=c;return 0==((d&1)+d%3&1)}},new function(){this.R=
function(d,c){for(var a=0;a<c;a++)for(var b=0;b<c;b++)this.j(a,b)&&d.M(b,a)};this.j=function(d,c){return 0==((d+c&1)+d*c%3&1)}}];function P(d,c){if(null==c||0==c.length)throw Error("QR Error: System.ArgumentException");this.a=d;var a=c.length;if(1<a&&0==c[0]){for(var b=1;b<a&&0==c[b];)b++;if(b==a)this.f=d.l().f;else{this.f=Array(a-b);for(a=0;a<this.f.length;a++)this.f[a]=0;for(a=0;a<this.f.length;a++)this.f[a]=c[b+a]}}else this.f=c;this.l=function(){return 0==this.f[0]};this.I=function(){return this.f.length-1};this.Z=function(b){return this.f[this.f.length-1-b]};this.la=function(b){if(0==b)return this.Z(0);var a=this.f.length;
if(1==b){for(var c=b=0;c<a;c++)b^=this.f[c];return b}var d=this.f[0];for(c=1;c<a;c++)d=Xb(this.a.multiply(b,d),this.f[c]);return d};this.ba=function(b){if(this.a!=b.a)throw Error("QR Error: GF256Polys do not have same GF256 field");if(this.l())return b;if(b.l())return this;var a=this.f;b=b.f;if(a.length>b.length){var c=a;a=b;b=c}c=Array(b.length);for(var g=b.length-a.length,m=0;m<g;m++)c[m]=b[m];for(m=g;m<b.length;m++)c[m]=a[m-g]^b[m];return new P(d,c)};this.wa=function(b){if(this.a!=b.a)throw Error("QR Error: GF256Polys do not have same GF256 field");
if(this.l()||b.l())return this.a.l();var a=this.f,c=a.length;b=b.f;for(var d=b.length,g=Array(c+d-1),p=0;p<c;p++)for(var w=a[p],q=0;q<d;q++)g[p+q]=Xb(g[p+q],this.a.multiply(w,b[q]));return new P(this.a,g)};this.xa=function(b){if(0==b)return this.a.l();if(1==b)return this;for(var a=this.f.length,c=Array(a),d=0;d<a;d++)c[d]=this.a.multiply(this.f[d],b);return new P(this.a,c)};this.eb=function(b,a){if(0>b)throw Error("QR Error: System.ArgumentException");if(0==a)return this.a.l();var c=this.f.length;
b=Array(c+b);for(var d=0;d<b.length;d++)b[d]=0;for(d=0;d<c;d++)b[d]=this.a.multiply(this.f[d],a);return new P(this.a,b)}};function Yb(d){this.Y=Array(256);this.aa=Array(256);for(var c=1,a=0;256>a;a++)this.Y[a]=c,c<<=1,256<=c&&(c^=d);for(a=0;255>a;a++)this.aa[this.Y[a]]=a;d=Array(1);d[0]=0;this.Ea=new P(this,Array(d));d=Array(1);d[0]=1;this.za=new P(this,Array(d));this.l=function(){return this.Ea};this.pa=function(b,a){if(0>b)throw Error("QR Error: System.ArgumentException");if(0==a)return this.Ea;b=Array(b+1);for(var c=0;c<b.length;c++)b[c]=0;b[0]=a;return new P(this,b)};this.exp=function(b){return this.Y[b]};this.log=
function(b){if(0==b)throw Error("QR Error: System.ArgumentException");return this.aa[b]};this.inverse=function(b){if(0==b)throw Error("QR Error: System.ArithmeticException");return this.Y[255-this.aa[b]]};this.multiply=function(b,a){return 0==b||0==a?0:1==b?a:1==a?b:this.Y[(this.aa[b]+this.aa[a])%255]}}var Zb=new Yb(285);new Yb(301);function Xb(d,c){return d^c};var vb=new function(d){this.a=d;this.decode=function(c,a){for(var b=new P(this.a,c),d=Array(a),k=0;k<d.length;k++)d[k]=0;var h=!0;for(k=0;k<a;k++){var l=b.la(this.a.exp(k));d[d.length-1-k]=l;0!=l&&(h=!1)}if(!h)for(k=new P(this.a,d),a=this.kb(this.a.pa(a,1),k,a),k=a[1],a=this.Ra(a[0]),b=this.Sa(k,a),k=0;k<a.length;k++){d=c.length-1-this.a.log(a[k]);if(0>d)throw Error("QR Error: ReedSolomonException Bad error location");c[d]^=b[k]}};this.kb=function(c,a,b){if(c.I()<a.I()){var d=c;c=a;a=d}d=this.a.za;
for(var k=this.a.l(),h=this.a.l(),l=this.a.za;a.I()>=Math.floor(b/2);){var m=c,p=d,w=h;c=a;d=k;h=l;if(c.l())throw Error("QR Error: r_{i-1} was zero");a=m;l=this.a.l();for(k=this.a.inverse(c.Z(c.I()));a.I()>=c.I()&&!a.l();){m=a.I()-c.I();var q=this.a.multiply(a.Z(a.I()),k);l=l.ba(this.a.pa(m,q));a=a.ba(c.eb(m,q))}k=l.wa(d).ba(p);l=l.wa(h).ba(w)}b=l.Z(0);if(0==b)throw Error("QR Error: ReedSolomonException sigmaTilde(0) was zero");b=this.a.inverse(b);c=l.xa(b);b=a.xa(b);return[c,b]};this.Ra=function(c){var a=
c.I();if(1==a)return Array(c.Z(1));for(var b=Array(a),d=0,k=1;256>k&&d<a;k++)0==c.la(k)&&(b[d]=this.a.inverse(k),d++);if(d!=a)throw Error("QR Error: Error locator degree does not match number of roots");return b};this.Sa=function(c,a){for(var b=a.length,d=Array(b),k=0;k<b;k++){for(var h=this.a.inverse(a[k]),l=1,m=0;m<b;m++)k!=m&&(l=this.a.multiply(l,Xb(1,this.a.multiply(a[m],h))));d[k]=this.a.multiply(c.la(h),this.a.inverse(l))}return d}}(Zb);var xb,Qa=null,f=0,n=0,Pa=!1,$b=[[10,9,8,8],[12,11,16,10],[14,13,16,12]],Sa={red:77,blue:150,green:29};function O(d,c){return 0<=d?d>>c:(d>>c)+(2<<~c)};var ac=3,bc=57,cc=2;function dc(d){function c(b,a){var c=b.g()-a.g();b=b.h()-a.h();return Math.sqrt(c*c+b*b)}var a=c(d[0],d[1]),b=c(d[1],d[2]),g=c(d[0],d[2]);b>=a&&b>=g?(b=d[0],a=d[1],g=d[2]):g>=b&&g>=a?(b=d[1],a=d[0],g=d[2]):(b=d[2],a=d[0],g=d[1]);if(0>function(b,a,c){var d=a.x;a=a.y;return(c.x-d)*(b.y-a)-(c.y-a)*(b.x-d)}(a,b,g)){var k=a;a=g;g=k}d[0]=a;d[1]=b;d[2]=g}
function ec(d,c,a){this.x=d;this.y=c;this.count=1;this.G=a;this.g=function(){return this.x};this.h=function(){return this.y};this.va=function(){this.count++};this.ha=function(b,a,c){return Math.abs(a-this.y)<=b&&Math.abs(c-this.x)<=b?(b=Math.abs(b-this.G),1>=b||1>=b/this.G):!1}}function fc(d){this.Ga=d[0];this.qb=d[1];this.rb=d[2]}
function Lb(){this.H=null;this.c=[];this.ma=!1;this.L=[0,0,0,0,0];this.U=null;this.ta=function(){this.L[0]=0;this.L[1]=0;this.L[2]=0;this.L[3]=0;this.L[4]=0;return this.L};this.N=function(d){for(var c=0,a=0;5>a;a++){var b=d[a];if(0==b)return!1;c+=b}if(7>c)return!1;c=Math.floor(c/7);a=Math.floor(.7*c);return Math.abs(c-d[0])<a&&Math.abs(c-d[1])<a&&Math.abs(3*c-d[2])<3*a&&Math.abs(c-d[3])<a&&Math.abs(c-d[4])<a};this.W=function(d,c){return c-d[4]-d[3]-d[2]/2};this.ia=function(d,c,a,b){for(var g=this.H,
k=n,h=this.ta(),l=d;0<=l&&g[c+l*f];)h[2]++,l--;if(0>l)return NaN;for(;0<=l&&!g[c+l*f]&&h[1]<=a;)h[1]++,l--;if(0>l||h[1]>a)return NaN;for(;0<=l&&g[c+l*f]&&h[0]<=a;)h[0]++,l--;if(h[0]>a)return NaN;for(l=d+1;l<k&&g[c+l*f];)h[2]++,l++;if(l==k)return NaN;for(;l<k&&!g[c+l*f]&&h[3]<a;)h[3]++,l++;if(l==k||h[3]>=a)return NaN;for(;l<k&&g[c+l*f]&&h[4]<a;)h[4]++,l++;return h[4]>=a||5*Math.abs(h[0]+h[1]+h[2]+h[3]+h[4]-b)>=2*b?NaN:this.N(h)?this.W(h,l):NaN};this.Ma=function(d,c,a,b){for(var g=this.H,k=f,h=this.ta(),
l=d;0<=l&&g[l+c*f];)h[2]++,l--;if(0>l)return NaN;for(;0<=l&&!g[l+c*f]&&h[1]<=a;)h[1]++,l--;if(0>l||h[1]>a)return NaN;for(;0<=l&&g[l+c*f]&&h[0]<=a;)h[0]++,l--;if(h[0]>a)return NaN;for(l=d+1;l<k&&g[l+c*f];)h[2]++,l++;if(l==k)return NaN;for(;l<k&&!g[l+c*f]&&h[3]<a;)h[3]++,l++;if(l==k||h[3]>=a)return NaN;for(;l<k&&g[l+c*f]&&h[4]<a;)h[4]++,l++;return h[4]>=a||5*Math.abs(h[0]+h[1]+h[2]+h[3]+h[4]-b)>=b?NaN:this.N(h)?this.W(h,l):NaN};this.$=function(d,c,a){var b=d[0]+d[1]+d[2]+d[3]+d[4];a=this.W(d,a);c=this.ia(c,
Math.floor(a),d[2],b);if(!isNaN(c)&&(a=this.Ma(Math.floor(a),Math.floor(c),d[2],b),!isNaN(a))){d=b/7;b=!1;for(var g=this.c.length,k=0;k<g;k++){var h=this.c[k];if(h.ha(d,c,a)){h.va();b=!0;break}}b||(a=new ec(a,c,d),this.c.push(a),null!=this.U&&this.U.Va(a));return!0}return!1};this.mb=function(){var d=this.c.length;if(3>d)throw Error("QR Error: Couldn't find enough finder patterns (found "+d+")");if(3<d){for(var c=0,a=0,b=0;b<d;b++){var g=this.c[b].G;c+=g;a+=g*g}var k=c/d;this.c.sort(function(b,a){a=
Math.abs(a.G-k);b=Math.abs(b.G-k);return a<b?-1:a==b?0:1});d=Math.max(.2*k,Math.sqrt(a/d-k*k));for(b=this.c.length-1;0<=b;b--)Math.abs(this.c[b].G-k)>d&&this.c.splice(b,1)}3<this.c.length&&this.c.sort(function(b,a){return b.count>a.count?-1:b.count<a.count?1:0});return[this.c[0],this.c[1],this.c[2]]};this.Ua=function(){var d=this.c.length;if(1>=d)return 0;for(var c=null,a=0;a<d;a++){var b=this.c[a];if(b.count>=cc)if(null==c)c=b;else return this.ma=!0,Math.floor((Math.abs(c.g()-b.g())-Math.abs(c.h()-
b.h()))/2)}return 0};this.ua=function(){for(var d=0,c=0,a=this.c.length,b=0;b<a;b++){var g=this.c[b];g.count>=cc&&(d++,c+=g.G)}if(3>d)return!1;d=c/a;var k=0;for(b=0;b<a;b++)g=this.c[b],k+=Math.abs(g.G-d);return k<=.05*c};this.Ta=function(d){this.H=d;var c=n,a=f,b=Math.floor(3*c/(4*bc));b<ac&&(b=ac);for(var g=!1,k=Array(5),h=b-1;h<c&&!g;h+=b){k[0]=0;k[1]=0;k[2]=0;k[3]=0;for(var l=k[4]=0,m=0;m<a;m++)if(d[m+h*f])1==(l&1)&&l++,k[l]++;else if(0==(l&1))if(4==l)if(this.N(k)){if(l=this.$(k,h,m))b=2,this.ma?
g=this.ua():(l=this.Ua(),l>k[2]&&(h+=l-k[2]-b,m=a-1));else{do m++;while(m<a&&!d[m+h*f]);m--}l=0;k[0]=0;k[1]=0;k[2]=0;k[3]=0;k[4]=0}else k[0]=k[2],k[1]=k[3],k[2]=k[4],k[3]=1,k[4]=0,l=3;else k[++l]++;else k[l]++;this.N(k)&&(l=this.$(k,h,a))&&(b=k[0],this.ma&&(g=this.ua()))}d=this.mb();dc(d);return new fc(d)}};function gc(d,c,a){this.x=d;this.y=c;this.count=1;this.G=a;this.g=function(){return Math.floor(this.x)};this.h=function(){return Math.floor(this.y)};this.va=function(){this.count++};this.ha=function(b,a,c){return Math.abs(a-this.y)<=b&&Math.abs(c-this.x)<=b?(b=Math.abs(b-this.G),1>=b||1>=b/this.G):!1}}
function Jb(d,c,a,b,g,k,h){this.H=d;this.c=[];this.ob=c;this.width=b;this.height=g;this.cb=k;this.L=[0,0,0];this.U=h;this.W=function(b,a){return a-b[2]-b[1]/2};this.N=function(b){for(var a=this.cb,c=a/2,d=0;3>d;d++)if(Math.abs(a-b[d])>=c)return!1;return!0};this.ia=function(b,a,c,d){var g=this.H,k=n,h=this.L;h[0]=0;h[1]=0;h[2]=0;for(var l=b;0<=l&&g[a+l*f]&&h[1]<=c;)h[1]++,l--;if(0>l||h[1]>c)return NaN;for(;0<=l&&!g[a+l*f]&&h[0]<=c;)h[0]++,l--;if(h[0]>c)return NaN;for(l=b+1;l<k&&g[a+l*f]&&h[1]<=c;)h[1]++,
l++;if(l==k||h[1]>c)return NaN;for(;l<k&&!g[a+l*f]&&h[2]<=c;)h[2]++,l++;return h[2]>c||5*Math.abs(h[0]+h[1]+h[2]-d)>=2*d?NaN:this.N(h)?this.W(h,l):NaN};this.$=function(b,a,c){c=this.W(b,c);a=this.ia(a,Math.floor(c),2*b[1],b[0]+b[1]+b[2]);if(!isNaN(a)){b=(b[0]+b[1]+b[2])/3;for(var d=this.c.length,g=0;g<d;g++)if(this.c[g].ha(b,a,c))return new gc(c,a,b);c=new gc(c,a,b);this.c.push(c);null!=this.U&&this.U.Va(c)}return null};this.find=function(){for(var c=this.ob,g=this.height,k=c+b,h=a+(g>>1),q=[0,0,
0],u=0;u<g;u++){var t=h+(0==(u&1)?u+1>>1:-(u+1>>1));q[0]=0;q[1]=0;q[2]=0;for(var x=c;x<k&&!d[x+f*t];)x++;for(var v=0;x<k;){if(d[x+t*f])if(1==v)q[v]++;else if(2==v){if(this.N(q)&&(v=this.$(q,t,x),null!=v))return v;q[0]=q[2];q[1]=1;q[2]=0;v=1}else q[++v]++;else 1==v&&v++,q[v]++;x++}if(this.N(q)&&(v=this.$(q,t,k),null!=v))return v}if(0!=this.c.length)return this.c[0];throw Error("QR Error: Couldn't find enough alignment patterns");}};function wb(d,c,a){this.F=0;this.b=7;this.S=d;this.fb=a;9>=c?this.ja=0:10<=c&&26>=c?this.ja=1:27<=c&&40>=c&&(this.ja=2);this.m=function(b){var a;if(b<this.b+1){var c=0;for(a=0;a<b;a++)c+=1<<a;c<<=this.b-b+1;a=(this.S[this.F]&c)>>this.b-b+1;this.b-=b;return a}if(b<this.b+1+8){var d=0;for(a=0;a<this.b+1;a++)d+=1<<a;a=(this.S[this.F]&d)<<b-(this.b+1);this.F++;a+=this.S[this.F]>>8-(b-(this.b+1));this.b-=b%8;0>this.b&&(this.b=8+this.b);return a}if(b<this.b+1+16){for(a=c=d=0;a<this.b+1;a++)d+=1<<a;d=(this.S[this.F]&
d)<<b-(this.b+1);this.F++;var l=this.S[this.F]<<b-(this.b+1+8);this.F++;for(a=0;a<b-(this.b+1+8);a++)c+=1<<a;c<<=8-(b-(this.b+1+8));a=d+l+((this.S[this.F]&c)>>8-(b-(this.b+1+8)));this.b-=(b-8)%8;0>this.b&&(this.b=8+this.b);return a}return 0};this.Fa=function(){return this.F>this.S.length-this.fb-2?0:this.m(4)};this.Ya=function(b){for(var a=0;1!=b>>a;)a++;return this.m($b[this.ja][a])};this.bb=function(b){var a="",c="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:".split("");do if(1<b){var d=this.m(11);
var l=d%45;a+=c[Math.floor(d/45)];a+=c[l];b-=2}else 1==b&&(d=this.m(6),a+=c[d],--b);while(0<b);return a};this.$a=function(b){var a=0,c="";do 3<=b?(a=this.m(10),100>a&&(c+="0"),10>a&&(c+="0"),b-=3):2==b?(a=this.m(7),10>a&&(c+="0"),b-=2):1==b&&(a=this.m(4),--b),c+=a;while(0<b);return c};this.Wa=function(b){var a=[];do{var c=this.m(8);a.push(c);b--}while(0<b);return a};this.ab=function(b){var a="";do{var c=this.m(13);c=(c/192<<8)+c%192;a+=String.fromCharCode(40956>=c+33088?c+33088:c+49472);b--}while(0<
b);return a};this.hb=function(){var b=this.m(8);128==(b&192)&&this.m(8);192==(b&224)&&this.m(8)};this.Xa=function(){var b=[];do{var a=this.Fa();if(0==a)if(0<b.length)break;else throw Error("QR Error: Empty data block");if(1!=a&&2!=a&&4!=a&&8!=a&&7!=a)throw Error("QR Error: Invalid mode: "+a+" in (block:"+this.F+" bit:"+this.b+")");if(7==a)this.hb();else{var c=this.Ya(a);if(1>c)throw Error("QR Error: Invalid data length: "+c);switch(a){case 1:a=this.$a(c);c=Array(a.length);for(var d=0;d<a.length;d++)c[d]=
a.charCodeAt(d);b.push(c);break;case 2:a=this.bb(c);c=Array(a.length);for(d=0;d<a.length;d++)c[d]=a.charCodeAt(d);b.push(c);break;case 4:a=this.Wa(c);b.push(a);break;case 8:a=this.ab(c),b.push(a)}}}while(1);return b}};
}).call(this)
//# sourceMappingURL=qr-scanner-worker.min.js.map

@ -19,6 +19,7 @@
"searchOrPaste": "Search Token or Paste Address",
"noExchange": "No Exchange Found",
"exchangeRate": "Exchange Rate",
"unknownError": "Oops! An unknown error occurred. Please refresh the page, or visit from another browser or device.",
"enterValueCont": "Enter a {{ missingCurrencyValue }} value to continue.",
"selectTokenCont": "Select a token to continue.",
"noLiquidity": "No liquidity.",

100
src/InjectedConnector.js Normal file

@ -0,0 +1,100 @@
import { Connectors } from 'web3-react'
const { Connector, ErrorCodeMixin } = Connectors
const InjectedConnectorErrorCodes = ['ETHEREUM_ACCESS_DENIED', 'NO_WEB3', 'UNLOCK_REQUIRED']
export default class InjectedConnector extends ErrorCodeMixin(Connector, InjectedConnectorErrorCodes) {
constructor(args = {}) {
super(args)
this.runOnDeactivation = []
this.networkChangedHandler = this.networkChangedHandler.bind(this)
this.accountsChangedHandler = this.accountsChangedHandler.bind(this)
const { ethereum } = window
if (ethereum && ethereum.isMetaMask) {
ethereum.autoRefreshOnNetworkChange = false
}
}
async onActivation() {
const { ethereum, web3 } = window
if (ethereum) {
await ethereum.enable().catch(error => {
const deniedAccessError = Error(error)
deniedAccessError.code = InjectedConnector.errorCodes.ETHEREUM_ACCESS_DENIED
throw deniedAccessError
})
// initialize event listeners
if (ethereum.on) {
ethereum.on('networkChanged', this.networkChangedHandler)
ethereum.on('accountsChanged', this.accountsChangedHandler)
this.runOnDeactivation.push(() => {
if (ethereum.removeListener) {
ethereum.removeListener('networkChanged', this.networkChangedHandler)
ethereum.removeListener('accountsChanged', this.accountsChangedHandler)
}
})
}
} else if (web3) {
console.warn('Your web3 provider is outdated, please upgrade to a modern provider.')
} else {
const noWeb3Error = Error('Your browser is not equipped with web3 capabilities.')
noWeb3Error.code = InjectedConnector.errorCodes.NO_WEB3
throw noWeb3Error
}
}
async getProvider() {
const { ethereum, web3 } = window
return ethereum || web3.currentProvider
}
async getAccount(provider) {
const account = await super.getAccount(provider)
if (account === null) {
const unlockRequiredError = Error('Ethereum account locked.')
unlockRequiredError.code = InjectedConnector.errorCodes.UNLOCK_REQUIRED
throw unlockRequiredError
}
return account
}
onDeactivation() {
this.runOnDeactivation.forEach(runner => runner())
this.runOnDeactivation = []
}
// event handlers
networkChangedHandler(networkId) {
const networkIdNumber = Number(networkId)
try {
super._validateNetworkId(networkIdNumber)
super._web3ReactUpdateHandler({
updateNetworkId: true,
networkId: networkIdNumber
})
} catch (error) {
super._web3ReactErrorHandler(error)
}
}
accountsChangedHandler(accounts) {
if (!accounts[0]) {
const unlockRequiredError = Error('Ethereum account locked.')
unlockRequiredError.code = InjectedConnector.errorCodes.UNLOCK_REQUIRED
super._web3ReactErrorHandler(unlockRequiredError)
} else {
super._web3ReactUpdateHandler({
updateAccount: true,
account: accounts[0]
})
}
}
}

@ -1,24 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1736 2036.92">
<defs>
<style>
.cls-2{fill:#fff}
</style>
<linearGradient id="linear-gradient" x1="868.25" y1="-1245.53" x2="868.25" y2="791.39" gradientTransform="translate(-.25 1245.53)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f9ba16"/>
<stop offset=".04" stop-color="#faa612"/>
<stop offset=".11" stop-color="#fc8b0c"/>
<stop offset=".19" stop-color="#fd7508"/>
<stop offset=".28" stop-color="#fe6404"/>
<stop offset=".4" stop-color="#ff5902"/>
<stop offset=".56" stop-color="#ff5200"/>
<stop offset="1" stop-color="#ff5000"/>
</linearGradient>
</defs>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path d="M1736 659l-148.2-256.1 17.6-82.9-134.9-137-111.9 15.4L1177.4 0h-310l-310 2.5-180.8 198-111.9-16.1-134.1 138.5 17.2 82.5L0 660.5 40.5 814l184 700.6a358.61 358.61 0 0 0 138.1 199.8s223.9 157.8 444.5 301a98.76 98.76 0 0 0 123.1 0c247.9-162.5 444.1-301.7 444.1-301.7a359 359 0 0 0 137.8-199.8l183.3-701z" fill="url(#linear-gradient)"/>
<path class="cls-2" d="M915.5 1213.3a266.26 266.26 0 0 0-35.9-13.3h-22.2a253.36 253.36 0 0 0-35.9 13.3l-54.9 23c-17.6 7.2-45.2 20.4-62.1 29.1L603.4 1318a19.39 19.39 0 0 0-2.5 35.9l87.5 62.1c15.4 10.8 39.8 29.8 53.8 42.3l24.8 21.2 51.7 45.2 23.3 20.8a40.33 40.33 0 0 0 51.7 0L918 1524l51.7-45.2 24.8-21.5c14.3-12.6 38.4-31.6 53.8-42.3l87.9-62.8a19 19 0 0 0-2.5-35.8l-102.2-51.3c-16.9-8.6-44.8-21.5-62.4-28.7l-53.6-23.1z"/>
<path class="cls-2" d="M1536.9 698.5l2.9-9a239.82 239.82 0 0 0-2.5-35.9 307.9 307.9 0 0 0-32.6-61.3l-56.7-83.6c-10.4-15.8-28.7-40.5-40.5-55.2l-76.1-94.7a259.16 259.16 0 0 0-22.6-26.5l-33.7 6.1-118.7 20.4-50.9 9.7a131.88 131.88 0 0 1-50.2-7.2l-92.2-29.8c-17.9-5.7-47.7-14-66-18.3a141.51 141.51 0 0 0-29.1 0 141.51 141.51 0 0 0-29.1 0c-18.3 4.3-48.1 12.6-66 18.3l-92.2 29.8a133.86 133.86 0 0 1-50.2 7.2l-51.3-9.7-117-22.6-33.7-6.1a259.16 259.16 0 0 0-22.6 26.5l-77.8 96.9c-11.8 14.7-30.1 39.5-40.5 55.2l-56.7 83.9a461.89 461.89 0 0 0-26.9 44.8 203.06 203.06 0 0 0-7.9 53.1l3.2 9a127.14 127.14 0 0 0 5.4 17.2 578.09 578.09 0 0 0 46.6 49.9l196.6 181.9a52.81 52.81 0 0 1 9.7 56L425 1082a101.22 101.22 0 0 0 0 63.9l6.5 17.9a172.65 172.65 0 0 0 53.1 74.6l31.2 25.5a59.06 59.06 0 0 0 57.4 6.8l111.6-53.1a284 284 0 0 0 56.3-37.7l89.3-80.7a35.89 35.89 0 0 0 2.4-50.7l-.6-.6L678 915.5a51.14 51.14 0 0 1-12.2-54.5l51.3-127a89.25 89.25 0 0 0 0-59.9 88.69 88.69 0 0 0-44.5-40.9l-245-92.6c-17.6-6.8-16.9-13.6 2.2-15.4l127-11.8a184.69 184.69 0 0 1 66.4 8.3l126.6 45.2a40 40 0 0 1 24.8 44.8L728.3 814c-4.3 18.3-8.6 38.7-10 45.2a35.78 35.78 0 0 0 31.2 18.3l88.3 17.2a204.2 204.2 0 0 0 67.1 0l80.7-16.9a35.71 35.71 0 0 0 31.2-18.7c0-6.5-5.7-26.5-10-44.8l-47-202.7a39.36 39.36 0 0 1 24.8-44.8l126.6-45.2a184.14 184.14 0 0 1 66.4-8.3l127 11.8c18.7 0 19.7 8.6 2.2 15.4l-244.7 92.9a88.54 88.54 0 0 0-44.5 40.2 89.25 89.25 0 0 0 0 59.9l53.1 127.4a51.39 51.39 0 0 1-12.9 54.2L904 1047.9a35.77 35.77 0 0 0 1.2 50.7l.6.6 89.7 79.6a285.94 285.94 0 0 0 56.3 35.9l111.6 53.1a59.06 59.06 0 0 0 57.4-6.8l31.2-25.5a173.23 173.23 0 0 0 53.1-74.6l6.5-17.9a101.22 101.22 0 0 0 0-63.9l-34.4-74.6a52.92 52.92 0 0 1 11.8-54.9l196.2-182.2a581.78 581.78 0 0 0 46.6-50.2 123.28 123.28 0 0 0 5.1-18.7z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

@ -1,8 +1,11 @@
<svg width="256" height="417" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid">
<path fill="#343434" d="M127.961 0l-2.795 9.5v275.668l2.795 2.79 127.962-75.638z"/>
<path fill="#8C8C8C" d="M127.962 0L0 212.32l127.962 75.639V154.158z"/>
<path fill="#3C3C3B" d="M127.961 312.187l-1.575 1.92v98.199l1.575 4.6L256 236.587z"/>
<path fill="#8C8C8C" d="M127.962 416.905v-104.72L0 236.585z"/>
<path fill="#141414" d="M127.961 287.958l127.96-75.637-127.96-58.162z"/>
<path fill="#393939" d="M0 212.32l127.96 75.638v-133.8z"/>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="256px" height="417px" viewBox="0 0 256 417" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<polygon fill="#343434" points="127.9611 0 125.1661 9.5 125.1661 285.168 127.9611 287.958 255.9231 212.32"/>
<polygon fill="#8C8C8C" points="127.962 0 0 212.32 127.962 287.959 127.962 154.158"/>
<polygon fill="#3C3C3B" points="127.9611 312.1866 126.3861 314.1066 126.3861 412.3056 127.9611 416.9066 255.9991 236.5866"/>
<polygon fill="#8C8C8C" points="127.962 416.9052 127.962 312.1852 0 236.5852"/>
<polygon fill="#141414" points="127.9611 287.9577 255.9211 212.3207 127.9611 154.1587"/>
<polygon fill="#393939" points="0.0009 212.3208 127.9609 287.9578 127.9609 154.1588"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

@ -1,60 +0,0 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 318.6 318.6" xml:space="preserve">
<style>
.st1{fill:#e4761b;stroke:#e4761b}.st1,.st10{stroke-linecap:round;stroke-linejoin:round}.st2{fill:#763d16;stroke:#763d16}.st2,.st3,.st4,.st5,.st6,.st7,.st8{stroke-linecap:round;stroke-linejoin:round}.st3{fill:#f6851b;stroke:#f6851b}.st4{fill:#e2761b;stroke:#e2761b}.st5{fill:#cd6116;stroke:#cd6116}.st6{fill:#c0ad9e;stroke:#c0ad9e}.st7{fill:#d7c1b3;stroke:#d7c1b3}.st8{fill:#e4751f;stroke:#e4751f}.st10{fill:#161616;stroke:#161616}
</style>
<path fill="#161616" stroke="#161616" d="M277.3 145.6l-5-3.6 8-7.3-6.1-4.8 8-6.1-5.3-4 8.4-40.8-12.6-37.9-81.1 30.3h-67.5L43 41.1 30.4 79l8.5 40.8-5.4 4 8 6.1-6.1 4.8 8 7.3-5 3.6 11.5 13.5-17.4 54.2 16.1 55.3 56.7-15.6 11 9 22.4 15.5H177l22.4-15.5 11-9 56.7 15.6 16.2-55.3-17.5-54.2z"/>
<path class="st1" d="M105.3 253l-56.7 15.6-16.1-55.3zm178-39.7l-16.2 55.3-56.7-15.6z"/>
<path class="st2" d="M265.8 159.1l-52.3-15.3 18.3-4.8zm-215.9 0L84 139l18.2 4.8zM43.4 142l-1.9-12.1L84 139z"/>
<path class="st2" d="M272.3 142l-40.5-3 42.4-9.1z"/>
<path class="st2" d="M272.3 142l-6.5 17.1-34-20.1zm-228.9 0l40.6-3-34.1 20.1zm188.4-3l45.1-19.2-2.7 10.1zM84 139l-42.5-9.1-2.6-10.1z"/>
<path class="st3" d="M124.1 71.4h67.5l-15.1 41.1z"/>
<path class="st3" d="M176.5 112.5h-37.3l-15.1-41.1z"/>
<path class="st2" d="M276.9 119.8L231.8 139l-.8-51.6zm-174.7 24L84 139l.7-51.6z"/>
<path class="st2" d="M84.7 87.4L84 139l-45.1-19.2zm146.3 0l.8 51.6-18.3 4.8z"/>
<path class="st1" d="M139.2 112.5L43 41.1l81.1 30.3z"/>
<path class="st4" d="M272.7 41.1l-96.2 71.4 15.1-41.1z"/>
<path class="st1" d="M210.4 253l26.5-39.7h46.4zM32.5 213.3h46.4l26.4 39.7z"/>
<path class="st3" d="M229.3 167.7l54 45.6h-46.4zm-142.9 0l-53.9 45.6 17.4-54.2zm-7.5 45.6H32.5l53.9-45.6z"/>
<path class="st3" d="M229.3 167.7l36.5-8.6 17.5 54.2z"/>
<path class="st2" d="M84.7 87.4l54.5 25.1-37 31.3zm128.8 56.4l-37-31.3L231 87.4zm52.3 15.3l6.5-17.1 5 3.6zm-215.9 0l-11.5-13.5 5-3.6zM272.3 142l1.9-12.1 6.1 4.8zm-228.9 0l-8-7.3 6.1-4.8z"/>
<path class="st2" d="M33.5 123.8l5.4-4 2.6 10.1zm248.7 0l-8 6.1 2.7-10.1z"/>
<path class="st3" d="M49.9 159.1l52.3-15.3-15.8 23.9zm215.9 0l-36.5 8.6-15.8-23.9z"/>
<path class="st2" d="M38.9 119.8L30.4 79l54.3 8.4zM231 87.4l54.3-8.4-8.4 40.8z"/>
<path class="st1" d="M102.2 143.8l37-31.3 3.4 57.7zm111.3 0l15.8 23.9-56.2 2.5zm-40.4 26.4l3.4-57.7 37 31.3z"/>
<path class="st1" d="M142.6 170.2l-56.2-2.5 15.8-23.9z"/>
<path class="st2" d="M272.7 41.1L285.3 79 231 87.4zM43 41.1l96.2 71.4-54.5-25.1zm188 46.3l-54.5 25.1 96.2-71.4z"/>
<path class="st2" d="M84.7 87.4L30.4 79 43 41.1z"/>
<path class="st5" d="M105.3 253l-26.4-39.7 31.1.4zm105.1 0l-4.7-39.3 31.2-.4z"/>
<path class="st3" d="M173.1 170.2h-30.5l-3.4-57.7z"/>
<path class="st3" d="M139.2 112.5h37.3l-3.4 57.7z"/>
<path class="st6" d="M116.3 262l-11-9 31.5 14.9zm62.6 5.9l31.5-14.9-11 9z"/>
<path class="st7" d="M136.6 258.6l.2 9.3-31.5-14.9zm42.6 0l31.2-5.6-31.5 14.9z"/>
<path class="st3" d="M86.4 167.7l23.6 46-31.1-.4zm150.5 45.6l-31.2.4 23.6-46z"/>
<path class="st8" d="M86.4 167.7l22.8 23.1.8 22.9zm142.9 0l-23.6 46 .9-22.9z"/>
<path class="st7" d="M105.3 253l33.9-16.5-2.6 22.1zm105.1 0l-31.2 5.6-2.7-22.1z"/>
<path class="st1" d="M139.2 236.5L105.3 253l4.7-39.3zm37.3 0l29.2-22.8 4.7 39.3z"/>
<path class="st5" d="M173.1 170.2l56.2-2.5-22.7 23.1zm-63.9 20.6l-22.8-23.1 56.2 2.5z"/>
<path class="st5" d="M142.6 170.2l-13.5 11.5-19.9 9.1zm64 20.6l-20-9.1-13.5-11.5z"/>
<path class="st3" d="M205.7 213.7l-27.4-14.6 28.3-8.3zm-95.7 0l-.8-22.9 28.2 8.3z"/>
<path d="M137.4 199.1l-28.2-8.3 19.9-9.1zm40.9 0l8.3-17.4 20 9.1z" fill="#233447" stroke="#233447" stroke-linecap="round" stroke-linejoin="round"/>
<path class="st5" d="M186.6 181.7l-8.3 17.4-5.2-28.9zm-57.5 0l13.5-11.5-5.2 28.9z"/>
<path class="st6" d="M199.4 262L177 277.5l1.9-9.6zm-62.6 5.9l1.9 9.6-22.4-15.5z"/>
<path class="st4" d="M178.3 199.1l-6.5-10.7 1.3-18.2z"/>
<path class="st8" d="M137.4 199.1l5.2-28.9 1.3 18.2z"/>
<path class="st3" d="M173.1 170.2l-1.3 18.2h-27.9z"/>
<path class="st3" d="M143.9 188.4l-1.3-18.2h30.5zm34.4 10.7l27.4 14.6-29.2 22.8zm-39.1 37.4L110 213.7l27.4-14.6z"/>
<path class="st3" d="M137.4 199.1l6.6 34.1-4.8 3.3zm39.1 37.4l-4.8-3.3 6.6-34.1z"/>
<path class="st8" d="M171.8 188.4l6.5 10.7-6.6 34.1zm-27.9 0l.1 44.8-6.6-34.1z"/>
<path class="st3" d="M143.9 188.4h27.9l-.1 44.8zm27.8 44.8H144l-.1-44.8z"/>
<path class="st6" d="M179.2 258.6l-.3 9.3-1.9 9.6zm-40.5 18.9l-1.9-9.6-.2-9.3z"/>
<path class="st6" d="M136.6 258.6l2.4-2.2-.3 21.1zm40.4 18.9l-.3-21.1 2.5 2.2z"/>
<path class="st6" d="M138.7 277.5l.3-21.1h37.7zm38-21.1l.3 21.1h-38.3z"/>
<path class="st10" d="M176.5 236.5l2.7 22.1-2.5-2.2zM139 256.4l-2.4 2.2 2.6-22.1zm.2-19.9l1.5 4.7-1.7 15.2z"/>
<path class="st10" d="M176.7 256.4l-1.7-15.2 1.5-4.7zm-33-18.7l-3 3.5-1.5-4.7z"/>
<path class="st10" d="M176.5 236.5l-1.5 4.7-3-3.5z"/>
<path class="st10" d="M172 237.7l-.3-4.5 4.8 3.3zm-32.8-1.2l4.8-3.3-.3 4.5z"/>
<path class="st10" d="M171.7 233.2l.3 4.5h-28.3z"/>
<path class="st10" d="M143.7 237.7l.3-4.5h27.7zm-3 3.5H175l1.7 15.2z"/>
<path class="st10" d="M176.7 256.4H139l1.7-15.2zm-36-15.2l3-3.5H172z"/>
<path class="st10" d="M172 237.7l3 3.5h-34.3z"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.3 KiB

@ -1,3 +0,0 @@
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.1603 6C10.7795 8.39146 7.72159 9.21084 5.33013 7.83013C2.93866 6.44942 2.11929 3.39146 3.5 1" stroke="#2F80ED" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 276 B

@ -1,3 +0,0 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 1.5H1.5V7.5H7.5V1.5ZM1.5 0H0V1.5V7.5V9H1.5H7.5H9V7.5V1.5V0H7.5H1.5ZM4.5 3H3V4.5V6H4.5H6V4.5V3H4.5ZM1.5 19.5V13.5H7.5V19.5H1.5ZM0 12H1.5H7.5H9V13.5V19.5V21H7.5H1.5H0V19.5V13.5V12ZM4.5 15H3V16.5V18H4.5H6V16.5V15H4.5ZM13.5 1.5H19.5V7.5H13.5V1.5ZM12 0H13.5H19.5H21V1.5V7.5V9H19.5H13.5H12V7.5V1.5V0ZM16.5 3H15V4.5V6H16.5H18V4.5V3H16.5ZM16.5 12H12V21H13.5V16.5H15V18H21V12H19.5V13.5H16.5V12ZM18 19.5H16.5V21H18V19.5ZM19.5 19.5H21V21H19.5V19.5Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 613 B

@ -0,0 +1,17 @@
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#fff">
<g fill="none" fill-rule="evenodd">
<g transform="translate(1 1)" stroke-width="2">
<circle stroke-opacity=".5" cx="18" cy="18" r="18"/>
<path d="M36 18c0-9.94-8.06-18-18-18">
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="1s"
repeatCount="indefinite"/>
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 694 B

@ -1,18 +0,0 @@
<svg width="288" height="288" viewBox="0 0 288 288" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>logo_solid_square_white</title>
<desc>Created using Figma</desc>
<g id="Canvas" transform="translate(60 145)">
<g id="logo_solid_square_white">
<g id="background">
<use xlink:href="#path0_fill" transform="translate(-60 -145)" fill="#3375BB"/>
</g>
<g id="logo">
<use xlink:href="#path1_stroke" transform="translate(11.2 -85.7)" fill="#FFFFFF"/>
</g>
</g>
</g>
<defs>
<path id="path0_fill" d="M 288 0L -1.2207e-05 0L -1.2207e-05 288L 288 288L 288 0Z"/>
<path id="path1_stroke" d="M 72.4 -7.62939e-07L 76.6355 -5.46695C 74.1422 -7.39862 70.6578 -7.39862 68.1645 -5.46695L 72.4 -7.62939e-07ZM 144.9 21.3L 151.815 21.4125C 151.845 19.5591 151.13 17.7711 149.83 16.4498C 148.53 15.1284 146.754 14.3843 144.9 14.3843L 144.9 21.3ZM 72.4 169.4L 68.5688 175.157C 70.8895 176.702 73.9105 176.702 76.2313 175.157L 72.4 169.4ZM 0 21.3L 0 14.3843C -1.85365 14.3843 -3.62972 15.1284 -4.92975 16.4498C -6.22977 17.7711 -6.94493 19.5591 -6.91479 21.4125L 0 21.3ZM 68.1645 5.46695C 99.5499 29.7826 135.462 28.2157 144.9 28.2157L 144.9 14.3843C 135.138 14.3843 103.85 15.6174 76.6355 -5.46695L 68.1645 5.46695ZM 137.985 21.1875C 137.464 53.3107 136.105 75.9691 133.501 92.7573C 130.933 109.367 127.26 119.322 122.533 126.314C 117.743 133.578 111.735 137.889 102.832 143.208C 93.8514 148.587 82.5075 154.381 68.5688 163.643L 76.2313 175.157C 89.4926 166.319 100.549 160.688 109.934 155.076C 119.396 149.405 127.719 143.159 133.992 134.061C 140.327 124.69 144.448 112.476 147.17 94.8708C 149.858 77.4434 151.286 53.8393 151.815 21.4125L 137.985 21.1875ZM 76.2313 163.643C 62.2932 154.381 50.957 148.588 41.9868 143.21C 33.0938 137.892 27.0986 133.583 22.3207 126.32C 17.6046 119.329 13.9431 109.373 11.3835 92.7609C 8.7891 75.9718 7.43568 53.3117 6.91479 21.1875L -6.91479 21.4125C -6.38568 53.8383 -4.96409 77.4407 -2.2866 94.8672C 0.425666 112.47 4.53296 124.684 10.8543 134.055C 17.1139 143.154 25.425 149.402 34.8789 155.075C 44.2556 160.687 55.3068 166.319 68.5688 175.157L 76.2313 163.643ZM 0 28.2157C 9.34002 28.2157 45.2509 29.782 76.6355 5.46695L 68.1645 -5.46695C 40.9491 15.618 9.65998 14.3843 0 14.3843L 0 28.2157Z"/>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

@ -1,47 +0,0 @@
@import '../../variables.scss';
.address-input-panel {
@extend %col-nowrap;
&__input {
font-size: 1rem;
outline: none;
border: none;
flex: 1 1 auto;
width: 0;
color: $royal-blue;
overflow: hidden;
text-overflow: ellipsis;
&::placeholder {
color: $chalice-gray;
}
&--error {
color: $salmon-red;
}
}
&__recipient-row {
display: flex;
justify-content: center;
align-items: center;
}
&__input-container {
flex: 1;
}
&__qr-container {
padding: 10px;
background: $concrete-gray;
border: 1px solid $mercury-gray;
border-radius: 10px;
margin-right: 16px;
height: 21px;
& > img {
height: 21px;
}
}
}

@ -1,13 +1,73 @@
import React, { useState, useEffect } from 'react'
import classnames from 'classnames'
import styled from 'styled-components'
import { useTranslation } from 'react-i18next'
import { useWeb3Context } from 'web3-react'
import { lighten } from 'polished'
import { isAddress } from '../../utils'
import { useDebounce } from '../../hooks'
// import QrCode from '../QrCode' // commented out pending further review
import './address-input-panel.scss'
const InputPanel = styled.div`
${({ theme }) => theme.flexColumnNoWrap}
box-shadow: 0 4px 8px 0 ${({ theme }) => lighten(0.9, theme.royalBlue)};
position: relative;
border-radius: 1.25rem;
background-color: ${({ theme }) => theme.white};
z-index: 1;
`
const ContainerRow = styled.div`
display: flex;
justify-content: center;
align-items: center;
border-radius: 1.25rem;
box-shadow: 0 0 0 0.5px ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)};
background-color: ${({ theme }) => theme.white};
transition: box-shadow 200ms ease-in-out;
`
const InputContainer = styled.div`
flex: 1;
`
const LabelRow = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
color: ${({ theme }) => theme.doveGray};
font-size: 0.75rem;
line-height: 1rem;
padding: 0.75rem 1rem;
`
const LabelContainer = styled.div`
flex: 1 1 auto;
width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`
const InputRow = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
padding: 0.25rem 0.85rem 0.75rem;
`
const Input = styled.input`
font-size: 1rem;
outline: none;
border: none;
flex: 1 1 auto;
width: 0;
color: ${({ error, theme }) => (error ? theme.salmonRed : theme.royalBlue)};
transition: color 200ms ease-in-out;
overflow: hidden;
text-overflow: ellipsis;
::placeholder {
color: ${({ theme }) => theme.chaliceGray};
}
`
export default function AddressInputPanel({ title, initialInput = '', onChange = () => {}, onError = () => {} }) {
const { t } = useTranslation()
@ -82,40 +142,29 @@ export default function AddressInputPanel({ title, initialInput = '', onChange =
}
return (
<div className="currency-input-panel">
<div
className={classnames('currency-input-panel__container address-input-panel__recipient-row', {
'currency-input-panel__container--error': input !== '' && error
})}
>
<div className="address-input-panel__input-container">
<div className="currency-input-panel__label-row">
<div className="currency-input-panel__label-container">
<span className="currency-input-panel__label">{title || t('recipientAddress')}</span>
</div>
</div>
<div className="currency-input-panel__input-row">
<input
<InputPanel>
<ContainerRow error={input !== '' && error}>
<InputContainer>
<LabelRow>
<LabelContainer>
<span>{title || t('recipientAddress')}</span>
</LabelContainer>
</LabelRow>
<InputRow>
<Input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
className={classnames('address-input-panel__input', {
'address-input-panel__input--error': input !== '' && error
})}
placeholder="0x1234..."
error={input !== '' && error}
onChange={onInput}
value={input}
/>
</div>
</div>
{/* commented out pending further review
<div className="address-input-panel__qr-container">
<QrCode onValueReceived={value => onChange(value)} />
</div>
*/}
</div>
</div>
</InputRow>
</InputContainer>
</ContainerRow>
</InputPanel>
)
}

@ -1,42 +0,0 @@
@import '../../variables.scss';
.contextual-info {
&__summary-wrapper {
color: $dove-gray;
font-size: 0.75rem;
text-align: center;
margin-top: 1rem;
padding-top: 1rem;
}
&--error {
color: $salmon-red;
}
&__details {
background-color: $concrete-gray;
padding: 1.5rem;
border-radius: 1rem;
font-size: 0.75rem;
margin-top: 1rem;
}
&__open-details-container {
cursor: pointer;
@extend %row-nowrap;
align-items: center;
justify-content: center;
font-size: 0.75rem;
color: $royal-blue;
span {
margin-right: 12px;
}
img {
height: 0.75rem;
width: 0.75rem;
}
}
}

@ -1,10 +1,38 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import c from 'classnames'
import styled from 'styled-components'
import DropdownBlue from '../../assets/images/dropdown-blue.svg'
import DropupBlue from '../../assets/images/dropup-blue.svg'
import './contextual-info.scss'
const SummaryWrapper = styled.div`
color: ${({ error, theme }) => (error ? theme.salmonRed : theme.doveGray)};
font-size: 0.75rem;
text-align: center;
margin-top: 1rem;
padding-top: 1rem;
`
const SummaryWrapperContainer = styled.div`
${({ theme }) => theme.flexRowNoWrap};
color: ${({ theme }) => theme.royalBlue};
text-align: center;
margin-top: 1rem;
padding-top: 1rem;
cursor: pointer;
align-items: center;
justify-content: center;
font-size: 0.75rem;
span {
margin-right: 12px;
}
img {
height: 0.75rem;
width: 0.75rem;
}
`
class ContextualInfo extends Component {
static propTypes = {
@ -38,18 +66,12 @@ class ContextualInfo extends Component {
const { openDetailsText, closeDetailsText, contextualInfo, isError } = this.props
if (contextualInfo) {
return (
<div className={c({ 'contextual-info--error': isError }, 'contextual-info__summary-wrapper')}>
<div>{contextualInfo}</div>
</div>
)
return <SummaryWrapper error={isError}>{contextualInfo}</SummaryWrapper>
}
return (
<>
<div
key="open-details"
className="contextual-info__summary-wrapper contextual-info__open-details-container"
<SummaryWrapperContainer
onClick={() =>
this.setState(prevState => {
return { showDetails: !prevState.showDetails }
@ -67,7 +89,7 @@ class ContextualInfo extends Component {
<img src={DropupBlue} alt="dropup" />
</>
)}
</div>
</SummaryWrapperContainer>
{this.renderDetails()}
</>
)

@ -1,42 +0,0 @@
@import '../../variables.scss';
.contextual-info {
&__summary-wrapper {
color: $dove-gray;
font-size: 0.75rem;
text-align: center;
margin-top: 1rem;
padding-top: 1rem;
}
&--error {
color: $salmon-red;
}
&__details {
background-color: $concrete-gray;
padding: 1.5rem;
border-radius: 1rem;
font-size: 0.75rem;
margin-top: 1rem;
}
&__open-details-container {
cursor: pointer;
@extend %row-nowrap;
align-items: center;
justify-content: center;
font-size: 0.75rem;
color: $royal-blue;
span {
margin-right: 12px;
}
img {
height: 0.75rem;
width: 0.75rem;
}
}
}

@ -1,23 +1,61 @@
import React, { useState } from 'react'
import styled from 'styled-components'
import c from 'classnames'
import { ReactComponent as Dropup } from '../../assets/images/dropup-blue.svg'
import { ReactComponent as Dropdown } from '../../assets/images/dropdown-blue.svg'
import './contextual-info.scss'
const SummaryWrapper = styled.div`
color: ${({ error, theme }) => (error ? theme.salmonRed : theme.doveGray)};
font-size: 0.75rem;
text-align: center;
margin-top: 1rem;
padding-top: 1rem;
`
const SummaryWrapperContainer = styled.div`
${({ theme }) => theme.flexRowNoWrap};
color: ${({ theme }) => theme.royalBlue};
text-align: center;
margin-top: 1rem;
padding-top: 1rem;
cursor: pointer;
align-items: center;
justify-content: center;
font-size: 0.75rem;
span {
margin-right: 12px;
}
img {
height: 0.75rem;
width: 0.75rem;
}
`
const Details = styled.div`
background-color: ${({ theme }) => theme.concreteGray};
padding: 1.5rem;
border-radius: 1rem;
font-size: 0.75rem;
margin-top: 1rem;
`
const ErrorSpan = styled.span`
color: ${({ isError, theme }) => isError && theme.salmonRed};
`
const WrappedDropup = ({ isError, ...rest }) => <Dropup {...rest} />
const ColoredDropup = styled(WrappedDropup)`
path {
stroke: ${props => props.isError && props.theme.salmonRed};
stroke: ${({ isError, theme }) => isError && theme.salmonRed};
}
`
const WrappedDropdown = ({ isError, ...rest }) => <Dropdown {...rest} />
const ColoredDropdown = styled(WrappedDropdown)`
path {
stroke: ${props => props.isError && props.theme.salmonRed};
stroke: ${({ isError, theme }) => isError && theme.salmonRed};
}
`
@ -32,24 +70,18 @@ export default function ContextualInfo({
const [showDetails, setShowDetails] = useState(false)
return !allowExpand ? (
<div className={c({ 'contextual-info--error': isError }, 'contextual-info__summary-wrapper')}>
<div>{contextualInfo}</div>
</div>
<SummaryWrapper>{contextualInfo}</SummaryWrapper>
) : (
<>
<div
key="open-details"
className="contextual-info__summary-wrapper contextual-info__open-details-container"
onClick={() => setShowDetails(s => !s)}
>
<SummaryWrapperContainer onClick={() => setShowDetails(s => !s)}>
<>
<span className={c({ 'contextual-info--error': isError })}>
<ErrorSpan isError={isError}>
{contextualInfo ? contextualInfo : showDetails ? closeDetailsText : openDetailsText}
</span>
</ErrorSpan>
{showDetails ? <ColoredDropup isError={isError} /> : <ColoredDropdown isError={isError} />}
</>
</div>
{showDetails && <div className="contextual-info__details">{renderTransactionDetails()}</div>}
</SummaryWrapperContainer>
{showDetails && <Details>{renderTransactionDetails()}</Details>}
</>
)
}

@ -1,283 +0,0 @@
@import '../../variables.scss';
.currency-input-panel {
@extend %col-nowrap;
box-shadow: 0 4px 8px 0 rgba($royal-blue, 0.1);
position: relative;
border-radius: 1.25rem;
z-index: 200;
&__container {
border-radius: 1.25rem;
box-shadow: 0 0 0 0.5px $mercury-gray;
background-color: $white;
transition: box-shadow 200ms ease-in-out;
&--error {
box-shadow: 0 0 0 0.5px $salmon-red;
}
&:focus-within {
box-shadow: 0 0 0.5px 0.5px $malibu-blue;
}
&--error:focus-within {
box-shadow: 0 0 0.5px 0.5px $salmon-red;
}
}
&__label-row {
@extend %row-nowrap;
align-items: center;
color: $dove-gray;
font-size: 0.75rem;
line-height: 1rem;
padding: 0.75rem 1rem;
}
&__label-container {
flex: 1 1 auto;
width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&__label-description {
opacity: 0.75;
margin-left: 0.25rem;
}
&__input-row {
@extend %row-nowrap;
align-items: center;
padding: 0.25rem 0.85rem 0.75rem;
}
&__input {
font-size: 1.5rem;
@extend %borderless-input;
&--error {
color: $salmon-red;
}
&[type='number'] {
-moz-appearance: textfield;
}
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
}
}
&__extra-text {
&--error {
color: $salmon-red;
}
}
&__currency-select {
@extend %row-nowrap;
align-items: center;
font-size: 1rem;
color: $royal-blue;
height: 2rem;
padding: 0 1rem;
border: 1px solid $royal-blue;
border-radius: 2.5rem;
background-color: $zumthor-blue;
outline: none;
cursor: pointer;
user-select: none;
&:active {
background-color: rgba($zumthor-blue, 0.8);
}
&:focus {
box-shadow: 0 0 0.5px 0.5px $malibu-blue;
}
&--selected {
background-color: $concrete-gray;
border-color: $mercury-gray;
color: $black;
padding: 0 0.5rem;
.currency-input-panel__dropdown-icon {
background-image: url(../../assets/images/dropdown.svg);
}
}
&--disabled {
.currency-input-panel__dropdown-icon {
opacity: 0;
}
}
}
&__sub-currency-select {
@extend %row-nowrap;
line-height: 0;
background: $zumthor-blue;
border: 1px solid $royal-blue;
color: $royal-blue;
height: 2rem;
padding: 10px 50px 10px 10px;
margin-right: -40px;
border-radius: 2.5rem;
outline: none;
cursor: pointer;
user-select: none;
&--pending {
line-height: 0.9;
.loader {
height: 0.5rem;
width: 0.5rem;
}
}
}
&__dropdown-icon {
height: 1rem;
width: 0.75rem;
margin-left: 0.7rem;
background-image: url(../../assets/images/dropdown-blue.svg);
background-repeat: no-repeat;
background-size: contain;
background-position: 50% 50%;
}
&__selected-token-logo {
margin-right: 0.4rem;
border-radius: 1rem;
object-fit: contain;
}
}
.token-modal {
background-color: $white;
position: relative;
bottom: 21rem;
width: 100%;
height: 21rem;
z-index: 2000;
border-top-left-radius: 1rem;
border-top-right-radius: 1rem;
transition: 250ms ease-in-out;
&__search-container {
@extend %row-nowrap;
padding: 1rem;
border-bottom: 1px solid $mercury-gray;
}
&__search-input {
@extend %borderless-input;
}
&__search-icon {
margin-right: 0.2rem;
}
&__token-list {
-webkit-overflow-scrolling: touch;
height: 17.5rem;
overflow-y: auto;
}
&__token-row {
@extend %row-nowrap;
align-items: center;
padding: 1rem 1.5rem;
margin: 0.25rem 0.5rem;
justify-content: space-between;
cursor: pointer;
user-select: none;
&--selected {
background-color: $concrete-gray;
box-sizing: border-box;
border: 1px solid $mercury-gray;
border-radius: 2.5rem;
.token-modal__token-label {
color: $black;
}
}
&--searching {
justify-content: center;
.loader {
margin-right: 1rem;
}
}
&:hover {
background-color: $concrete-gray;
.token-modal__token-label {
color: $black;
}
}
&:active {
background-color: darken($concrete-gray, 1);
}
&--no-exchange {
color: $dove-gray;
justify-content: center;
background-color: darken($concrete-gray, 1);
}
&--create-exchange {
color: $white;
justify-content: center;
background-color: $malibu-blue;
text-decoration: none;
&:hover {
background-color: lighten($malibu-blue, 1);
}
&:active {
background-color: darken($malibu-blue, 1);
}
}
}
&__token-logo {
object-fit: contain;
border-radius: 1rem;
}
&__token-label {
color: $dove-gray;
}
@media only screen and (min-width: 768px) {
max-width: 560px;
max-height: 768px;
position: absolute;
margin-left: auto;
margin-right: auto;
border-radius: 1rem;
padding-bottom: 1rem;
left: 0;
right: 0;
bottom: 0;
top: 0;
margin-top: 4rem;
}
}
.token-modal-appear {
bottom: 0;
}
.token-modal-appear.modal-container-appear-active {
bottom: 0;
}

@ -1,25 +1,196 @@
import React, { useState, useRef, useEffect, useMemo } from 'react'
import { CSSTransitionGroup } from 'react-transition-group'
import classnames from 'classnames'
import React, { useState, useRef, useMemo } from 'react'
import { Link } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { ethers } from 'ethers'
import styled from 'styled-components'
import escapeStringRegex from 'escape-string-regexp'
import { lighten, darken } from 'polished'
import Tooltip from '@reach/tooltip'
import '@reach/tooltip/styles.css'
import { BorderlessInput } from '../../theme'
import { useTokenContract } from '../../hooks'
import { isAddress, calculateGasMargin } from '../../utils'
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
import Modal from '../Modal'
import TokenLogo from '../TokenLogo'
import SearchIcon from '../../assets/images/magnifying-glass.svg'
import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions'
import { useTokenDetails, useAllTokenDetails } from '../../contexts/Tokens'
import './currency-panel.scss'
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
const SubCurrencySelect = styled.button`
${({ theme }) => theme.flexRowNoWrap}
background: ${({ theme }) => theme.zumthorBlue};
border: 1px solid ${({ theme }) => theme.royalBlue};
color: ${({ theme }) => theme.royalBlue};
line-height: 0;
height: 2rem;
padding: 10px 50px 10px 15px;
margin-right: -40px;
border-radius: 2.5rem;
outline: none;
cursor: pointer;
user-select: none;
`
const InputRow = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
padding: 0.25rem 0.85rem 0.75rem;
`
const Input = styled(BorderlessInput)`
font-size: 1.5rem;
color: ${({ error, theme }) => error && theme.salmonRed};
`
const StyledBorderlessInput = styled(BorderlessInput)`
min-height: 1.75rem;
flex-shrink: 0;
`
const CurrencySelect = styled.button`
align-items: center;
font-size: 1rem;
color: ${({ selected, theme }) => (selected ? theme.black : theme.royalBlue)};
height: 2rem;
border: 1px solid ${({ selected, theme }) => (selected ? theme.mercuryGray : theme.royalBlue)};
border-radius: 2.5rem;
background-color: ${({ selected, theme }) => (selected ? theme.concreteGray : theme.zumthorBlue)};
outline: none;
cursor: pointer;
user-select: none;
:hover {
border: 1px solid
${({ selected, theme }) => (selected ? darken(0.1, theme.mercuryGray) : darken(0.1, theme.royalBlue))};
}
:focus {
box-shadow: 0 0 0.5px 0.5px ${({ theme }) => theme.malibuBlue};
}
:active {
background-color: ${({ theme }) => theme.zumthorBlue};
}
`
const Aligner = styled.span`
display: flex;
align-items: center;
justify-content: space-between;
`
const StyledDropDown = styled(DropDown)`
margin: 0 0.5rem 0 0.5rem;
height: 35%;
path {
stroke: ${({ selected, theme }) => (selected ? theme.black : theme.royalBlue)};
}
`
const InputPanel = styled.div`
${({ theme }) => theme.flexColumnNoWrap}
box-shadow: 0 4px 8px 0 ${({ theme }) => lighten(0.9, theme.royalBlue)};
position: relative;
border-radius: 1.25rem;
background-color: ${({ theme }) => theme.white};
z-index: 1;
`
const Container = styled.div`
border-radius: 1.25rem;
box-shadow: 0 0 0 0.5px ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)};
background-color: ${({ theme }) => theme.white};
transition: box-shadow 200ms ease-in-out;
:focus-within {
box-shadow: 0 0 0.5px 0.5px ${({ theme }) => theme.malibuBlue};
}
`
const LabelRow = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
color: ${({ theme }) => theme.doveGray};
font-size: 0.75rem;
line-height: 1rem;
padding: 0.75rem 1rem;
span:hover {
cursor: pointer;
color: ${({ theme }) => darken(0.2, theme.doveGray)};
}
`
const LabelContainer = styled.div`
flex: 1 1 auto;
width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`
const ErrorSpan = styled.span`
color: ${({ error, theme }) => error && theme.salmonRed};
:hover {
cursor: pointer;
color: ${({ error, theme }) => error && darken(0.1, theme.salmonRed)};
}
`
const TokenModal = styled.div`
${({ theme }) => theme.flexColumnNoWrap}
background-color: ${({ theme }) => theme.white};
width: 100%;
`
const SearchContainer = styled.div`
${({ theme }) => theme.flexRowNoWrap}
padding: 1rem;
border-bottom: 1px solid ${({ theme }) => theme.mercuryGray};
`
const TokenModalInfo = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
padding: 1rem 1.5rem;
margin: 0.25rem 0.5rem;
justify-content: center;
user-select: none;
`
const TokenList = styled.div`
flex-grow: 1;
height: 100%;
overflow-y: auto;
`
const TokenModalRow = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
padding: 1rem 1.5rem;
margin: 0.25rem 0.5rem;
justify-content: space-between;
cursor: pointer;
user-select: none;
#symbol {
color: ${({ theme }) => theme.doveGrey};
}
:hover {
background-color: ${({ theme }) => theme.concreteGray};
}
`
const StyledTokenName = styled.span`
margin: 0 0.25rem 0 0.25rem;
`
export default function CurrencyInputPanel({
filteredTokens = [],
onValueChange = () => {},
renderInput,
onCurrencySelected = () => {},
@ -36,7 +207,7 @@ export default function CurrencyInputPanel({
}) {
const { t } = useTranslation()
const [isShowingModal, setIsShowingModal] = useState(false)
const [modalIsOpen, setModalIsOpen] = useState(false)
const tokenContract = useTokenContract(selectedTokenAddress)
const { exchangeAddress: selectedTokenExchangeAddress } = useTokenDetails(selectedTokenAddress)
@ -44,25 +215,16 @@ export default function CurrencyInputPanel({
const pendingApproval = usePendingApproval(selectedTokenAddress)
const addTransaction = useTransactionAdder()
const inputRef = useRef()
const allTokens = useAllTokenDetails()
// manage focus on modal show
useEffect(() => {
if (inputRef.current && isShowingModal) {
inputRef.current.focus()
}
}, [isShowingModal])
function renderUnlockButton() {
if (disableUnlock || !showUnlock || selectedTokenAddress === 'ETH' || !selectedTokenAddress) {
return null
} else {
if (!pendingApproval) {
return (
<button
className="currency-input-panel__sub-currency-select"
<SubCurrencySelect
onClick={async () => {
const estimatedGas = await tokenContract.estimate.approve(
selectedTokenExchangeAddress,
@ -79,15 +241,10 @@ export default function CurrencyInputPanel({
}}
>
{t('unlock')}
</button>
</SubCurrencySelect>
)
} else {
return (
<button className="currency-input-panel__sub-currency-select currency-input-panel__sub-currency-select--pending">
<div className="loader" />
{t('pending')}
</button>
)
return <SubCurrencySelect>{t('pending')}</SubCurrencySelect>
}
}
}
@ -98,13 +255,11 @@ export default function CurrencyInputPanel({
}
return (
<div className="currency-input-panel__input-row">
<input
<InputRow>
<Input
type="number"
min="0"
className={classnames('currency-input-panel__input', {
'currency-input-panel__input--error': errorMessage
})}
error={!!errorMessage}
placeholder="0.0"
onChange={e => onValueChange(e.target.value)}
onKeyPress={e => {
@ -119,65 +274,74 @@ export default function CurrencyInputPanel({
value={value}
/>
{renderUnlockButton()}
<button
className={classnames('currency-input-panel__currency-select', {
'currency-input-panel__currency-select--selected': selectedTokenAddress,
'currency-input-panel__currency-select--disabled': disableTokenSelect
})}
<CurrencySelect
selected={!!selectedTokenAddress}
onClick={() => {
if (!disableTokenSelect) {
setIsShowingModal(true)
setModalIsOpen(true)
}
}}
>
{selectedTokenAddress ? (
<TokenLogo className="currency-input-panel__selected-token-logo" address={selectedTokenAddress} />
) : null}
{(allTokens[selectedTokenAddress] && allTokens[selectedTokenAddress].symbol) || t('selectToken')}
<span className="currency-input-panel__dropdown-icon" />
</button>
</div>
<Aligner>
{selectedTokenAddress ? <TokenLogo address={selectedTokenAddress} /> : null}
{
<StyledTokenName>
{(allTokens[selectedTokenAddress] && allTokens[selectedTokenAddress].symbol) || t('selectToken')}
</StyledTokenName>
}
{!disableTokenSelect && <StyledDropDown selected={!!selectedTokenAddress} />}
</Aligner>
</CurrencySelect>
</InputRow>
)
}
return (
<div className="currency-input-panel">
<div
className={classnames('currency-input-panel__container', {
'currency-input-panel__container--error': errorMessage
})}
>
<div className="currency-input-panel__label-row">
<div className="currency-input-panel__label-container">
<span className="currency-input-panel__label">{title}</span>
<span className="currency-input-panel__label-description">{description}</span>
</div>
<span
className={classnames('currency-input-panel__extra-text', {
'currency-input-panel__extra-text--error': errorMessage
})}
<InputPanel>
<Container error={!!errorMessage}>
<LabelRow>
<LabelContainer>
<span>{title}</span> <span>{description}</span>
</LabelContainer>
<ErrorSpan
data-tip={'Enter max'}
error={!!errorMessage}
onClick={() => {
extraTextClickHander()
}}
>
{extraText}
</span>
</div>
<Tooltip
label="Enter Max"
style={{
background: 'hsla(0, 0%, 0%, 0.75)',
color: 'white',
border: 'none',
borderRadius: '24px',
padding: '0.5em 1em',
marginTop: '-64px'
}}
>
<span>{extraText}</span>
</Tooltip>
</ErrorSpan>
</LabelRow>
{_renderInput()}
</div>
{!disableTokenSelect && isShowingModal && (
</Container>
{!disableTokenSelect && (
<CurrencySelectModal
onTokenSelect={onCurrencySelected}
onClose={() => {
setIsShowingModal(false)
isOpen={modalIsOpen}
onDismiss={() => {
setModalIsOpen(false)
}}
onTokenSelect={onCurrencySelected}
/>
)}
</div>
</InputPanel>
)
}
function CurrencySelectModal({ onClose, onTokenSelect }) {
function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
const { t } = useTranslation()
const [searchQuery, setSearchQuery] = useState('')
@ -187,10 +351,10 @@ function CurrencySelectModal({ onClose, onTokenSelect }) {
const tokenList = useMemo(() => {
return Object.keys(allTokens)
.sort((a, b) => {
const aSymbol = allTokens[a].symbol
const bSymbol = allTokens[b].symbol
if (aSymbol === 'ETH' || bSymbol === 'ETH') {
return aSymbol === bSymbol ? 0 : aSymbol === 'ETH' ? -1 : 1
const aSymbol = allTokens[a].symbol.toLowerCase()
const bSymbol = allTokens[b].symbol.toLowerCase()
if (aSymbol === 'ETH'.toLowerCase() || bSymbol === 'ETH'.toLowerCase()) {
return aSymbol === bSymbol ? 0 : aSymbol === 'ETH'.toLowerCase() ? -1 : 1
} else {
return aSymbol < bSymbol ? -1 : aSymbol > bSymbol ? 1 : 0
}
@ -220,66 +384,41 @@ function CurrencySelectModal({ onClose, onTokenSelect }) {
function _onTokenSelect(address) {
setSearchQuery('')
onTokenSelect(address)
onClose()
}
function _onClose(address) {
setSearchQuery('')
onClose()
onDismiss()
}
function renderTokenList() {
if (isAddress(searchQuery) && exchangeAddress === undefined) {
return (
<div className="token-modal__token-row token-modal__token-row--searching">
<div className="loader" />
<div>Searching for Exchange...</div>
</div>
)
return <TokenModalInfo>Searching for Exchange...</TokenModalInfo>
}
if (isAddress(searchQuery) && exchangeAddress === ethers.constants.AddressZero) {
return (
<>
<div className="token-modal__token-row token-modal__token-row--no-exchange">
<div>{t('noExchange')}</div>
</div>
<Link
to={`/create-exchange/${searchQuery}`}
className="token-modal__token-row token-modal__token-row--create-exchange"
onClick={onClose}
>
<div>{t('createExchange')}</div>
</Link>
<TokenModalInfo>{t('noExchange')}</TokenModalInfo>
<TokenModalInfo>
<Link to={`/create-exchange/${searchQuery}`}>{t('createExchange')}</Link>
</TokenModalInfo>
</>
)
}
if (!filteredTokenList.length) {
return (
<div className="token-modal__token-row token-modal__token-row--no-exchange">
<div>{t('noExchange')}</div>
</div>
)
return <TokenModalInfo>{t('noExchange')}</TokenModalInfo>
}
return filteredTokenList.map(({ address, symbol }) => {
return (
<div key={address} className="token-modal__token-row" onClick={() => _onTokenSelect(address)}>
<TokenLogo className="token-modal__token-logo" address={address} />
<div className="token-modal__token-label">{symbol}</div>
</div>
<TokenModalRow key={address} onClick={() => _onTokenSelect(address)}>
<TokenLogo address={address} />
<span id="symbol">{symbol}</span>
</TokenModalRow>
)
})
}
// manage focus on modal show
const inputRef = useRef()
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
}
}, [])
function onInput(event) {
const input = event.target.value
@ -288,29 +427,14 @@ function CurrencySelectModal({ onClose, onTokenSelect }) {
}
return (
<Modal onClose={_onClose}>
<CSSTransitionGroup
transitionName="token-modal"
transitionAppear={true}
transitionLeave={true}
transitionAppearTimeout={200}
transitionLeaveTimeout={200}
transitionEnterTimeout={200}
>
<div className="token-modal">
<div className="token-modal__search-container">
<input
ref={inputRef}
type="text"
placeholder={t('searchOrPaste')}
className="token-modal__search-input"
onChange={onInput}
/>
<img src={SearchIcon} className="token-modal__search-icon" alt="search" />
</div>
<div className="token-modal__token-list">{renderTokenList()}</div>
</div>
</CSSTransitionGroup>
<Modal isOpen={isOpen} onDismiss={onDismiss} initialFocusRef={inputRef}>
<TokenModal>
<SearchContainer>
<StyledBorderlessInput ref={inputRef} type="text" placeholder={t('searchOrPaste')} onChange={onInput} />
<img src={SearchIcon} alt="search" />
</SearchContainer>
<TokenList>{renderTokenList()}</TokenList>
</TokenModal>
</Modal>
)
}

@ -1,83 +0,0 @@
@import '../../variables.scss';
.header {
@extend %col-nowrap;
display: block;
&__no-decoration {
text-decoration: none;
}
&__top {
@extend %row-nowrap;
padding: 1.25rem 0.75rem;
align-items: center;
border-bottom: 1px solid $concrete-gray;
}
&__title {
font-size: 1rem;
font-weight: 500;
margin-left: 1rem;
line-height: 1.5rem;
color: $wisteria-purple;
}
&__center-group {
flex: 1 1 auto;
width: 0;
}
&__navigation {
margin-bottom: 1rem;
}
&__dialog {
@extend %col-nowrap;
border-radius: 0.875rem;
border: 1px solid $mercury-gray;
margin: 1rem 0.75rem 0 0.75rem;
padding: 1.5rem 1rem;
text-align: center;
display: none;
&__description {
font-size: 0.75rem;
color: $dove-gray;
margin-top: 0.4rem;
}
&--disconnected {
display: block;
}
}
&__download {
@extend %row-nowrap;
justify-content: center;
margin-top: 1rem;
img {
height: 3.5rem;
width: 3.5rem;
border-radius: 50%;
cursor: pointer;
}
img + img {
margin-left: 1rem;
}
}
@media only screen and (min-width: 768px) {
//position: fixed;
top: 0px;
left: 0px;
flex: 0 0 auto;
&__top {
border: none;
padding: 1.25rem;
}
}
}

@ -1,174 +1,58 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import classnames from 'classnames'
import { useWeb3Context, Connectors } from 'web3-react'
import UAParser from 'ua-parser-js'
import styled from 'styled-components'
import Logo from '../Logo'
import CoinbaseWalletLogo from '../../assets/images/coinbase-wallet-logo.png'
import TrustLogo from '../../assets/images/trust-wallet-logo.svg'
import BraveLogo from '../../assets/images/brave-logo.svg'
import MetamaskLogo from '../../assets/images/metamask-logo.svg'
import { Link } from '../../theme'
import Web3Status from '../Web3Status'
import { darken } from 'polished'
import './header.scss'
const HeaderElement = styled.div`
margin: 1.25rem;
display: flex;
min-width: 0;
`
const { Connector, InjectedConnector } = Connectors
const Title = styled.div`
display: flex;
align-items: center;
const links = {
coinbaseWallet: {
android: 'https://play.google.com/store/apps/details?id=org.toshi',
ios: 'https://itunes.apple.com/us/app/coinbase-wallet/id1278383455'
},
trust: {
android:
'https://links.trustwalletapp.com/a/key_live_lfvIpVeI9TFWxPCqwU8rZnogFqhnzs4D?&event=openURL&url=https://uniswap.exchange/swap',
ios:
'https://links.trustwalletapp.com/a/key_live_lfvIpVeI9TFWxPCqwU8rZnogFqhnzs4D?&event=openURL&url=https://uniswap.exchange/swap'
},
metamask: {
chrome: 'https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn'
},
brave: {
android: 'https://play.google.com/store/apps/details?id=com.brave.browser',
ios: 'https://itunes.apple.com/us/app/brave-browser-fast-adblocker/id1052879175'
}
}
function getTrustLink() {
const os = ua.getOS()
if (os.name === 'Android') {
return links.trust.android
#image {
font-size: 1.5rem;
margin-right: 1rem;
}
if (os.name === 'iOS') {
return links.trust.ios
}
}
function getCoinbaseWalletLink() {
const os = ua.getOS()
if (os.name === 'Android') {
return links.coinbaseWallet.android
#link {
text-decoration-color: ${({ theme }) => theme.wisteriaPurple};
}
if (os.name === 'iOS') {
return links.coinbaseWallet.ios
#title {
display: inline;
font-size: 1rem;
font-weight: 500;
color: ${({ theme }) => theme.wisteriaPurple};
:hover {
color: ${({ theme }) => darken(0.2, theme.wisteriaPurple)};
}
}
}
function getBraveLink() {
const os = ua.getOS()
if (os.name === 'Mac OS') {
return links.brave.ios
}
return links.brave.android
}
function getMetamaskLink() {
return links.metamask.chrome
}
const ua = new UAParser(window.navigator.userAgent)
function isMobile() {
return ua.getDevice().type === 'mobile'
}
function BaseBlockingWarning({ title, description, children }) {
return (
<div
className={classnames('header__dialog', {
'header__dialog--disconnected': true
})}
>
<div key="warning-title">{title}</div>
<div key="warning-desc" className="header__dialog__description">
{description}
</div>
{children}
</div>
)
}
function BlockingWarning() {
const { t } = useTranslation()
const correctNetwork = process.env.REACT_APP_NETWORK_NAME || 'Main Ethereum Network'
const context = useWeb3Context()
if (context.error && context.error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
return <BaseBlockingWarning title={t('wrongNetwork')} description={t('switchNetwork', { correctNetwork })} />
}
// this is an intermediate state before infura is set
if (context.error && context.error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) {
return null
}
if (context.error) {
console.error(context.error)
return <BaseBlockingWarning title={t('disconnected')} />
}
if (!context.account) {
return (
<BaseBlockingWarning
title={t('noWallet')}
description={isMobile() ? t('installWeb3MobileBrowser') : t('installMetamask')}
>
<div key="warning-logos" className="header__download">
{isMobile() ? (
<>
<img
alt="coinbase"
src={CoinbaseWalletLogo}
key="coinbase-wallet"
onClick={() => window.open(getCoinbaseWalletLink(), '_blank')}
/>
<img alt="trust" src={TrustLogo} key="trust" onClick={() => window.open(getTrustLink(), '_blank')} />
</>
) : (
<>
<img
alt="metamask"
src={MetamaskLogo}
key="metamask"
onClick={() => window.open(getMetamaskLink(), '_blank')}
/>
<img alt="brave" src={BraveLogo} key="brave" onClick={() => window.open(getBraveLink(), '_blank')} />
</>
)}
</div>
</BaseBlockingWarning>
)
}
return null
}
`
export default function Header() {
const context = useWeb3Context()
return (
<div className="header">
<BlockingWarning />
<div className={classnames('header__top')}>
<a className="header__no-decoration" href="https://uniswap.io" target="_blank" rel="noopener noreferrer">
<Logo />
</a>
<>
<HeaderElement>
<Title>
<span id="image" role="img" aria-label="Unicorn Emoji">
🦄
</span>
<div className="header__center-group">
<a className="header__no-decoration" href="https://uniswap.io" target="_blank" rel="noopener noreferrer">
<span className="header__title">Uniswap</span>
</a>
</div>
<Link id="link" href="https://uniswap.io">
<h1 id="title">Uniswap</h1>
</Link>
</Title>
</HeaderElement>
<Web3Status isConnected={!!(context.active && context.account)} />
</div>
</div>
<HeaderElement>
<Web3Status />
</HeaderElement>
</>
)
}

@ -1,12 +0,0 @@
import React from 'react'
import './logo.scss'
export default function Logo(props) {
return (
<div className="logo">
<span role="img" aria-label="logo">
🦄
</span>
</div>
)
}

@ -1,4 +0,0 @@
.logo {
font-size: 1.5rem;
line-height: 1.75rem;
}

@ -1,52 +1,70 @@
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
import { CSSTransitionGroup } from 'react-transition-group'
import './modal.scss'
import React from 'react'
import styled, { css } from 'styled-components'
import { animated, useTransition } from 'react-spring'
import { DialogOverlay, DialogContent } from '@reach/dialog'
import '@reach/dialog/styles.css'
const modalRoot = document.querySelector('#modal-root')
export default class Modal extends Component {
static propTypes = {
onClose: PropTypes.func.isRequired
const AnimatedDialogOverlay = animated(DialogOverlay)
const StyledDialogOverlay = styled(AnimatedDialogOverlay).attrs({
suppressClassNameWarning: true
})`
&[data-reach-dialog-overlay] {
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
}
`
componentDidMount() {
// The portal element is inserted in the DOM tree after
// the Modal's children are mounted, meaning that children
// will be mounted on a detached DOM node. If a child
// component requires to be attached to the DOM tree
// immediately when mounted, for example to measure a
// DOM node, or uses 'autoFocus' in a descendant, add
// state to Modal and only render the children when Modal
// is inserted in the DOM tree.
// modalRoot.style.display = 'block';
// modalRoot.appendChild(this.el);
const FilteredDialogContent = ({ minHeight, ...rest }) => <DialogContent {...rest} />
const StyledDialogContent = styled(FilteredDialogContent)`
&[data-reach-dialog-content] {
margin: 0 0 2rem 0;
${({ theme }) => theme.mediaWidth.upToMedium`margin: 0;`}
padding: 0;
width: 50vw;
max-width: 650px;
${({ theme }) => theme.mediaWidth.upToMedium`width: 75vw;`}
${({ theme }) => theme.mediaWidth.upToSmall`width: 90vw;`}
${({ minHeight }) =>
minHeight &&
css`
min-height: ${minHeight}vh;
`}
max-height: 50vh;
${({ theme }) => theme.mediaHeight.upToMedium`max-height: 75vh;`}
${({ theme }) => theme.mediaHeight.upToSmall`max-height: 90vh;`}
display: flex;
overflow: hidden;
border-radius: 1.5rem;
}
`
componentWillUnmount() {
setTimeout(() => {
// modalRoot.style.display = 'none';
// modalRoot.removeChild(this.el);
}, 500)
}
const HiddenCloseButton = styled.button`
margin: 0;
padding: 0;
width: 0;
height: 0;
border: none;
`
render() {
return ReactDOM.createPortal(
<div>
<CSSTransitionGroup
transitionName="modal-container"
transitionAppear={true}
transitionLeave={true}
transitionAppearTimeout={200}
transitionLeaveTimeout={200}
transitionEnterTimeout={200}
>
<div className="modal-container" onClick={this.props.onClose} key="modal" />
</CSSTransitionGroup>
{this.props.children}
</div>,
modalRoot
)
}
export default function Modal({ isOpen, onDismiss, minHeight = 50, initialFocusRef, children }) {
const transitions = useTransition(isOpen, null, {
config: { duration: 125 },
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
})
return transitions.map(
({ item, key, props }) =>
item && (
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
<StyledDialogContent hidden={true} minHeight={minHeight}>
<HiddenCloseButton onClick={onDismiss} />
{children}
</StyledDialogContent>
</StyledDialogOverlay>
)
)
}

@ -1,18 +0,0 @@
@import '../../variables.scss';
.modal-container {
position: relative;
height: 100vh;
width: 100vw;
background-color: rgba($black, 0.6);
z-index: 1000;
}
.modal-container-appear {
opacity: 0.01;
}
.modal-container-appear.modal-container-appear-active {
opacity: 1;
transition: opacity 200ms ease-in-out;
}

@ -1,10 +1,10 @@
import React, { useCallback } from 'react'
import { withRouter, NavLink } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { transparentize, darken } from 'polished'
import { useBodyKeyDown } from '../../hooks'
import './navigation-tabs.scss'
import { useBetaMessageManager } from '../../contexts/Application'
const tabOrder = [
@ -25,6 +25,84 @@ const tabOrder = [
}
]
const BetaMessage = styled.div`
${({ theme }) => theme.flexRowNoWrap}
cursor: pointer;
flex: 1 0 auto;
align-items: center;
position: relative;
padding: 0.5rem 1rem;
padding-right: 2rem;
margin-bottom: 1rem;
border: 1px solid ${({ theme }) => transparentize(0.6, theme.wisteriaPurple)};
background-color: ${({ theme }) => transparentize(0.9, theme.wisteriaPurple)};
border-radius: 2rem;
font-size: 0.75rem;
line-height: 1rem;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: ${({ theme }) => theme.wisteriaPurple};
&:after {
content: '✕';
top: 0.5rem;
right: 1rem;
position: absolute;
color: ${({ theme }) => theme.wisteriaPurple};
}
`
const Tabs = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
height: 2.5rem;
background-color: ${({ theme }) => theme.concreteGray};
border-radius: 3rem;
box-shadow: 0 0 0 1px ${({ theme }) => darken(0.05, theme.concreteGray)};
margin-bottom: 1rem;
`
const activeClassName = 'ACTIVE'
const StyledNavLink = styled(NavLink).attrs({
activeClassName
})`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
justify-content: center;
height: 2.5rem;
flex: 1 0 auto;
border-radius: 3rem;
outline: none;
cursor: pointer;
text-decoration: none;
color: ${({ theme }) => theme.doveGray};
font-size: 1rem;
&.${activeClassName} {
background-color: ${({ theme }) => theme.white};
border-radius: 3rem;
box-shadow: 0 0 0.5px 1px ${({ theme }) => theme.mercuryGray};
font-weight: 500;
color: ${({ theme }) => theme.royalBlue};
:hover {
box-shadow: 0 0 0.5px 1px ${({ theme }) => darken(0.1, theme.mercuryGray)};
}
}
:hover,
:focus {
color: ${({ theme }) => darken(0.1, theme.royalBlue)};
/* box-shadow: 0 0 0.5px 0.5px ${({ theme }) => darken(0.2, theme.mercuryGray)}; */
}
:focus {
text-decoration: underline;
}
`
function NavigationTabs({ location: { pathname }, history }) {
const { t } = useTranslation()
@ -49,26 +127,20 @@ function NavigationTabs({ location: { pathname }, history }) {
return (
<>
<div className="tabs">
<Tabs>
{tabOrder.map(({ path, textKey, regex }) => (
<NavLink
key={path}
to={path}
className="tab"
activeClassName="tab--selected"
isActive={(_, { pathname }) => pathname.match(regex)}
>
<StyledNavLink key={path} to={path} isActive={(_, { pathname }) => pathname.match(regex)}>
{t(textKey)}
</NavLink>
</StyledNavLink>
))}
</div>
</Tabs>
{showBetaMessage && (
<div className="beta-message" onClick={dismissBetaMessage}>
<BetaMessage onClick={dismissBetaMessage}>
<span role="img" aria-label="warning">
💀
</span>{' '}
{t('betaWarning')}
</div>
</BetaMessage>
)}
</>
)

@ -1,58 +0,0 @@
@import '../../variables';
.beta-message {
@extend %row-nowrap;
cursor: pointer;
flex: 1 0 auto;
align-items: center;
position: relative;
padding: 0.5rem 1rem;
margin-bottom: 1rem;
border: 1px solid rgba($wisteria-purple, 0.4);
background-color: rgba($wisteria-purple, 0.1);
border-radius: 2rem;
font-size: 0.75rem;
line-height: 1rem;
text-align: center;
color: $wisteria-purple;
&:after {
content: '';
top: 0.5rem;
right: 1rem;
position: absolute;
color: $wisteria-purple;
}
}
.tabs {
@extend %row-nowrap;
align-items: center;
height: 2.5rem;
background-color: $concrete-gray;
border-radius: 3rem;
box-shadow: 0 0 0 0.5px darken($concrete-gray, 5);
margin-bottom: 1rem;
}
.tab {
@extend %row-nowrap;
align-items: center;
justify-content: center;
height: 2.5rem;
flex: 1 0 auto;
border-radius: 3rem;
cursor: pointer;
text-decoration: none;
color: $dove-gray;
font-size: 1rem;
&--selected {
background-color: $white;
border-radius: 3rem;
box-shadow: 0 0 0.5px 0.5px $mercury-gray;
font-weight: 500;
color: $royal-blue;
}
}

@ -1,13 +1,39 @@
import React from 'react'
import styled from 'styled-components'
import './oversized-panel.scss'
const Panel = styled.div`
position: relative;
background-color: ${({ theme }) => theme.concreteGray};
width: calc(100% - 1rem);
margin: 0 auto;
border-radius: 0.625rem;
`
export default function OversizedPanel(props) {
const PanelTop = styled.div`
content: '';
position: absolute;
top: -0.5rem;
left: 0;
height: 1rem;
width: 100%;
background-color: ${({ theme }) => theme.concreteGray};
`
const PanelBottom = styled.div`
position: absolute;
top: 80%;
left: 0;
height: 1rem;
width: 100%;
background-color: ${({ theme }) => theme.concreteGray};
`
export default function OversizedPanel({ hideTop, hideBottom, children }) {
return (
<div className="oversized-panel">
{props.hideTop || <div className="oversized-panel__top" />}
{props.children}
{props.hideBottom || <div className="oversized-panel__bottom" />}
</div>
<Panel>
{hideTop || <PanelTop />}
{children}
{hideBottom || <PanelBottom />}
</Panel>
)
}

@ -1,30 +0,0 @@
@import '../../variables.scss';
.oversized-panel {
position: relative;
background-color: $concrete-gray;
width: calc(100% - 1rem);
margin: 0 auto;
border-radius: 0.625rem;
&__top {
content: '';
position: absolute;
top: -0.5rem;
left: 0;
height: 1rem;
width: 100%;
background-color: $concrete-gray;
z-index: 100;
}
&__bottom {
position: absolute;
top: 80%;
left: 0;
height: 1rem;
width: 100%;
background-color: $concrete-gray;
z-index: 100;
}
}

@ -1,109 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { CSSTransitionGroup } from 'react-transition-group'
import Modal from '../Modal'
import QrCodeSVG from '../../assets/images/qr-code.svg'
import QrScanner from '../../libraries/qr-scanner'
import './qr-code.scss'
class QrCode extends Component {
static propTypes = {
onValueReceived: PropTypes.func
}
static defaultProps = {
onValueReceived() {}
}
state = {
videoOpen: false,
stream: null
}
componentDidUpdate() {
const { videoOpen, stream } = this.state
if (videoOpen && !stream && this.videoRef) {
this.startStreamingVideo(this.videoRef)
} else if (!videoOpen && stream) {
this.setState({ stream: null })
}
}
startStreamingVideo(videoRef) {
if (navigator && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices
.getUserMedia({ video: { facingMode: 'user' }, audio: false })
.then(stream => {
videoRef.srcObject = stream
new QrScanner(videoRef, val => {
this.closeVideo()
this.props.onValueReceived(val)
})
this.setState({
stream: stream.getTracks()[0]
})
})
.catch(error => {
this.closeVideo()
console.error(error)
})
}
}
openVideo = () => {
this.setState({ videoOpen: true })
}
closeVideo = () => {
if (this.state.stream) {
this.state.stream.stop()
}
this.setState({ videoOpen: false, stream: null })
this.videoRef = null
}
setVideoRef = element => {
this.videoRef = element
}
renderQrReader() {
if (this.state.videoOpen) {
return (
<Modal key="modal" onClose={this.closeVideo}>
<CSSTransitionGroup
transitionName="qr-modal"
transitionAppear={true}
transitionLeave={true}
transitionAppearTimeout={200}
transitionLeaveTimeout={200}
transitionEnterTimeout={200}
>
<div className="qr-code__modal">
<video playsInline muted autoPlay ref={this.setVideoRef} className="qr-code__video" />
</div>
</CSSTransitionGroup>
</Modal>
)
}
return null
}
render() {
return [
<img
key="icon"
src={QrCodeSVG}
alt="code"
onClick={() => {
this.state.videoOpen ? this.closeVideo() : this.openVideo()
}}
/>,
this.renderQrReader()
]
}
}
export default QrCode

@ -1,15 +0,0 @@
.qr-code {
&__video {
height: 100%;
width: 100%;
}
&__modal {
z-index: 2000;
position: absolute;
top: 100px;
bottom: 100px;
right: 100px;
left: 100px;
}
}

@ -1,72 +1,55 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import EthereumLogo from '../../assets/images/ethereum-logo.svg'
import React, { useState } from 'react'
import styled from 'styled-components'
const RINKEBY_TOKEN_MAP = {
'0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B': '0x0d8775f648430679a709e98d2b0cb6250d2887ef',
'0x2448eE2641d78CC42D7AD76498917359D961A783': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
'0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85': '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2',
'0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0': '0xd26114cd6ee289accf82350c8d8487fedb8a0c07',
'0xF22e3F33768354c9805d046af3C0926f27741B43': '0xe41d2489571d322189246dafa5ebde1f4699f498'
}
import { ReactComponent as EthereumLogo } from '../../assets/images/ethereum-logo.svg'
const TOKEN_ICON_API = 'https://raw.githubusercontent.com/TrustWallet/tokens/master/tokens'
const BAD_IMAGES = {}
export default class TokenLogo extends Component {
static propTypes = {
address: PropTypes.string,
size: PropTypes.string,
className: PropTypes.string
}
static defaultProps = {
address: '',
size: '1rem',
className: ''
}
const Image = styled.img`
width: ${({ size }) => size};
height: ${({ size }) => size};
border-radius: 1rem;
`
state = {
error: false
}
const Emoji = styled.span`
width: ${({ size }) => size};
font-size: ${({ size }) => size};
`
render() {
const { address, size, className } = this.props
// let path = GenericTokenLogo;
let path = ''
const mainAddress = RINKEBY_TOKEN_MAP[address] ? RINKEBY_TOKEN_MAP[address] : address
const StyledEthereumLogo = styled(EthereumLogo)`
width: ${({ size }) => size};
height: ${({ size }) => size};
`
if (mainAddress === 'ETH') {
path = EthereumLogo
}
if (!this.state.error && !BAD_IMAGES[mainAddress] && mainAddress !== 'ETH') {
path = `${TOKEN_ICON_API}/${mainAddress.toLowerCase()}.png`
}
if (!path) {
return (
<div className={className} style={{ width: size, fontSize: size }}>
<span role="img" aria-label="thinking">
🤔
</span>
</div>
)
}
export default function TokenLogo({ address, size = '1rem', ...rest }) {
const [error, setError] = useState(false)
let path = ''
if (address === 'ETH') {
return <StyledEthereumLogo size={size} />
} else if (!error && !BAD_IMAGES[address]) {
path = `${TOKEN_ICON_API}/${address.toLowerCase()}.png`
} else {
return (
<img
alt="images"
src={path}
className={className}
style={{
width: size,
height: size
}}
onError={() => {
this.setState({ error: true })
BAD_IMAGES[mainAddress] = true
}}
/>
<Emoji {...rest}>
<span role="img" aria-label="Thinking">
🤔
</span>
</Emoji>
)
}
return (
<Image
{...rest}
alt={address}
src={path}
size={size}
onError={() => {
BAD_IMAGES[address] = true
setError(true)
}}
/>
)
}

@ -0,0 +1,44 @@
import React from 'react'
import styled from 'styled-components'
import { useCopyClipboard } from '../../hooks'
import { Link } from '../../theme'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCopy, faCheckCircle } from '@fortawesome/free-regular-svg-icons'
const CopyIcon = styled(Link)`
color: ${({ theme }) => theme.silverGray};
flex-shrink: 0;
margin-right: 1rem;
margin-left: 0.5rem;
text-decoration: none;
:hover,
:active,
:focus {
text-decoration: none;
color: ${({ theme }) => theme.doveGray};
}
`
const TransactionStatusText = styled.span`
margin-left: 0.25rem;
`
export default function Copy({ toCopy }) {
const [isCopied, setCopied] = useCopyClipboard()
return (
<CopyIcon onClick={() => setCopied(toCopy)}>
{isCopied ? (
<TransactionStatusText>
<FontAwesomeIcon icon={faCheckCircle} />
<TransactionStatusText>Copied</TransactionStatusText>
</TransactionStatusText>
) : (
<TransactionStatusText>
<FontAwesomeIcon icon={faCopy} />
</TransactionStatusText>
)}
</CopyIcon>
)
}

@ -0,0 +1,136 @@
import React from 'react'
import styled, { keyframes } from 'styled-components'
import { useWeb3Context } from 'web3-react'
import { useCopyClipboard } from '../../hooks'
import { getEtherscanLink } from '../../utils'
import { Link } from '../../theme'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCopy, faCheckCircle } from '@fortawesome/free-regular-svg-icons'
import { faCircleNotch, faCheck } from '@fortawesome/free-solid-svg-icons'
import { transparentize } from 'polished'
const CopyIcon = styled(Link)`
color: ${({ theme }) => theme.silverGray};
flex-shrink: 0;
margin-right: 1rem;
margin-left: 0.5rem;
text-decoration: none;
:hover,
:active,
:focus {
text-decoration: none;
color: ${({ theme }) => theme.doveGray};
}
`
const TransactionStatusWrapper = styled.div`
display: flex;
align-items: center;
min-width: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`
const TransactionWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap}
justify-content: space-between;
width: 100%;
margin-top: 0.75rem;
a {
/* flex: 1 1 auto; */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
max-width: 250px;
}
`
const TransactionStatusText = styled.span`
margin-left: 0.25rem;
`
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
const TransactionState = styled.div`
background-color: ${({ pending, theme }) =>
pending ? transparentize(0.95, theme.royalBlue) : transparentize(0.95, theme.connectedGreen)};
border-radius: 1.5rem;
padding: 0.5rem 0.75rem;
font-weight: 500;
font-size: 0.75rem;
border: 1px solid;
border-color: ${({ pending, theme }) =>
pending ? transparentize(0.75, theme.royalBlue) : transparentize(0.75, theme.connectedGreen)};
#pending {
animation: 2s ${rotate} linear infinite;
}
:hover {
border-color: ${({ pending, theme }) =>
pending ? transparentize(0, theme.royalBlue) : transparentize(0, theme.connectedGreen)};
}
`
const ButtonWrapper = styled.div`
a {
color: ${({ pending, theme }) => (pending ? theme.royalBlue : theme.connectedGreen)};
}
`
export default function Info({ hash, pending }) {
const { networkId } = useWeb3Context()
const [isCopied, copy] = useCopyClipboard()
return (
<TransactionWrapper key={hash}>
<TransactionStatusWrapper>
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>{hash} </Link>
<CopyIcon onClick={() => copy(hash)}>
{isCopied ? (
<TransactionStatusText>
<FontAwesomeIcon icon={faCheckCircle} />
<TransactionStatusText>Copied</TransactionStatusText>
</TransactionStatusText>
) : (
<TransactionStatusText>
<FontAwesomeIcon icon={faCopy} />
</TransactionStatusText>
)}
</CopyIcon>
</TransactionStatusWrapper>
{pending ? (
<ButtonWrapper pending={pending}>
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>
<TransactionState pending={pending}>
<FontAwesomeIcon id="pending" icon={faCircleNotch} />
<TransactionStatusText>Pending</TransactionStatusText>
</TransactionState>
</Link>
</ButtonWrapper>
) : (
<ButtonWrapper pending={pending}>
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>
<TransactionState pending={pending}>
<FontAwesomeIcon icon={faCheck} />
<TransactionStatusText>Confirmed</TransactionStatusText>
</TransactionState>
</Link>
</ButtonWrapper>
)}
</TransactionWrapper>
)
}

@ -0,0 +1,108 @@
import React from 'react'
import styled, { keyframes } from 'styled-components'
import { useWeb3Context } from 'web3-react'
import Copy from './Copy'
import { getEtherscanLink } from '../../utils'
import { Link } from '../../theme'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCircleNotch, faCheck } from '@fortawesome/free-solid-svg-icons'
import { transparentize } from 'polished'
const TransactionStatusWrapper = styled.div`
display: flex;
align-items: center;
min-width: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`
const TransactionWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap}
justify-content: space-between;
width: 100%;
margin-top: 0.75rem;
a {
/* flex: 1 1 auto; */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
max-width: 250px;
}
`
const TransactionStatusText = styled.span`
margin-left: 0.25rem;
`
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
const TransactionState = styled.div`
background-color: ${({ pending, theme }) =>
pending ? transparentize(0.95, theme.royalBlue) : transparentize(0.95, theme.connectedGreen)};
border-radius: 1.5rem;
padding: 0.5rem 0.75rem;
font-weight: 500;
font-size: 0.75rem;
border: 1px solid;
border-color: ${({ pending, theme }) =>
pending ? transparentize(0.75, theme.royalBlue) : transparentize(0.75, theme.connectedGreen)};
#pending {
animation: 2s ${rotate} linear infinite;
}
:hover {
border-color: ${({ pending, theme }) =>
pending ? transparentize(0, theme.royalBlue) : transparentize(0, theme.connectedGreen)};
}
`
const ButtonWrapper = styled.div`
a {
color: ${({ pending, theme }) => (pending ? theme.royalBlue : theme.connectedGreen)};
}
`
export default function Transaction({ hash, pending }) {
const { networkId } = useWeb3Context()
return (
<TransactionWrapper key={hash}>
<TransactionStatusWrapper>
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>{hash} </Link>
<Copy toCopy={hash} />
</TransactionStatusWrapper>
{pending ? (
<ButtonWrapper pending={pending}>
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>
<TransactionState pending={pending}>
<FontAwesomeIcon id="pending" icon={faCircleNotch} />
<TransactionStatusText>Pending</TransactionStatusText>
</TransactionState>
</Link>
</ButtonWrapper>
) : (
<ButtonWrapper pending={pending}>
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>
<TransactionState pending={pending}>
<FontAwesomeIcon icon={faCheck} />
<TransactionStatusText>Confirmed</TransactionStatusText>
</TransactionState>
</Link>
</ButtonWrapper>
)}
</TransactionWrapper>
)
}

@ -0,0 +1,208 @@
import React from 'react'
import styled, { css } from 'styled-components'
import { useWeb3Context } from 'web3-react'
import Transaction from './Transaction'
import Copy from './Copy'
import Modal from '../Modal'
import { getEtherscanLink } from '../../utils'
import { Link } from '../../theme'
const Wrapper = styled.div`
margin: 0;
padding: 0;
width: 100%;
${({ theme }) => theme.flexColumnNoWrap}
`
const UpperSection = styled.div`
padding: 2rem;
background-color: ${({ theme }) => theme.concreteGray};
h5 {
margin: 0;
margin-bottom: 0.5rem;
font-size: 1rem;
font-weight: 400;
}
h5:last-child {
margin-bottom: 0px;
}
h4 {
margin-top: 0;
font-weight: 500;
}
`
const YourAccount = styled.div`
h5 {
margin: 0 0 1rem 0;
font-weight: 400;
color: ${({ theme }) => theme.doveGray};
}
h4 {
margin: 0;
font-weight: 500;
}
`
const LowerSection = styled.div`
${({ theme }) => theme.flexColumnNoWrap}
padding: 2rem;
flex-grow: 1;
overflow: auto;
h5 {
margin: 0;
font-weight: 400;
color: ${({ theme }) => theme.doveGray};
}
div:last-child {
/* margin-bottom: 0; */
}
`
const AccountControl = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
min-width: 0;
${({ hasENS, isENS }) =>
hasENS &&
isENS &&
css`
margin-bottom: 0.75rem;
`}
font-weight: ${({ hasENS, isENS }) => (hasENS ? (isENS ? css`500` : css`400`) : css`500`)};
font-size: ${({ hasENS, isENS }) => (hasENS ? (isENS ? css`1rem` : css`0.8rem`) : css`1rem`)};
a:hover {
text-decoration: underline;
}
a {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`
const TransactionListWrapper = styled.div`
${({ theme }) => theme.flexColumnNoWrap} /* margin: 0 0 1rem 0; */
`
const StyledLink = styled(Link)`
color: ${({ hasENS, isENS, theme }) => (hasENS ? (isENS ? theme.royalBlue : theme.doveGray) : theme.royalBlue)};
`
// function getErrorMessage(event) {
// switch (event.code) {
// case InjectedConnector.errorCodes.ETHEREUM_ACCESS_DENIED: {
// return 'Permission Required'
// }
// case InjectedConnector.errorCodes.UNLOCK_REQUIRED: {
// return 'Account Unlock Required'
// }
// case InjectedConnector.errorCodes.NO_WEB3: {
// return 'Not a Web3 Browser'
// }
// default: {
// return 'Connection Error'
// }
// }
// }
export default function WalletModal({ isOpen, error, onDismiss, pendingTransactions, confirmedTransactions, ENSName }) {
const { account, networkId } = useWeb3Context()
function renderTransactions(transactions, pending) {
return (
<TransactionListWrapper>
{transactions.map((hash, i) => {
return <Transaction key={i} hash={hash} pending={pending} />
})}
</TransactionListWrapper>
)
}
function wrappedOnDismiss() {
onDismiss()
}
function getWalletDisplay() {
if (error) {
return (
<>
<UpperSection>
<h4>Wrong Network</h4>
<h5>Please connect to the main Ethereum network.</h5>
</UpperSection>
</>
)
} else if (account) {
return (
<>
<UpperSection>
<YourAccount>
<h5>Your Account</h5>
{ENSName && (
<AccountControl hasENS={!!ENSName} isENS={true}>
<StyledLink hasENS={!!ENSName} isENS={true} href={getEtherscanLink(networkId, ENSName, 'address')}>
{ENSName} {' '}
</StyledLink>
<Copy toCopy={ENSName} />
</AccountControl>
)}
<AccountControl hasENS={!!ENSName} isENS={false}>
<StyledLink hasENS={!!ENSName} isENS={false} href={getEtherscanLink(networkId, account, 'address')}>
{account} {' '}
</StyledLink>
<Copy toCopy={account} />
</AccountControl>
</YourAccount>
</UpperSection>
{!!pendingTransactions.length || !!confirmedTransactions.length ? (
<LowerSection>
<h5>Recent Transactions</h5>
{renderTransactions(pendingTransactions, true)}
{renderTransactions(confirmedTransactions, false)}
</LowerSection>
) : (
<LowerSection>
<h5>Your transactions will appear here...</h5>
</LowerSection>
)}
</>
)
} else {
return (
<>
<UpperSection>
<h4>No Ethereum account found</h4>
<h5>Please visit this page in a Web3 enabled browser.</h5>
<h5>
<Link href={'https://ethereum.org/use/#_3-what-is-a-wallet-and-which-one-should-i-use'}>
Learn more
</Link>
</h5>
</UpperSection>
</>
)
}
}
return (
<Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} minHeight={null}>
<Wrapper>{getWalletDisplay()}</Wrapper>
</Modal>
)
}

@ -0,0 +1,115 @@
import React, { useState, useEffect } from 'react'
import { useWeb3Context, Connectors } from 'web3-react'
import styled, { keyframes } from 'styled-components'
import { ethers } from 'ethers'
import { useTranslation } from 'react-i18next'
import { isMobile } from 'react-device-detect'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
const { Connector } = Connectors
const MessageWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 20rem;
`
const Message = styled.h2`
color: ${({ theme }) => theme.uniswapPink};
`
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
const Spinner = styled.div`
font-size: 4rem;
svg {
animation: 2s ${rotate} linear infinite;
path {
color: ${({ theme }) => theme.uniswapPink};
}
}
`
function tryToSetConnector(setConnector, setError) {
setConnector('Injected', { suppressAndThrowErrors: true }).catch(error => {
setConnector('Network')
})
}
export default function Web3ReactManager({ children }) {
const { t } = useTranslation()
const { active, error, setConnector, setError } = useWeb3Context()
// control whether or not we render the error, after parsing
const blockRender = error && error.code && error.code === Connector.errorCodes.UNSUPPORTED_NETWORK
useEffect(() => {
if (!active && !error) {
if (window.ethereum || window.web3) {
if (isMobile) {
tryToSetConnector(setConnector, setError)
} else {
const library = new ethers.providers.Web3Provider(window.ethereum || window.web3)
library.listAccounts().then(accounts => {
if (accounts.length >= 1) {
tryToSetConnector(setConnector, setError)
} else {
setConnector('Network')
}
})
}
} else {
setConnector('Network')
}
}
}, [active, error, setConnector, setError])
// parse the error
useEffect(() => {
if (error) {
// if the user changes to the wrong network, unset the connector
if (error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
setConnector('Network')
}
}
}, [error, setConnector])
const [showLoader, setShowLoader] = useState(false)
useEffect(() => {
const timeout = setTimeout(() => {
setShowLoader(true)
}, 600)
return () => {
clearTimeout(timeout)
}
}, [])
if (blockRender) {
return null
} else if (error) {
return (
<MessageWrapper>
<Message>{t('unknownError')}</Message>
</MessageWrapper>
)
} else if (!active) {
return showLoader ? (
<MessageWrapper>
<Spinner>
<FontAwesomeIcon icon={faCircleNotch} />
</Spinner>
</MessageWrapper>
) : null
} else {
return children
}
}

@ -1,126 +1,287 @@
import React, { useState } from 'react'
import { useWeb3Context } from 'web3-react'
import classnames from 'classnames'
import Jazzicon from 'jazzicon'
import { CSSTransitionGroup } from 'react-transition-group'
import React, { useReducer, useEffect, useRef } from 'react'
import styled, { keyframes } from 'styled-components'
import { useTranslation } from 'react-i18next'
import { useWeb3Context, Connectors } from 'web3-react'
import Jazzicon from 'jazzicon'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { ethers } from 'ethers'
import { faCircleNotch, faPlug, faArrowRight } from '@fortawesome/free-solid-svg-icons'
import { darken } from 'polished'
import Modal from '../Modal'
import WalletModal from '../WalletModal'
import { useAllTransactions } from '../../contexts/Transactions'
import { shortenAddress } from '../../utils'
import { useENSName } from '../../hooks'
import './web3-status.scss'
const { Connector } = Connectors
function getEtherscanLink(tx) {
return `https://etherscan.io/tx/${tx}`
}
function getPendingText(pendingTransactions, pendingLabel) {
return (
<div className="web3-status__pending-container">
<div className="loader" />
<span key="text">
{pendingTransactions.length} {pendingLabel}
</span>
</div>
)
}
function getText(text, disconnectedText) {
if (!text || text.length < 42 || !ethers.utils.isHexString(text)) {
return disconnectedText
const Web3StatusGeneric = styled.button`
${({ theme }) => theme.flexRowNoWrap}
width: 100%;
font-size: 0.9rem;
align-items: center;
padding: 0.5rem;
border-radius: 2rem;
box-sizing: border-box;
cursor: pointer;
user-select: none;
:focus {
outline: none;
}
`
const Web3StatusError = styled(Web3StatusGeneric)`
background-color: ${({ theme }) => theme.salmonRed};
color: ${({ theme }) => theme.white};
border: 1px solid ${({ theme }) => theme.salmonRed};
font-weight: 500;
:hover,
:focus {
background-color: ${({ theme }) => darken(0.1, theme.salmonRed)};
}
`
const address = ethers.utils.getAddress(text)
return `${address.substring(0, 6)}...${address.substring(38)}`
const Web3StatusConnect = styled(Web3StatusGeneric)`
background-color: ${({ theme }) => theme.royalBlue};
color: ${({ theme }) => theme.white};
border: 1px solid ${({ theme }) => theme.royalBlue};
font-weight: 500;
:hover,
:focus {
background-color: ${({ theme }) => darken(0.1, theme.royalBlue)};
}
`
const Web3StatusConnected = styled(Web3StatusGeneric)`
background-color: ${({ pending, theme }) => (pending ? theme.zumthorBlue : theme.white)};
color: ${({ pending, theme }) => (pending ? theme.royalBlue : theme.doveGray)};
border: 1px solid ${({ pending, theme }) => (pending ? theme.royalBlue : theme.mercuryGray)};
font-weight: 400;
:hover,
:focus {
border: 1px solid
${({ pending, theme }) => (pending ? darken(0.1, theme.royalBlue) : darken(0.1, theme.mercuryGray))};
}
`
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
const Text = styled.p`
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0 0.5rem 0 0.25rem;
font-size: 0.83rem;
`
const Identicon = styled.div`
height: 1rem;
width: 1rem;
border-radius: 1.125rem;
background-color: ${({ theme }) => theme.silverGray};
`
const SpinningIcon = styled(FontAwesomeIcon)`
animation: 2s ${rotate} linear infinite;
color: ${({ theme }) => theme.royalBlue};
`
const RightIcon = styled(FontAwesomeIcon)`
margin-left: 0.25rem;
margin-right: 0.5rem;
`
const LeftIcon = styled(FontAwesomeIcon)`
margin-right: 0.25rem;
margin-left: 0.5rem;
`
const walletModalInitialState = {
open: false,
error: undefined
}
const WALLET_MODAL_ERROR = 'WALLET_MODAL_ERROR'
const WALLET_MODAL_OPEN = 'WALLET_MODAL_OPEN'
const WALLET_MODAL_OPEN_ERROR = 'WALLET_MODAL_OPEN_ERROR'
const WALLET_MODAL_CLOSE = 'WALLET_MODAL_CLOSE'
function walletModalReducer(state, { type, payload }) {
switch (type) {
case WALLET_MODAL_ERROR: {
const { error } = payload
return { ...state, error }
}
case WALLET_MODAL_OPEN: {
return { ...state, open: true }
}
case WALLET_MODAL_OPEN_ERROR: {
const { error } = payload || {}
return { open: true, error }
}
case WALLET_MODAL_CLOSE: {
return { ...state, open: false }
}
default: {
throw Error(`Unexpected action type in walletModalReducer reducer: '${type}'.`)
}
}
}
export default function Web3Status() {
const { t } = useTranslation()
const { active, account } = useWeb3Context()
const { active, account, connectorName, setConnector } = useWeb3Context()
const ENSName = useENSName(account)
const allTransactions = useAllTransactions()
const pending = Object.keys(allTransactions).filter(hash => !allTransactions[hash].receipt)
const confirmed = Object.keys(allTransactions).filter(hash => allTransactions[hash].receipt)
const hasPendingTransactions = !!pending.length
const hasConfirmedTransactions = !!confirmed.length
const [isShowingModal, setIsShowingModal] = useState(false)
const [{ open: walletModalIsOpen, error: walletModalError }, dispatch] = useReducer(
walletModalReducer,
walletModalInitialState
)
function setError(error) {
dispatch({ type: WALLET_MODAL_ERROR, payload: { error } })
}
function openWalletModal(error) {
dispatch({ type: WALLET_MODAL_OPEN, ...(error ? { payload: { error } } : {}) })
}
function closeWalletModal() {
dispatch({ type: WALLET_MODAL_CLOSE })
}
function handleClick() {
if (pending.length && !isShowingModal) {
setIsShowingModal(true)
// janky logic to detect log{ins,outs}...
useEffect(() => {
// if the injected connector is not active...
const { ethereum } = window
if (connectorName !== 'Injected') {
if (connectorName === 'Network' && ethereum && ethereum.on && ethereum.removeListener) {
function tryToActivateInjected() {
const library = new ethers.providers.Web3Provider(window.ethereum)
// if calling enable won't pop an approve modal, then try to activate injected...
library.listAccounts().then(accounts => {
if (accounts.length >= 1) {
setConnector('Injected', { suppressAndThrowErrors: true }).catch(error => {
// ...and if the error is that they're on the wrong network, display it, otherwise eat it
if (error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
setError(error)
}
})
}
})
}
ethereum.on('networkChanged', tryToActivateInjected)
ethereum.on('accountsChanged', tryToActivateInjected)
return () => {
if (ethereum.removeListener) {
ethereum.removeListener('networkChanged', tryToActivateInjected)
ethereum.removeListener('accountsChanged', tryToActivateInjected)
}
}
}
} else {
// ...poll to check the accounts array, and if it's ever 0 i.e. the user logged out, update the connector
if (ethereum) {
const accountPoll = setInterval(() => {
const library = new ethers.providers.Web3Provider(ethereum)
library.listAccounts().then(accounts => {
if (accounts.length === 0) {
setConnector('Network')
}
})
}, 750)
return () => {
clearInterval(accountPoll)
}
}
}
}, [connectorName, setConnector])
useEffect(() => {
if (account) {
setError()
}
})
function onClick() {
if (walletModalError) {
openWalletModal()
} else if (connectorName === 'Network' && (window.ethereum || window.web3)) {
setConnector('Injected', { suppressAndThrowErrors: true }).catch(error => {
if (error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
setError(error)
}
})
} else {
openWalletModal()
}
}
function renderPendingTransactions() {
return pending.map(transaction => {
const ref = useRef()
useEffect(() => {
if (ref.current) {
ref.current.innerHTML = ''
if (account) {
ref.current.appendChild(Jazzicon(16, parseInt(account.slice(2, 10), 16)))
}
}
}, [account, walletModalError])
function getWeb3Status() {
if (walletModalError) {
// this is ok because we're guaranteed that the error is a wrong network error
return (
<div
key={transaction}
className={classnames('pending-modal__transaction-row')}
onClick={() => window.open(getEtherscanLink(transaction), '_blank')}
>
<div className="pending-modal__transaction-label">{transaction}</div>
<div className="pending-modal__pending-indicator">
<div className="loader" /> {t('pending')}
</div>
</div>
<Web3StatusError onClick={onClick}>
<Text>Wrong Network</Text>
<RightIcon icon={faPlug} size={'sm'} />
</Web3StatusError>
)
} else if (!account) {
return (
<Web3StatusConnect onClick={onClick}>
<Text>{t('Connect')}</Text>
<RightIcon icon={faArrowRight} size={'sm'} />
</Web3StatusConnect>
)
} else {
return (
<Web3StatusConnected onClick={onClick} pending={hasPendingTransactions}>
{hasPendingTransactions && <SpinningIcon as={LeftIcon} icon={faCircleNotch} size={'sm'} />}
<Text>{ENSName || shortenAddress(account)}</Text>
<Identicon ref={ref} />
</Web3StatusConnected>
)
})
}
function renderModal() {
if (!isShowingModal) {
return null
}
return (
<Modal onClose={() => setIsShowingModal(false)}>
<CSSTransitionGroup
transitionName="token-modal"
transitionAppear={true}
transitionLeave={true}
transitionAppearTimeout={200}
transitionLeaveTimeout={200}
transitionEnterTimeout={200}
>
<div className="pending-modal">
<div className="pending-modal__transaction-list">
<div className="pending-modal__header">Transactions</div>
{renderPendingTransactions()}
</div>
</div>
</CSSTransitionGroup>
</Modal>
)
}
return (
<div
className={classnames('web3-status', {
'web3-status__connected': active,
'web3-status--pending': hasPendingTransactions,
'web3-status--confirmed': hasConfirmedTransactions
})}
onClick={handleClick}
>
<div className="web3-status__text">
{hasPendingTransactions ? getPendingText(pending, t('pending')) : getText(account, t('disconnected'))}
</div>
<div
className="web3-status__identicon"
ref={el => {
if (!el || !account) {
return
} else {
el.innerHTML = ''
el.appendChild(Jazzicon(16, parseInt(account.slice(2, 10), 16)))
}
}}
/>
{renderModal()}
</div>
active && (
<>
{getWeb3Status()}
<WalletModal
isOpen={walletModalIsOpen}
error={walletModalError}
onDismiss={closeWalletModal}
ENSName={ENSName}
pendingTransactions={pending}
confirmedTransactions={confirmed}
/>
</>
)
)
}

@ -1,104 +0,0 @@
@import '../../variables.scss';
.web3-status {
@extend %row-nowrap;
height: 2rem;
font-size: 0.9rem;
align-items: center;
border: 1px solid $mercury-gray;
padding: 0.5rem;
border-radius: 2rem;
color: $dove-gray;
font-weight: 400;
box-sizing: border-box;
&__identicon {
height: 1rem;
width: 1rem;
border-radius: 1.125rem;
background-color: $silver-gray;
}
&__text {
flex: 1 1 auto;
overflow: hidden;
margin-right: 0.75rem;
margin-left: 0.25rem;
font-size: 0.75rem;
}
&__pending-container {
display: flex;
justify-content: space-around;
}
&--pending {
background-color: $zumthor-blue;
color: $royal-blue;
border: 1px solid $royal-blue;
}
}
.pending-modal {
background-color: $white;
position: relative;
bottom: 12rem;
height: 12rem;
width: 100%;
z-index: 2000;
border-top-left-radius: 1rem;
border-top-right-radius: 1rem;
transition: 250ms ease-in-out;
&__transaction-list {
height: 12rem;
overflow-y: auto;
}
&__transaction-row {
@extend %row-nowrap;
box-sizing: border-box;
align-items: center;
padding: 0.25rem 1.5rem;
justify-content: space-between;
cursor: pointer;
width: 100%;
user-select: none;
}
&__transaction-label {
color: $royal-blue;
width: 60%;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 200;
}
&__pending-indicator {
@extend %row-nowrap;
color: $royal-blue;
border: 1px solid $royal-blue;
background-color: $zumthor-blue;
padding: 0.5rem 0.75rem;
border-radius: 100px;
font-size: 0.75rem;
& > .loading {
margin-right: 0.5rem;
}
}
&__header {
font-size: 1rem;
color: $dove-gray;
padding: 1rem 1.5rem 0.25rem 1.5rem;
}
}
.pending-modal-appear {
bottom: 0;
}
.pending-modal-appear.modal-container-appear-active {
bottom: 0;
}

@ -1,4 +1,6 @@
export const FACTORY_ADDRESSES = {
1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
4: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36'
3: '0x9c83dCE8CA20E9aAF9D3efc003b2ea62aBC08351',
4: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36',
42: '0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30'
}

@ -1,4 +1,4 @@
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
import React, { createContext, useContext, useReducer, useCallback, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { safeAccess, isAddress, getTokenAllowance } from '../utils'
@ -46,9 +46,7 @@ export default function Provider({ children }) {
dispatch({ type: UPDATE, payload: { networkId, address, tokenAddress, spenderAddress, value, blockNumber } })
}, [])
const contextValue = useMemo(() => [state, { update }], [state, update])
return <AllowancesContext.Provider value={contextValue}>{children}</AllowancesContext.Provider>
return <AllowancesContext.Provider value={[state, { update }]}>{children}</AllowancesContext.Provider>
}
export function useAddressAllowance(address, tokenAddress, spenderAddress) {

@ -1,4 +1,4 @@
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
import React, { createContext, useContext, useReducer, useCallback, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { safeAccess } from '../utils'
@ -51,13 +51,11 @@ export default function Provider({ children }) {
dispatch({ type: UPDATE_BLOCK_NUMBER, payload: { networkId, blockNumber } })
}, [])
const contextValue = useMemo(() => [state, { dismissBetaMessage, updateBlockNumber }], [
state,
dismissBetaMessage,
updateBlockNumber
])
return <ApplicationContext.Provider value={contextValue}>{children}</ApplicationContext.Provider>
return (
<ApplicationContext.Provider value={[state, { dismissBetaMessage, updateBlockNumber }]}>
{children}
</ApplicationContext.Provider>
)
}
export function Updater() {

@ -1,4 +1,4 @@
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
import React, { createContext, useContext, useReducer, useCallback, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { safeAccess, isAddress, getEtherBalance, getTokenBalance } from '../utils'
@ -44,9 +44,7 @@ export default function Provider({ children }) {
dispatch({ type: UPDATE, payload: { networkId, address, tokenAddress, value, blockNumber } })
}, [])
const contextValue = useMemo(() => [state, { update }], [state, update])
return <BalancesContext.Provider value={contextValue}>{children}</BalancesContext.Provider>
return <BalancesContext.Provider value={[state, { update }]}>{children}</BalancesContext.Provider>
}
export function useAddressBalance(address, tokenAddress) {
@ -66,14 +64,13 @@ export function useAddressBalance(address, tokenAddress) {
library
) {
let stale = false
;(tokenAddress === 'ETH' ? getEtherBalance(address, library) : getTokenBalance(tokenAddress, address, library))
.then(value => {
if (!stale) {
update(networkId, address, tokenAddress, value, globalBlockNumber)
}
})
.catch(e => {
.catch(() => {
if (!stale) {
update(networkId, address, tokenAddress, null, globalBlockNumber)
}

@ -1,4 +1,4 @@
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
import React, { createContext, useContext, useReducer, useCallback, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
@ -18,14 +18,17 @@ const EXCHANGE_ADDRESS = 'exchangeAddress'
const UPDATE = 'UPDATE'
const ETH = {
ETH: {
[NAME]: 'Ethereum',
[SYMBOL]: 'ETH',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: null
}
}
const INITIAL_TOKENS_CONTEXT = {
1: {
ETH: {
[NAME]: 'Ethereum',
[SYMBOL]: 'ETH',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: null
},
'0x960b236A07cf122663c4303350609A66A7B288C0': {
[NAME]: 'Aragon Network Token',
[SYMBOL]: 'ANT',
@ -341,17 +344,16 @@ export default function Provider({ children }) {
dispatch({ type: UPDATE, payload: { networkId, tokenAddress, name, symbol, decimals, exchangeAddress } })
}, [])
const contextValue = useMemo(() => [state, { update }], [state, update])
return <TokensContext.Provider value={contextValue}>{children}</TokensContext.Provider>
return <TokensContext.Provider value={[state, { update }]}>{children}</TokensContext.Provider>
}
export function useTokenDetails(tokenAddress) {
const { networkId, library } = useWeb3Context()
const [state, { update }] = useTokensContext()
const allTokensInNetwork = { ...ETH, ...(safeAccess(state, [networkId]) || {}) }
const { [NAME]: name, [SYMBOL]: symbol, [DECIMALS]: decimals, [EXCHANGE_ADDRESS]: exchangeAddress } =
safeAccess(state, [networkId, tokenAddress]) || {}
safeAccess(allTokensInNetwork, [tokenAddress]) || {}
useEffect(() => {
if (
@ -390,7 +392,7 @@ export function useAllTokenDetails(requireExchange = true) {
const { networkId } = useWeb3Context()
const [state] = useTokensContext()
const tokenDetails = safeAccess(state, [networkId]) || {}
const tokenDetails = { ...ETH, ...(safeAccess(state, [networkId]) || {}) }
return requireExchange
? Object.keys(tokenDetails)

@ -1,4 +1,4 @@
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
import React, { createContext, useContext, useReducer, useCallback, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
@ -93,9 +93,9 @@ export default function Provider({ children }) {
dispatch({ type: FINALIZE, payload: { networkId, hash, receipt } })
}, [])
const contextValue = useMemo(() => [state, { add, check, finalize }], [state, add, check, finalize])
return <TransactionsContext.Provider value={contextValue}>{children}</TransactionsContext.Provider>
return (
<TransactionsContext.Provider value={[state, { add, check, finalize }]}>{children}</TransactionsContext.Provider>
)
}
export function Updater() {

@ -1,8 +1,9 @@
import { useState, useMemo, useCallback, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import ERC20_ABI from '../abi/erc20'
import { getContract, getFactoryContract, getExchangeContract } from '../utils'
import ERC20_ABI from '../constants/abis/erc20'
import { getContract, getFactoryContract, getExchangeContract, isAddress } from '../utils'
import copy from 'copy-to-clipboard'
// modified from https://usehooks.com/useDebounce/
export function useDebounce(value, delay) {
@ -28,8 +29,13 @@ export function useDebounce(value, delay) {
// modified from https://usehooks.com/useKeyPress/
export function useBodyKeyDown(targetKey, onKeyDown, suppressOnKeyDown = false) {
const downHandler = useCallback(
({ target: { tagName }, key }) => {
event => {
const {
target: { tagName },
key
} = event
if (key === targetKey && tagName === 'BODY' && !suppressOnKeyDown) {
event.preventDefault()
onKeyDown()
}
},
@ -44,6 +50,35 @@ export function useBodyKeyDown(targetKey, onKeyDown, suppressOnKeyDown = false)
}, [downHandler])
}
export function useENSName(address) {
const { library } = useWeb3Context()
const [ENSName, setENSNname] = useState()
useEffect(() => {
if (isAddress(address)) {
let stale = false
library.lookupAddress(address).then(name => {
if (!stale) {
if (name) {
setENSNname(name)
} else {
setENSNname(null)
}
}
})
return () => {
stale = true
setENSNname()
}
}
}, [library, address])
return ENSName
}
// returns null on errors
export function useContract(address, ABI, withSignerIfPossible = true) {
const { library, account } = useWeb3Context()
@ -94,3 +129,26 @@ export function useExchangeContract(exchangeAddress, withSignerIfPossible = true
}
}, [exchangeAddress, library, withSignerIfPossible, account])
}
export function useCopyClipboard(timeout = 500) {
const [isCopied, setIsCopied] = useState(false)
const staticCopy = useCallback(text => {
const didCopy = copy(text)
setIsCopied(didCopy)
}, [])
useEffect(() => {
if (isCopied) {
const hide = setTimeout(() => {
setIsCopied(false)
}, timeout)
return () => {
clearTimeout(hide)
}
}
}, [isCopied, setIsCopied, timeout])
return [isCopied, staticCopy]
}

@ -1,26 +1,22 @@
import i18next from 'i18next'
import { initReactI18next } from 'react-i18next'
import XHR from 'i18next-xhr-backend'
import LanguageDetector from 'i18next-browser-languagedetector'
import { initReactI18next } from 'react-i18next'
i18next
// load translation using xhr -> see /public/locales
// https://github.com/i18next/i18next-xhr-backend
.use(XHR)
// detect user language
// https://github.com/i18next/i18next-browser-languageDetector
.use(LanguageDetector)
.use(initReactI18next)
// https://www.i18next.com/overview/configuration-options
.init({
backend: {
loadPath: '/locales/{{lng}}.json'
},
react: {
useSuspense: false
useSuspense: true
},
lng: 'en',
fallbackLng: 'en',
preload: ['en'],
keySeparator: false,
interpolation: { escapeValue: false }
})

@ -11,6 +11,7 @@ import BalancesContextProvider from './contexts/Balances'
import AllowancesContextProvider from './contexts/Allowances'
import App from './pages/App'
import InjectedConnector from './InjectedConnector'
import './i18n'
@ -21,12 +22,10 @@ if (process.env.NODE_ENV === 'production') {
}
ReactGA.pageview(window.location.pathname + window.location.search)
const { InjectedConnector, NetworkOnlyConnector } = Connectors
const Injected = new InjectedConnector({ supportedNetworks: [Number(process.env.REACT_APP_NETWORK_ID) || 1] })
const Infura = new NetworkOnlyConnector({
providerURL: process.env.REACT_APP_NETWORK_URL || ''
})
const connectors = { Injected, Infura }
const { NetworkOnlyConnector } = Connectors
const Injected = new InjectedConnector({ supportedNetworks: [Number(process.env.REACT_APP_NETWORK_ID || '1')] })
const Network = new NetworkOnlyConnector({ providerURL: process.env.REACT_APP_NETWORK_URL || '' })
const connectors = { Injected, Network }
function ContextProviders({ children }) {
return (

@ -1,204 +0,0 @@
/* eslint-disable */
/*
@asset(/libraries/qr-scanner/qr-scanner-worker.min.js) */
'use strict'
export default class QrScanner {
constructor(video, onDecode, canvasSize = QrScanner.DEFAULT_CANVAS_SIZE) {
this.$video = video
this.$canvas = document.createElement('canvas')
this._onDecode = onDecode
this._active = false
this.$canvas.width = canvasSize
this.$canvas.height = canvasSize
this._sourceRect = { x: 0, y: 0, width: canvasSize, height: canvasSize }
this.$video.addEventListener('canplay', () => this._updateSourceRect())
this.$video.addEventListener(
'play',
() => {
this._updateSourceRect()
this._scanFrame()
},
false
)
this._qrWorker = new Worker(QrScanner.WORKER_PATH)
}
_updateSourceRect() {
const smallestDimension = Math.min(this.$video.videoWidth, this.$video.videoHeight)
const sourceRectSize = Math.round((2 / 3) * smallestDimension)
this._sourceRect.width = this._sourceRect.height = sourceRectSize
this._sourceRect.x = (this.$video.videoWidth - sourceRectSize) / 2
this._sourceRect.y = (this.$video.videoHeight - sourceRectSize) / 2
}
_scanFrame() {
if (this.$video.paused || this.$video.ended) return false
requestAnimationFrame(() => {
QrScanner.scanImage(this.$video, this._sourceRect, this._qrWorker, this.$canvas, true)
.then(this._onDecode, error => {
if (error !== 'QR code not found.') console.error(error)
})
.then(() => this._scanFrame())
})
}
_getCameraStream(facingMode, exact = false) {
const constraintsToTry = [{ width: { min: 1024 } }, { width: { min: 768 } }, {}]
if (facingMode) {
if (exact) facingMode = { exact: facingMode }
constraintsToTry.forEach(constraint => (constraint.facingMode = facingMode))
}
return this._getMatchingCameraStream(constraintsToTry)
}
_getMatchingCameraStream(constraintsToTry) {
if (constraintsToTry.length === 0) return Promise.reject('Camera not found.')
return navigator.mediaDevices
.getUserMedia({ video: constraintsToTry.shift() })
.catch(() => this._getMatchingCameraStream(constraintsToTry))
}
start() {
if (this._active) return Promise.resolve()
this._active = true
clearTimeout(this._offTimeout)
let facingMode = 'environment'
return this._getCameraStream('environment', true)
.catch(() => {
facingMode = 'user'
return this._getCameraStream()
})
.then(stream => {
this.$video.srcObject = stream
this._setVideoMirror(facingMode)
})
.catch(e => {
this._active = false
throw e
})
}
stop() {
if (!this._active) return
this._active = false
this.$video.pause()
this._offTimeout = setTimeout(() => {
this.$video.srcObject.getTracks()[0].stop()
this.$video.srcObject = null
}, 3e3)
}
_setVideoMirror(facingMode) {
const scaleFactor = facingMode === 'user' ? -1 : 1
this.$video.style.transform = 'scaleX(' + scaleFactor + ')'
}
setGrayscaleWeights(red, green, blue) {
this._qrWorker.postMessage({ type: 'grayscaleWeights', data: { red, green, blue } })
}
static scanImage(
imageOrFileOrUrl,
sourceRect = null,
worker = null,
canvas = null,
fixedCanvasSize = false,
alsoTryWithoutSourceRect = false
) {
const promise = new Promise((resolve, reject) => {
worker = worker || new Worker(QrScanner.WORKER_PATH)
let timeout, onMessage, onError
onMessage = event => {
if (event.data.type !== 'qrResult') return
worker.removeEventListener('message', onMessage)
worker.removeEventListener('error', onError)
clearTimeout(timeout)
if (event.data.data !== null) resolve(event.data.data)
else reject('QR code not found.')
}
onError = () => {
worker.removeEventListener('message', onMessage)
worker.removeEventListener('error', onError)
clearTimeout(timeout)
reject('Worker error.')
}
worker.addEventListener('message', onMessage)
worker.addEventListener('error', onError)
timeout = setTimeout(onError, 3e3)
QrScanner._loadImage(imageOrFileOrUrl)
.then(image => {
const imageData = QrScanner._getImageData(image, sourceRect, canvas, fixedCanvasSize)
worker.postMessage({ type: 'decode', data: imageData }, [imageData.data.buffer])
})
.catch(reject)
})
if (sourceRect && alsoTryWithoutSourceRect)
return promise.catch(() => QrScanner.scanImage(imageOrFileOrUrl, null, worker, canvas, fixedCanvasSize))
else return promise
}
static _getImageData(image, sourceRect = null, canvas = null, fixedCanvasSize = false) {
canvas = canvas || document.createElement('canvas')
const sourceRectX = sourceRect && sourceRect.x ? sourceRect.x : 0
const sourceRectY = sourceRect && sourceRect.y ? sourceRect.y : 0
const sourceRectWidth = sourceRect && sourceRect.width ? sourceRect.width : image.width || image.videoWidth
const sourceRectHeight = sourceRect && sourceRect.height ? sourceRect.height : image.height || image.videoHeight
if (!fixedCanvasSize && (canvas.width !== sourceRectWidth || canvas.height !== sourceRectHeight)) {
canvas.width = sourceRectWidth
canvas.height = sourceRectHeight
}
const context = canvas.getContext('2d', { alpha: false })
context.imageSmoothingEnabled = false
context.drawImage(
image,
sourceRectX,
sourceRectY,
sourceRectWidth,
sourceRectHeight,
0,
0,
canvas.width,
canvas.height
)
return context.getImageData(0, 0, canvas.width, canvas.height)
}
static _loadImage(imageOrFileOrUrl) {
if (
imageOrFileOrUrl instanceof HTMLCanvasElement ||
imageOrFileOrUrl instanceof HTMLVideoElement ||
(window.ImageBitmap && imageOrFileOrUrl instanceof window.ImageBitmap) ||
(window.OffscreenCanvas && imageOrFileOrUrl instanceof window.OffscreenCanvas)
)
return Promise.resolve(imageOrFileOrUrl)
else if (imageOrFileOrUrl instanceof Image)
return QrScanner._awaitImageLoad(imageOrFileOrUrl).then(() => imageOrFileOrUrl)
else if (
imageOrFileOrUrl instanceof File ||
imageOrFileOrUrl instanceof URL ||
typeof imageOrFileOrUrl === 'string'
) {
const image = new Image()
if (imageOrFileOrUrl instanceof File) image.src = URL.createObjectURL(imageOrFileOrUrl)
else image.src = imageOrFileOrUrl
return QrScanner._awaitImageLoad(image).then(() => {
if (imageOrFileOrUrl instanceof File) URL.revokeObjectURL(image.src)
return image
})
} else return Promise.reject('Unsupported image type.')
}
static _awaitImageLoad(image) {
return new Promise((resolve, reject) => {
if (image.complete && image.naturalWidth !== 0) resolve()
else {
let onLoad, onError
onLoad = () => {
image.removeEventListener('load', onLoad)
image.removeEventListener('error', onError)
resolve()
}
onError = () => {
image.removeEventListener('load', onLoad)
image.removeEventListener('error', onError)
reject('Image load error')
}
image.addEventListener('load', onLoad)
image.addEventListener('error', onError)
}
})
}
}
QrScanner.DEFAULT_CANVAS_SIZE = 400
QrScanner.WORKER_PATH = '/libraries/qr-scanner/qr-scanner-worker.min.js'
//# sourceMappingURL=qr-scanner.min.js.map

@ -1,50 +1,49 @@
import React, { useEffect } from 'react'
import React, { Suspense, lazy } from 'react'
import styled from 'styled-components'
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'
import { useWeb3Context, Connectors } from 'web3-react'
import NavigationTabs from '../components/NavigationTabs'
import Web3ReactManager from '../components/Web3ReactManager'
import Header from '../components/Header'
import Swap from './Swap'
import Send from './Send'
import Pool from './Pool'
import NavigationTabs from '../components/NavigationTabs'
import './App.scss'
const Swap = lazy(() => import('./Swap'))
const Send = lazy(() => import('./Send'))
const Pool = lazy(() => import('./Pool'))
const { Connector, InjectedConnector } = Connectors
const HeaderWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap}
width: 100%;
justify-content: space-between;
`
const BodyWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap}
width: 100%;
justify-content: center;
flex-grow: 1;
flex-basis: 0;
overflow: auto;
`
const Body = styled.div`
width: 35rem;
margin: 1.25rem;
`
export default function App() {
const { setConnector, setError, error, active, connectorName } = useWeb3Context()
// start web3-react on page-load
useEffect(() => {
setConnector('Injected', { suppressAndThrowErrors: true }).catch(error => {
if (error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
setError(error, { connectorName: 'Injected' })
} else {
setConnector('Infura')
}
})
}, []) // eslint-disable-line react-hooks/exhaustive-deps
// if the metamask user logs out, set the infura provider
useEffect(() => {
if (error && error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) {
setConnector('Infura')
}
}, [error, connectorName, setConnector])
// active state
if (active || error) {
return (
<div id="app-container">
<Header />
{/* this is an intermediate state before infura is set */}
{(!error || error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) && (
<div className="app__wrapper">
<div className="body">
<div className="body__content">
<BrowserRouter>
<NavigationTabs />
return (
<>
<Suspense fallback={null}>
<HeaderWrapper>
<Header />
</HeaderWrapper>
<BodyWrapper>
<Body>
<Web3ReactManager>
<BrowserRouter>
<NavigationTabs />
{/* this Suspense is for route code-splitting */}
<Suspense fallback={null}>
<Switch>
<Route exact strict path="/swap" component={Swap} />
<Route exact strict path="/send" component={Send} />
@ -59,15 +58,12 @@ export default function App() {
/>
<Redirect to="/swap" />
</Switch>
</BrowserRouter>
</div>
</div>
</div>
)}
</div>
)
}
// loading state
return null
</Suspense>
</BrowserRouter>
</Web3ReactManager>
</Body>
</BodyWrapper>
</Suspense>
</>
)
}

@ -1,40 +0,0 @@
@import '../variables.scss';
.app {
&__wrapper {
height: 100%;
position: relative;
margin: auto;
max-width: 560px;
width: 100%;
overflow-y: auto;
& > div {
position: absolute;
width: 100%;
height: 100%;
top: 0;
bottom: 0;
}
}
}
#app-container {
width: 100vw;
height: 100vh;
@extend %col-nowrap;
}
.body {
@extend %col-nowrap;
height: 100%;
background-color: $white;
&__content {
padding: 1rem 0.75rem;
flex: 1 1 auto;
height: 0;
overflow-y: auto;
}
}

@ -1,23 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom'
import Web3Provider, { Connectors } from 'web3-react'
import App from './App'
// TODO, fix this hacky workaround
const { NetworkOnlyConnector } = Connectors
const Injected = new NetworkOnlyConnector({
providerURL: process.env.REACT_APP_NETWORK_URL
})
export const connectors = { Injected }
it('renders without crashing', () => {
const div = document.createElement('div')
ReactDOM.render(
<Web3Provider connectors={connectors} libraryName="ethers.js">
<App />
</Web3Provider>,
div
)
ReactDOM.unmountComponentAtNode(div)
})

@ -1,10 +1,11 @@
import React, { useReducer, useState, useCallback, useEffect, useMemo } from 'react'
import classnames from 'classnames'
import { useTranslation } from 'react-i18next'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import ReactGA from 'react-ga'
import styled from 'styled-components'
import { Button } from '../../theme'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import OversizedPanel from '../../components/OversizedPanel'
import ContextualInfo from '../../components/ContextualInfo'
@ -17,8 +18,6 @@ import { useTokenDetails } from '../../contexts/Tokens'
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances'
import './pool.scss'
const INPUT = 0
const OUTPUT = 1
@ -31,6 +30,72 @@ const DEADLINE_FROM_NOW = 60 * 15
// denominated in bips
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
const BlueSpan = styled.span`
color: ${({ theme }) => theme.royalBlue};
`
const NewExchangeWarning = styled.div`
margin-top: 1rem;
padding: 1rem;
margin-bottom: 2rem;
border: 1px solid rgba($pizazz-orange, 0.4);
background-color: rgba($pizazz-orange, 0.1);
border-radius: 1rem;
`
const NewExchangeWarningText = styled.div`
font-size: 0.75rem;
line-height: 1rem;
text-align: center;
:first-child {
padding-bottom: 0.3rem;
font-weight: 500;
}
`
const DownArrowBackground = styled.div`
${({ theme }) => theme.flexRowNoWrap}
justify-content: center;
align-items: center;
`
const DownArrow = styled.img`
width: 0.625rem;
height: 0.625rem;
position: relative;
padding: 0.875rem;
`
const SummaryPanel = styled.div`
${({ theme }) => theme.flexColumnNoWrap}
padding: 1rem 0;
`
const ExchangeRateWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
color: ${({ theme }) => theme.doveGray};
font-size: 0.75rem;
padding: 0.25rem 1rem 0;
`
const ExchangeRate = styled.span`
flex: 1 1 auto;
width: 0;
color: ${({ theme }) => theme.chaliceGray};
`
const Flex = styled.div`
display: flex;
justify-content: center;
padding: 2rem;
button {
max-width: 20rem;
}
`
function calculateSlippageBounds(value) {
if (value) {
const offset = value.mul(ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000))
@ -209,15 +274,15 @@ export default function AddLiquidity() {
action: 'Open'
})
const b = text => <span className="swap__highlight-text">{text}</span>
const b = text => <BlueSpan>{text}</BlueSpan>
if (isNewExchange) {
return (
<div>
<div className="pool__summary-item">
<div>
{t('youAreAdding')} {b(`${inputValue} ETH`)} {t('and')} {b(`${outputValue} ${symbol}`)} {t('intoPool')}
</div>
<div className="pool__summary-item">
<div>
{t('youAreSettingExRate')}{' '}
{b(
`1 ETH = ${amountFormatter(
@ -229,26 +294,26 @@ export default function AddLiquidity() {
)}
.
</div>
<div className="pool__summary-item">
<div>
{t('youWillMint')} {b(`${inputValue}`)} {t('liquidityTokens')}
</div>
<div className="pool__summary-item">{t('totalSupplyIs0')}</div>
<div>{t('totalSupplyIs0')}</div>
</div>
)
} else {
return (
<>
<div className="pool__summary-modal__item">
<div>
{t('youAreAdding')} {b(`${amountFormatter(inputValueParsed, 18, 4)} ETH`)} {t('and')} {'at most'}{' '}
{b(`${amountFormatter(outputValueMax, 18, 4)} ${symbol}`)} {t('intoPool')}
</div>
<div className="pool__summary-modal__item">
<div>
{t('youWillMint')} {b(amountFormatter(liquidityMinted, 18, 4))} {t('liquidityTokens')}
</div>
<div className="pool__summary-modal__item">
<div>
{t('totalSupplyIs')} {b(amountFormatter(totalPoolTokens, 18, 4))}
</div>
<div className="pool__summary-modal__item">
<div>
{t('tokenWorth')} {b(amountFormatter(ethPerLiquidityToken, 18, 4))} ETH {t('and')}{' '}
{b(amountFormatter(tokenPerLiquidityToken, decimals, Math.min(decimals, 4)))} {symbol}
</div>
@ -447,15 +512,15 @@ export default function AddLiquidity() {
return (
<>
{isNewExchange ? (
<div className="pool__new-exchange-warning">
<div className="pool__new-exchange-warning-text">
<NewExchangeWarning>
<NewExchangeWarningText>
<span role="img" aria-label="first-liquidity">
🚰
</span>{' '}
{t('firstLiquidity')}
</div>
<div className="pool__new-exchange-warning-text">{t('initialExchangeRate', { symbol })}</div>
</div>
</NewExchangeWarningText>
<NewExchangeWarningText>{t('initialExchangeRate', { symbol })}</NewExchangeWarningText>
</NewExchangeWarning>
) : null}
<CurrencyInputPanel
@ -470,9 +535,9 @@ export default function AddLiquidity() {
disableTokenSelect
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img className="swap__down-arrow" src={isActive ? PlusBlue : PlusGrey} alt="plus" />
</div>
<DownArrowBackground>
<DownArrow src={isActive ? PlusBlue : PlusGrey} alt="plus" />
</DownArrowBackground>
</OversizedPanel>
<CurrencyInputPanel
title={t('deposit')}
@ -490,13 +555,13 @@ export default function AddLiquidity() {
errorMessage={outputError}
/>
<OversizedPanel hideBottom>
<div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('exchangeRate')}</span>
<SummaryPanel>
<ExchangeRateWrapper>
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
<span>{marketRate ? `1 ETH = ${amountFormatter(marketRate, 18, 4)} ${symbol}` : ' - '}</span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('currentPoolSize')}</span>
</ExchangeRateWrapper>
<ExchangeRateWrapper>
<ExchangeRate>{t('currentPoolSize')}</ExchangeRate>
<span>
{exchangeETHBalance && exchangeTokenBalance
? `${amountFormatter(exchangeETHBalance, 18, 4)} ETH + ${amountFormatter(
@ -506,11 +571,11 @@ export default function AddLiquidity() {
)} ${symbol}`
: ' - '}
</span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">
</ExchangeRateWrapper>
<ExchangeRateWrapper>
<ExchangeRate>
{t('yourPoolShare')} ({exchangeETHBalance && amountFormatter(poolTokenPercentage, 16, 2)}%)
</span>
</ExchangeRate>
<span>
{ethShare && tokenShare
? `${amountFormatter(ethShare, 18, 4)} ETH + ${amountFormatter(
@ -520,21 +585,15 @@ export default function AddLiquidity() {
)} ${symbol}`
: ' - '}
</span>
</div>
</div>
</ExchangeRateWrapper>
</SummaryPanel>
</OversizedPanel>
{renderSummary()}
<div className="pool__cta-container">
<button
className={classnames('pool__cta-btn', {
'pool__cta-btn--inactive': !isActive
})}
disabled={!isValid}
onClick={onAddLiquidity}
>
<Flex>
<Button disabled={!isValid} onClick={onAddLiquidity} fullWidth>
{t('addLiquidity')}
</button>
</div>
</Button>
</Flex>
</>
)
}

@ -2,16 +2,58 @@ import React, { useState, useEffect } from 'react'
import { withRouter } from 'react-router'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import classnames from 'classnames'
import styled from 'styled-components'
import { useTranslation } from 'react-i18next'
import ReactGA from 'react-ga'
import { Button } from '../../theme'
import AddressInputPanel from '../../components/AddressInputPanel'
import OversizedPanel from '../../components/OversizedPanel'
import { useFactoryContract } from '../../hooks'
import { useTokenDetails } from '../../contexts/Tokens'
import { useTransactionAdder } from '../../contexts/Transactions'
const SummaryPanel = styled.div`
${({ theme }) => theme.flexColumnNoWrap};
padding: 1rem 0;
`
const ExchangeRateWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
color: ${({ theme }) => theme.doveGray};
font-size: 0.75rem;
padding: 0.25rem 1rem 0;
`
const ExchangeRate = styled.span`
flex: 1 1 auto;
width: 0;
color: ${({ theme }) => theme.chaliceGray};
`
const CreateExchangeWrapper = styled.div`
color: ${({ theme }) => theme.doveGray};
text-align: center;
margin-top: 1rem;
padding-top: 1rem;
`
const SummaryText = styled.div`
font-size: 0.75rem;
color: ${({ error, theme }) => error && theme.salmonRed};
`
const Flex = styled.div`
display: flex;
justify-content: center;
padding: 2rem;
button {
max-width: 20rem;
}
`
function CreateExchange({ history, location }) {
const { t } = useTranslation()
const { account } = useWeb3Context()
@ -81,35 +123,29 @@ function CreateExchange({ history, location }) {
onError={setTokenAddressError}
/>
<OversizedPanel hideBottom>
<div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('name')}</span>
<SummaryPanel>
<ExchangeRateWrapper>
<ExchangeRate>{t('name')}</ExchangeRate>
<span>{name ? name : ' - '}</span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('symbol')}</span>
</ExchangeRateWrapper>
<ExchangeRateWrapper>
<ExchangeRate>{t('symbol')}</ExchangeRate>
<span>{symbol ? symbol : ' - '}</span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('decimals')}</span>
</ExchangeRateWrapper>
<ExchangeRateWrapper>
<ExchangeRate>{t('decimals')}</ExchangeRate>
<span>{decimals || decimals === 0 ? decimals : ' - '}</span>
</div>
</div>
</ExchangeRateWrapper>
</SummaryPanel>
</OversizedPanel>
<div className="create-exchange__summary-panel">
<div
className={classnames('create-exchange__summary-text', {
'create-exchange--error': !!errorMessage
})}
>
{errorMessage ? errorMessage : t('enterTokenCont')}
</div>
</div>
<div className="pool__cta-container">
<button className="pool__cta-btn" disabled={!isValid} onClick={createExchange}>
<CreateExchangeWrapper>
<SummaryText>{errorMessage ? errorMessage : t('enterTokenCont')}</SummaryText>
</CreateExchangeWrapper>
<Flex>
<Button disabled={!isValid} onClick={createExchange} fullWidth>
{t('createExchange')}
</button>
</div>
</Button>
</Flex>
</>
)
}

@ -1,15 +1,13 @@
import React, { useState, useCallback } from 'react'
import { withRouter, NavLink } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { CSSTransitionGroup } from 'react-transition-group'
import styled from 'styled-components'
import OversizedPanel from '../../components/OversizedPanel'
import Dropdown from '../../assets/images/dropdown-blue.svg'
import Modal from '../../components/Modal'
import { useBodyKeyDown } from '../../hooks'
import './pool.scss'
const poolTabOrder = [
{
path: '/add-liquidity',
@ -28,10 +26,61 @@ const poolTabOrder = [
}
]
const LiquidityContainer = styled.div`
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
font-size: 0.75rem;
padding: 0.625rem 1rem;
font-size: 0.75rem;
color: ${({ theme }) => theme.royalBlue};
font-weight: 500;
cursor: pointer;
img {
height: 0.75rem;
width: 0.75rem;
}
`
const LiquidityLabel = styled.span`
flex: 1 0 auto;
`
const activeClassName = 'MODE'
const StyledNavLink = styled(NavLink).attrs({
activeClassName
})`
${({ theme }) => theme.flexRowNoWrap}
padding: 1rem;
margin-left: 1rem;
margin-right: 1rem;
font-size: 1rem;
cursor: pointer;
text-decoration: none;
color: ${({ theme }) => theme.doveGray};
font-size: 1rem;
&.${activeClassName} {
background-color: ${({ theme }) => theme.white};
border-radius: 3rem;
box-shadow: 0 0 0.5px 0.5px ${({ theme }) => theme.mercuryGray};
font-weight: 500;
color: ${({ theme }) => theme.royalBlue};
}
`
const PoolModal = styled.div`
background-color: ${({ theme }) => theme.white};
width: 100%;
height: 100%;
padding: 2rem 0 2rem 0;
`
function ModeSelector({ location: { pathname }, history }) {
const { t } = useTranslation()
const [isShowingModal, setIsShowingModal] = useState(false)
const [modalIsOpen, setModalIsOpen] = useState(false)
const activeTabKey = poolTabOrder[poolTabOrder.findIndex(({ regex }) => pathname.match(regex))].textKey
@ -49,53 +98,41 @@ function ModeSelector({ location: { pathname }, history }) {
navigate(-1)
}, [navigate])
useBodyKeyDown('ArrowDown', navigateRight, isShowingModal)
useBodyKeyDown('ArrowUp', navigateLeft, isShowingModal)
useBodyKeyDown('ArrowDown', navigateRight, modalIsOpen)
useBodyKeyDown('ArrowUp', navigateLeft, modalIsOpen)
return (
<OversizedPanel hideTop>
<div
className="pool__liquidity-container"
<LiquidityContainer
onClick={() => {
setIsShowingModal(true)
setModalIsOpen(true)
}}
>
<span className="pool__liquidity-label">{t(activeTabKey)}</span>
<LiquidityLabel>{t(activeTabKey)}</LiquidityLabel>
<img src={Dropdown} alt="dropdown" />
</div>
{isShowingModal && (
<Modal
onClose={() => {
setIsShowingModal(false)
}}
>
<CSSTransitionGroup
transitionName="pool-modal"
transitionAppear={true}
transitionLeave={true}
transitionAppearTimeout={200}
transitionLeaveTimeout={200}
transitionEnterTimeout={200}
>
<div className="pool-modal">
{poolTabOrder.map(({ path, textKey, regex }) => (
<NavLink
key={path}
to={path}
className="pool-modal__item"
activeClassName="pool-modal__item--selected"
isActive={(_, { pathname }) => pathname.match(regex)}
onClick={() => {
setIsShowingModal(false)
}}
>
{t(textKey)}
</NavLink>
))}
</div>
</CSSTransitionGroup>
</Modal>
)}
</LiquidityContainer>
<Modal
isOpen={modalIsOpen}
onDismiss={() => {
setModalIsOpen(false)
}}
minHeight={null}
>
<PoolModal>
{poolTabOrder.map(({ path, textKey, regex }) => (
<StyledNavLink
key={path}
to={path}
isActive={(_, { pathname }) => pathname.match(regex)}
onClick={() => {
setModalIsOpen(false)
}}
>
{t(textKey)}
</StyledNavLink>
))}
</PoolModal>
</Modal>
</OversizedPanel>
)
}

@ -1,10 +1,11 @@
import React, { useState, useEffect, useCallback } from 'react'
import classnames from 'classnames'
import { useTranslation } from 'react-i18next'
import ReactGA from 'react-ga'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import styled from 'styled-components'
import { Button } from '../../theme'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import ContextualInfo from '../../components/ContextualInfo'
import OversizedPanel from '../../components/OversizedPanel'
@ -25,6 +26,69 @@ const DEADLINE_FROM_NOW = 60 * 15
// denominated in bips
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
const BlueSpan = styled.span`
color: ${({ theme }) => theme.royalBlue};
`
const DownArrowBackground = styled.div`
${({ theme }) => theme.flexRowNoWrap}
justify-content: center;
align-items: center;
`
const DownArrow = styled.img`
width: 0.625rem;
height: 0.625rem;
position: relative;
padding: 0.875rem;
`
const RemoveLiquidityOutput = styled.div`
${({ theme }) => theme.flexRowNoWrap}
min-height: 3.5rem;
`
const RemoveLiquidityOutputText = styled.div`
font-size: 1.25rem;
line-height: 1.5rem;
padding: 1rem 0.75rem;
`
const RemoveLiquidityOutputPlus = styled.div`
font-size: 1.25rem;
line-height: 1.5rem;
padding: 1rem 0;
`
const SummaryPanel = styled.div`
${({ theme }) => theme.flexColumnNoWrap}
padding: 1rem 0;
`
const ExchangeRateWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
color: ${({ theme }) => theme.doveGray};
font-size: 0.75rem;
padding: 0.25rem 1rem 0;
`
const ExchangeRate = styled.span`
flex: 1 1 auto;
width: 0;
color: ${({ theme }) => theme.chaliceGray};
`
const Flex = styled.div`
display: flex;
justify-content: center;
padding: 2rem;
button {
max-width: 20rem;
}
`
function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) {
try {
if (
@ -193,7 +257,7 @@ export default function RemoveLiquidity() {
})
}
const b = text => <span className="swap__highlight-text">{text}</span>
const b = text => <BlueSpan>{text}</BlueSpan>
function renderTransactionDetails() {
ReactGA.event({
@ -203,17 +267,17 @@ export default function RemoveLiquidity() {
return (
<div>
<div className="pool__summary-modal__item">
<div>
{t('youAreRemoving')} {b(`${amountFormatter(ethWithdrawnMin, 18, 4)} ETH`)} {t('and')}{' '}
{b(`${amountFormatter(tokenWithdrawnMin, decimals, Math.min(decimals, 4))} ${symbol}`)} {t('outPool')}
</div>
<div className="pool__summary-modal__item">
<div>
{t('youWillRemove')} {b(amountFormatter(valueParsed, 18, 4))} {t('liquidityTokens')}
</div>
<div className="pool__summary-modal__item">
<div>
{t('totalSupplyIs')} {b(amountFormatter(totalPoolTokens, 18, 4))}
</div>
<div className="pool__summary-modal__item">
<div>
{t('tokenWorth')} {b(amountFormatter(ETHPer, 18, 4))} ETH {t('and')}{' '}
{b(amountFormatter(tokenPer, decimals, Math.min(4, decimals)))} {symbol}
</div>
@ -278,9 +342,9 @@ export default function RemoveLiquidity() {
selectedTokenAddress={outputCurrency}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img className="swap__down-arrow" src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
</div>
<DownArrowBackground>
<DownArrow src={isActive ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
</DownArrowBackground>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
@ -288,35 +352,30 @@ export default function RemoveLiquidity() {
key="remove-liquidity-input"
renderInput={() =>
ethWithdrawn && tokenWithdrawn ? (
<div className="remove-liquidity__output">
<div className="remove-liquidity__output-text">{`${amountFormatter(
ethWithdrawn,
18,
4,
false
)} ETH`}</div>
<div className="remove-liquidity__output-plus"> + </div>
<div className="remove-liquidity__output-text">{`${amountFormatter(
tokenWithdrawn,
decimals,
Math.min(4, decimals)
)} ${symbol}`}</div>
</div>
<RemoveLiquidityOutput>
<RemoveLiquidityOutputText>
{`${amountFormatter(ethWithdrawn, 18, 4, false)} ETH`}
</RemoveLiquidityOutputText>
<RemoveLiquidityOutputPlus> + </RemoveLiquidityOutputPlus>
<RemoveLiquidityOutputText>
{`${amountFormatter(tokenWithdrawn, decimals, Math.min(4, decimals))} ${symbol}`}
</RemoveLiquidityOutputText>
</RemoveLiquidityOutput>
) : (
<div className="remove-liquidity__output" />
<RemoveLiquidityOutput />
)
}
disableTokenSelect
disableUnlock
/>
<OversizedPanel key="remove-liquidity-input-under" hideBottom>
<div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('exchangeRate')}</span>
<SummaryPanel>
<ExchangeRateWrapper>
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
{marketRate ? <span>{`1 ETH = ${amountFormatter(marketRate, 18, 4)} ${symbol}`}</span> : ' - '}
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('currentPoolSize')}</span>
</ExchangeRateWrapper>
<ExchangeRateWrapper>
<ExchangeRate>{t('currentPoolSize')}</ExchangeRate>
{exchangeETHBalance && exchangeTokenBalance && decimals ? (
<span>{`${amountFormatter(exchangeETHBalance, 18, 4)} ETH + ${amountFormatter(
exchangeTokenBalance,
@ -326,11 +385,11 @@ export default function RemoveLiquidity() {
) : (
' - '
)}
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">
</ExchangeRateWrapper>
<ExchangeRateWrapper>
<ExchangeRate>
{t('yourPoolShare')} ({ownershipPercentageFormatted && ownershipPercentageFormatted}%)
</span>
</ExchangeRate>
{ETHOwnShare && TokenOwnShare ? (
<span>
{`${amountFormatter(ETHOwnShare, 18, 4)} ETH + ${amountFormatter(
@ -342,21 +401,15 @@ export default function RemoveLiquidity() {
) : (
' - '
)}
</div>
</div>
</ExchangeRateWrapper>
</SummaryPanel>
</OversizedPanel>
{renderSummary()}
<div className="pool__cta-container">
<button
className={classnames('pool__cta-btn', {
'pool__cta-btn--inactive': !isActive
})}
disabled={!isValid}
onClick={onRemoveLiquidity}
>
<Flex>
<Button disabled={!isValid} onClick={onRemoveLiquidity} fullWidth>
{t('removeLiquidity')}
</button>
</div>
</Button>
</Flex>
</>
)
}

@ -1,13 +1,12 @@
import React, { useEffect } from 'react'
import React, { Suspense, lazy, useEffect } from 'react'
import ReactGA from 'react-ga'
import { Switch, Route, Redirect } from 'react-router-dom'
import ModeSelector from './ModeSelector'
import AddLiquidity from './AddLiquidity'
import CreateExchange from './CreateExchange'
import RemoveLiquidity from './RemoveLiquidity'
import './pool.scss'
const AddLiquidity = lazy(() => import('./AddLiquidity'))
const RemoveLiquidity = lazy(() => import('./RemoveLiquidity'))
const CreateExchange = lazy(() => import('./CreateExchange'))
export default function Pool() {
useEffect(() => {
@ -17,20 +16,23 @@ export default function Pool() {
return (
<>
<ModeSelector />
<Switch>
<Route exact strict path="/add-liquidity" component={AddLiquidity} />
<Route exact strict path="/remove-liquidity" component={RemoveLiquidity} />
<Route exact strict path="/create-exchange" component={CreateExchange} />
<Route
path="/create-exchange/:tokenAddress"
render={({ match }) => {
return (
<Redirect to={{ pathname: '/create-exchange', state: { tokenAddress: match.params.tokenAddress } }} />
)
}}
/>
<Redirect to="/add-liquidity" />
</Switch>
{/* this Suspense is for route code-splitting */}
<Suspense fallback={null}>
<Switch>
<Route exact strict path="/add-liquidity" component={AddLiquidity} />
<Route exact strict path="/remove-liquidity" component={RemoveLiquidity} />
<Route exact strict path="/create-exchange" component={CreateExchange} />
<Route
path="/create-exchange/:tokenAddress"
render={({ match }) => {
return (
<Redirect to={{ pathname: '/create-exchange', state: { tokenAddress: match.params.tokenAddress } }} />
)
}}
/>
<Redirect to="/add-liquidity" />
</Switch>
</Suspense>
</>
)
}

@ -1,187 +0,0 @@
@import '../../variables.scss';
.pool {
@extend %col-nowrap;
height: 100%;
background-color: $white;
&__content {
padding: 1rem 0.75rem;
flex: 1 1 auto;
height: 0;
overflow-y: auto;
}
&__liquidity-container {
@extend %row-nowrap;
align-items: center;
font-size: 0.75rem;
padding: 0.625rem 1rem;
font-size: 0.75rem;
color: $royal-blue;
font-weight: 500;
cursor: pointer;
img {
height: 0.75rem;
width: 0.75rem;
}
}
&__liquidity-label {
flex: 1 0 auto;
}
&__summary-panel {
@extend %col-nowrap;
padding: 1rem 0;
}
&__last-summary-text {
margin-top: 1rem;
}
&__exchange-rate-wrapper {
@extend %row-nowrap;
align-items: center;
color: $dove-gray;
font-size: 0.75rem;
padding: 0.25rem 1rem 0;
}
&__exchange-rate {
flex: 1 1 auto;
width: 0;
color: $chalice-gray;
}
&__cta-container {
display: flex;
}
&__cta-btn {
@extend %primary-button;
margin: 2rem auto 0;
&--inactive {
background: $mercury-gray;
}
}
&__new-exchange-warning {
margin-top: 1rem;
padding: 1rem;
margin-bottom: 2rem;
border: 1px solid rgba($pizazz-orange, 0.4);
background-color: rgba($pizazz-orange, 0.1);
border-radius: 1rem;
}
&__new-exchange-warning-text {
font-size: 0.75rem;
line-height: 1rem;
text-align: center;
&:first-child {
padding-bottom: 0.3rem;
font-weight: 500;
}
}
&__summary-item {
&:not(:last-child) {
margin-bottom: 0.5rem;
}
}
}
.pool-modal {
background-color: $white;
position: relative;
bottom: 11.0625rem;
width: 100%;
height: 11.0625rem;
z-index: 2000;
border-top-left-radius: 1rem;
border-top-right-radius: 1rem;
transition: 250ms ease-in-out;
padding: 1rem 0 0.5rem;
@media only screen and (min-width: 768px) {
max-width: 560px;
position: absolute;
margin-left: auto;
margin-right: auto;
border-radius: 1rem;
padding-bottom: 1rem;
left: 0;
right: 0;
bottom: 0;
top: 0;
margin-top: 4rem;
}
&__item {
@extend %row-nowrap;
padding: 1rem;
margin-left: 1rem;
margin-right: 1rem;
font-size: 1rem;
cursor: pointer;
text-decoration: none;
color: $dove-gray;
font-size: 1rem;
&--selected {
background-color: $white;
border-radius: 3rem;
box-shadow: 0 0 0.5px 0.5px $mercury-gray;
font-weight: 500;
color: $royal-blue;
}
}
}
.pool-modal-appear {
bottom: 0;
}
.pool-modal-appear.modal-container-appear-active {
bottom: 0;
}
.create-exchange {
&__summary-panel {
color: $dove-gray;
text-align: center;
margin-top: 1rem;
padding-top: 1rem;
}
&__summary-text {
font-size: 0.75rem;
}
&--error {
color: $salmon-red;
}
}
.remove-liquidity {
&__output {
@extend %row-nowrap;
min-height: 3.5rem;
}
&__output-text {
font-size: 1.25rem;
line-height: 1.5rem;
padding: 1rem 0.75rem;
}
&__output-plus {
font-size: 1.25rem;
line-height: 1.5rem;
padding: 1rem 0;
}
}

@ -3,7 +3,9 @@ import ReactGA from 'react-ga'
import { useTranslation } from 'react-i18next'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import styled from 'styled-components'
import { Button } from '../../theme'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import NewContextualInfo from '../../components/ContextualInfoNew'
import OversizedPanel from '../../components/OversizedPanel'
@ -17,8 +19,6 @@ import { useTransactionAdder } from '../../contexts/Transactions'
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances'
import './send.scss'
const INPUT = 0
const OUTPUT = 1
@ -36,6 +36,52 @@ const DEADLINE_FROM_NOW = 60 * 15
// denominated in bips
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
const BlueSpan = styled.span`
color: ${({ theme }) => theme.royalBlue};
`
const DownArrowBackground = styled.div`
${({ theme }) => theme.flexRowNoWrap}
justify-content: center;
align-items: center;
`
const DownArrow = styled.img`
width: 0.625rem;
height: 0.625rem;
position: relative;
padding: 0.875rem;
cursor: ${({ clickable }) => clickable && 'pointer'};
`
const LastSummaryText = styled.div`
margin-top: 1rem;
`
const ExchangeRateWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
color: ${({ theme }) => theme.doveGray};
font-size: 0.75rem;
padding: 0.5rem 1rem;
`
const ExchangeRate = styled.span`
flex: 1 1 auto;
width: 0;
color: ${({ theme }) => theme.chaliceGray};
`
const Flex = styled.div`
display: flex;
justify-content: center;
padding: 2rem;
button {
max-width: 20rem;
}
`
function calculateSlippageBounds(value, token = false) {
if (value) {
const offset = value.mul(token ? TOKEN_ALLOWED_SLIPPAGE : ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000))
@ -455,7 +501,7 @@ export default function Swap() {
action: 'Open'
})
const b = text => <span className="swap__highlight-text">{text}</span>
const b = text => <BlueSpan>{text}</BlueSpan>
if (independentField === INPUT) {
return (
@ -471,7 +517,7 @@ export default function Swap() {
)}
.
</div>
<div className="send__last-summary-text">
<LastSummaryText>
{b(recipient.address)} {t('willReceive')}{' '}
{b(
`${amountFormatter(
@ -481,10 +527,10 @@ export default function Swap() {
)} ${outputSymbol}`
)}{' '}
{t('orTransFail')}
</div>
<div className="send__last-summary-text">
</LastSummaryText>
<LastSummaryText>
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
</div>
</LastSummaryText>
</div>
)
} else {
@ -501,7 +547,7 @@ export default function Swap() {
)}{' '}
{t('to')} {b(recipient.address)}.
</div>
<div className="send__last-summary-text">
<LastSummaryText>
{t('itWillCost')}{' '}
{b(
`${amountFormatter(
@ -511,10 +557,10 @@ export default function Swap() {
)} ${inputSymbol}`
)}{' '}
{t('orTransFail')}
</div>
<div className="send__last-summary-text">
</LastSummaryText>
<LastSummaryText>
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
</div>
</LastSummaryText>
</div>
)
}
@ -652,16 +698,16 @@ export default function Swap() {
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img
<DownArrowBackground>
<DownArrow
onClick={() => {
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
}}
className="swap__down-arrow swap__down-arrow--clickable"
clickable
alt="swap"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
/>
</div>
</DownArrowBackground>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
@ -680,19 +726,18 @@ export default function Swap() {
disableUnlock
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img className="swap__down-arrow" src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
</div>
<DownArrowBackground>
<DownArrow src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
</DownArrowBackground>
</OversizedPanel>
<AddressInputPanel onChange={setRecipient} onError={setRecipientError} />
<OversizedPanel hideBottom>
<div
className="swap__exchange-rate-wrapper"
<ExchangeRateWrapper
onClick={() => {
setInverted(inverted => !inverted)
}}
>
<span className="swap__exchange-rate">{t('exchangeRate')}</span>
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
{inverted ? (
<span>
{exchangeRate
@ -706,14 +751,14 @@ export default function Swap() {
: ' - '}
</span>
)}
</div>
</ExchangeRateWrapper>
</OversizedPanel>
{renderSummary()}
<div className="swap__cta-container">
<button className="swap__cta-btn" disabled={!isValid} onClick={onSwap}>
<Flex>
<Button disabled={!isValid} onClick={onSwap} fullWidth>
{t('swap')}
</button>
</div>
</Button>
</Flex>
</>
)
}

@ -1,11 +0,0 @@
@import '../../variables.scss';
.send {
@extend %col-nowrap;
height: 100%;
background-color: $white;
&__last-summary-text {
margin-top: 1rem;
}
}

@ -3,7 +3,9 @@ import ReactGA from 'react-ga'
import { useTranslation } from 'react-i18next'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import styled from 'styled-components'
import { Button } from '../../theme'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import NewContextualInfo from '../../components/ContextualInfoNew'
import OversizedPanel from '../../components/OversizedPanel'
@ -16,8 +18,6 @@ import { useTransactionAdder } from '../../contexts/Transactions'
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances'
import './swap.scss'
const INPUT = 0
const OUTPUT = 1
@ -35,6 +35,52 @@ const DEADLINE_FROM_NOW = 60 * 15
// denominated in bips
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
const BlueSpan = styled.span`
color: ${({ theme }) => theme.royalBlue};
`
const LastSummaryText = styled.div`
margin-top: 1rem;
`
const DownArrowBackground = styled.div`
${({ theme }) => theme.flexRowNoWrap}
justify-content: center;
align-items: center;
`
const DownArrow = styled.img`
width: 0.625rem;
height: 0.625rem;
position: relative;
padding: 0.875rem;
cursor: ${({ clickable }) => clickable && 'pointer'};
`
const ExchangeRateWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
color: ${({ theme }) => theme.doveGray};
font-size: 0.75rem;
padding: 0.5rem 1rem;
`
const ExchangeRate = styled.span`
flex: 1 1 auto;
width: 0;
color: ${({ theme }) => theme.chaliceGray};
`
const Flex = styled.div`
display: flex;
justify-content: center;
padding: 2rem;
button {
max-width: 20rem;
}
`
function calculateSlippageBounds(value, token = false) {
if (value) {
const offset = value.mul(token ? TOKEN_ALLOWED_SLIPPAGE : ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000))
@ -450,7 +496,7 @@ export default function Swap() {
action: 'Open'
})
const b = text => <span className="swap__highlight-text">{text}</span>
const b = text => <BlueSpan>{text}</BlueSpan>
if (independentField === INPUT) {
return (
@ -466,7 +512,7 @@ export default function Swap() {
)}
.
</div>
<div className="send__last-summary-text">
<LastSummaryText>
{t('youWillReceive')}{' '}
{b(
`${amountFormatter(
@ -476,10 +522,10 @@ export default function Swap() {
)} ${outputSymbol}`
)}{' '}
{t('orTransFail')}
</div>
<div className="send__last-summary-text">
</LastSummaryText>
<LastSummaryText>
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
</div>
</LastSummaryText>
</div>
)
} else {
@ -496,7 +542,7 @@ export default function Swap() {
)}
.
</div>
<div className="send__last-summary-text">
<LastSummaryText>
{t('itWillCost')}{' '}
{b(
`${amountFormatter(
@ -506,10 +552,10 @@ export default function Swap() {
)} ${inputSymbol}`
)}{' '}
{t('orTransFail')}
</div>
<div className="send__last-summary-text">
</LastSummaryText>
<LastSummaryText>
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
</div>
</LastSummaryText>
</div>
)
}
@ -629,16 +675,16 @@ export default function Swap() {
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img
<DownArrowBackground>
<DownArrow
onClick={() => {
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
}}
className="swap__down-arrow swap__down-arrow--clickable"
clickable
alt="swap"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
/>
</div>
</DownArrowBackground>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
@ -657,13 +703,12 @@ export default function Swap() {
disableUnlock
/>
<OversizedPanel hideBottom>
<div
className="swap__exchange-rate-wrapper"
<ExchangeRateWrapper
onClick={() => {
setInverted(inverted => !inverted)
}}
>
<span className="swap__exchange-rate">{t('exchangeRate')}</span>
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
{inverted ? (
<span>
{exchangeRate
@ -677,14 +722,14 @@ export default function Swap() {
: ' - '}
</span>
)}
</div>
</ExchangeRateWrapper>
</OversizedPanel>
{renderSummary()}
<div className="swap__cta-container">
<button className="swap__cta-btn" disabled={!isValid} onClick={onSwap}>
<Flex>
<Button disabled={!isValid} onClick={onSwap}>
{t('swap')}
</button>
</div>
</Button>
</Flex>
</>
)
}

@ -1,81 +0,0 @@
@import '../../variables.scss';
.swap {
@extend %col-nowrap;
height: 100%;
background-color: $white;
&--inactive {
opacity: 0.5;
cursor: default;
}
&__content {
padding: 1rem 0.75rem;
flex: 1 1 auto;
height: 0;
overflow-y: auto;
}
&__down-arrow {
width: 0.625rem;
height: 0.625rem;
position: relative;
z-index: 200;
padding: 0.875rem;
&--clickable {
cursor: pointer;
}
}
&__down-arrow-background {
@extend %row-nowrap;
justify-content: center;
align-items: center;
}
&__exchange-rate-wrapper {
@extend %row-nowrap;
align-items: center;
color: $dove-gray;
font-size: 0.75rem;
padding: 0.5rem 1rem;
}
&__exchange-rate {
flex: 1 1 auto;
width: 0;
color: $chalice-gray;
}
&__highlight-text {
color: $royal-blue;
}
&__cta-container {
display: flex;
margin-top: 1rem;
}
&__cta-btn {
@extend %primary-button;
margin: 1rem auto;
}
&__sub-icon {
margin-right: 5px;
}
&__sub-text {
margin-top: 5px;
}
}
.summary-modal-appear {
bottom: 0;
}
.summary-modal-appear.modal-container-appear-active {
bottom: 0;
}

70
src/theme/components.js Normal file

@ -0,0 +1,70 @@
import styled from 'styled-components'
import { lighten, darken } from 'polished'
export const Button = styled.button`
padding: 1rem 2rem 1rem 2rem;
border-radius: 3rem;
cursor: pointer;
user-select: none;
font-size: 1rem;
border: none;
outline: none;
background-color: ${({ theme }) => theme.royalBlue};
color: ${({ theme }) => theme.white};
transition: background-color 125ms ease-in-out;
width: 100%;
:hover,
:focus {
background-color: ${({ theme }) => lighten(0.05, theme.royalBlue)};
}
:active {
background-color: ${({ theme }) => darken(0.05, theme.royalBlue)};
}
:disabled {
background-color: ${({ theme }) => theme.mercuryGray};
cursor: auto;
}
`
export const Link = styled.a.attrs({
target: '_blank',
rel: 'noopener noreferrer'
})`
text-decoration: none;
cursor: pointer;
color: ${({ theme }) => theme.royalBlue};
:focus {
outline: none;
text-decoration: underline;
}
:active {
text-decoration: none;
}
`
export const BorderlessInput = styled.input`
color: ${({ theme }) => theme.mineshaftGray};
font-size: 1rem;
outline: none;
border: none;
flex: 1 1 auto;
width: 0;
[type='number'] {
-moz-appearance: textfield;
}
::-webkit-outer-spin-button,
::-webkit-inner-spin-button {
-webkit-appearance: none;
}
::placeholder {
color: ${({ theme }) => theme.mercuryGray};
}
`

@ -1,12 +1,71 @@
import React from 'react'
import { ThemeProvider as StyledComponentsThemeProvider, createGlobalStyle } from 'styled-components'
import { ThemeProvider as StyledComponentsThemeProvider, createGlobalStyle, css } from 'styled-components'
export * from './components'
const MEDIA_WIDTHS = {
upToSmall: 600,
upToMedium: 960,
upToLarge: 1280
}
const mediaWidthTemplates = Object.keys(MEDIA_WIDTHS).reduce((accumulator, size) => {
accumulator[size] = (...args) => css`
@media (max-width: ${MEDIA_WIDTHS[size]}px) {
${css(...args)}
}
`
return accumulator
}, {})
const mediaHeightTemplates = Object.keys(MEDIA_WIDTHS).reduce((accumulator, size) => {
accumulator[size] = (...args) => css`
@media (max-height: ${MEDIA_WIDTHS[size] / (16 / 9)}px) {
${css(...args)}
}
`
return accumulator
}, {})
const flexColumnNoWrap = css`
display: flex;
flex-flow: column nowrap;
`
const flexRowNoWrap = css`
display: flex;
flex-flow: row nowrap;
`
const theme = {
white: '#FFFFFF',
black: '#000000',
// grays
concreteGray: '#FAFAFA',
mercuryGray: '#E1E1E1',
silverGray: '#C4C4C4',
chaliceGray: '#AEAEAE',
doveGray: '#737373',
mineshaftGray: '#2B2B2B',
// blues
zumthorBlue: '#EBF4FF',
malibuBlue: '#5CA2FF',
royalBlue: '#2F80ED',
// purples
wisteriaPurple: '#DC6BE5',
// reds
salmonRed: '#FF6871',
// orange
pizazzOrange: '#FF8F05',
// pink
uniswapPink: '#DC6BE5',
royalBlue: '#2f80ed',
salmonRed: '#ff6871',
white: '#FFF',
black: '#000'
connectedGreen: '#27AE60',
// media queries
mediaWidth: mediaWidthTemplates,
mediaHeight: mediaHeightTemplates,
// css snippets
flexColumnNoWrap,
flexRowNoWrap
}
export default function ThemeProvider({ children }) {
@ -15,60 +74,29 @@ export default function ThemeProvider({ children }) {
export const GlobalStyle = createGlobalStyle`
@import url('https://rsms.me/inter/inter.css');
html { font-family: 'Inter', sans-serif; }
@supports (font-variation-settings: normal) {
html { font-family: 'Inter var', sans-serif; }
}
html,
body {
padding: 0;
margin: 0;
font-family: Inter, sans-serif;
font-variant: none;
padding: 0;
font-size: 16px;
font-variant: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
#root {
position: relative;
display: flex;
flex-flow: column nowrap;
height: 100vh;
${({ theme }) => theme.flexColumnNoWrap}
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
overflow-x: hidden;
overflow-y: auto;
background-color: ${props => props.theme.white};
z-index: 100;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
@media only screen and (min-width: 768px) {
justify-content: center;
align-items: center;
}
}
#modal-root {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 200;
}
.loader {
border: 1px solid transparent;
border-top: 1px solid ${props => props.theme.royalBlue};
border-radius: 50%;
width: 0.75rem;
height: 0.75rem;
margin-right: 0.25rem;
animation: spin 1s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`

@ -1,9 +1,9 @@
import { ethers } from 'ethers'
import FACTORY_ABI from '../abi/factory'
import EXCHANGE_ABI from '../abi/exchange'
import ERC20_ABI from '../abi/erc20'
import ERC20_WITH_BYTES_ABI from '../abi/erc20_bytes32'
import FACTORY_ABI from '../constants/abis/factory'
import EXCHANGE_ABI from '../constants/abis/exchange'
import ERC20_ABI from '../constants/abis/erc20'
import ERC20_BYTES32_ABI from '../constants/abis/erc20_bytes32'
import { FACTORY_ADDRESSES } from '../constants'
export const ERROR_CODES = ['TOKEN_NAME', 'TOKEN_SYMBOL', 'TOKEN_DECIMALS'].reduce(
@ -23,6 +23,61 @@ export function safeAccess(object, path) {
: null
}
const ETHERSCAN_PREFIXES = {
1: '',
3: 'ropsten.',
4: 'rinkeby.',
5: 'goerli.',
42: 'kovan.'
}
export function getEtherscanLink(networkId, data, type) {
const prefix = `https://${ETHERSCAN_PREFIXES[networkId] || ETHERSCAN_PREFIXES[1]}etherscan.io`
switch (type) {
case 'transaction': {
return `${prefix}/tx/${data}`
}
case 'address':
default: {
return `${prefix}/address/${data}`
}
}
}
export function getNetworkName(networkId) {
switch (networkId) {
case 1: {
return 'the Main Ethereum Network'
}
case 3: {
return 'the Ropsten Test Network'
}
case 4: {
return 'the Rinkeby Test Network'
}
case 5: {
return 'the Görli Test Network'
}
case 42: {
return 'the Kovan Test Network'
}
default: {
return 'the correct network'
}
}
}
export function shortenAddress(address, digits = 4) {
if (!isAddress(address)) {
throw Error(`Invalid 'address' parameter '${address}'.`)
}
return `${address.substring(0, digits + 2)}...${address.substring(42 - digits)}`
}
export function shortenTransactionHash(hash, digits = 4) {
return `${hash.substring(0, digits + 2)}...${hash.substring(66 - digits)}`
}
export function isAddress(value) {
try {
return ethers.utils.getAddress(value.toLowerCase())
@ -69,7 +124,7 @@ export async function getTokenName(tokenAddress, library) {
return getContract(tokenAddress, ERC20_ABI, library)
.name()
.catch(() =>
getContract(tokenAddress, ERC20_WITH_BYTES_ABI, library)
getContract(tokenAddress, ERC20_BYTES32_ABI, library)
.name()
.then(bytes32 => ethers.utils.parseBytes32String(bytes32))
)
@ -88,7 +143,7 @@ export async function getTokenSymbol(tokenAddress, library) {
return getContract(tokenAddress, ERC20_ABI, library)
.symbol()
.catch(() => {
const contractBytes32 = getContract(tokenAddress, ERC20_WITH_BYTES_ABI, library)
const contractBytes32 = getContract(tokenAddress, ERC20_BYTES32_ABI, library)
return contractBytes32.symbol().then(bytes32 => ethers.utils.parseBytes32String(bytes32))
})
.catch(error => {

@ -1,74 +0,0 @@
$white: #fff;
$black: #000;
// Gray
$concrete-gray: #fafafa;
$mercury-gray: #e1e1e1;
$silver-gray: #c4c4c4;
$chalice-gray: #aeaeae;
$dove-gray: #737373;
$mine-shaft-gray: #2b2b2b;
// Blue
$zumthor-blue: #ebf4ff;
$malibu-blue: #5ca2ff;
$royal-blue: #2f80ed;
// Purple
$wisteria-purple: #dc6be5;
// Red
$salmon-red: #ff6871;
// Orange
$pizazz-orange: #ff8f05;
%col-nowrap {
display: flex;
flex-flow: column nowrap;
}
%row-nowrap {
display: flex;
flex-flow: row nowrap;
}
%primary-button {
font-size: 1rem;
min-width: 16rem;
padding: 1rem;
background-color: $royal-blue;
cursor: pointer;
border-radius: 3rem;
color: $white;
outline: none;
border: 1px solid transparent;
user-select: none;
transition: background-color 300ms ease-in-out;
&:hover {
background-color: lighten($royal-blue, 5);
}
&:active {
background-color: darken($royal-blue, 5);
}
&:disabled {
background-color: $mercury-gray;
cursor: auto;
}
}
%borderless-input {
color: $mine-shaft-gray;
font-size: 1rem;
outline: none;
border: none;
flex: 1 1 auto;
width: 0;
&::placeholder {
color: $mercury-gray;
}
}

2804
yarn.lock

File diff suppressed because it is too large Load Diff