on master help sign in
feat: implement progress button for ethereum transactions
54fae1cMatt Condon (shrugs)
src/components/CopiableAddress.jsnew file+23
@@ -0,0 +1,23 @@
1+import React from 'react';
2+import cn from 'classnames';
3+
4+import useCopiable from 'lib/useCopiable';
5+
6+export default function CopiableAddress({
7+ as: As = 'span',
8+ children,
9+ className,
10+}) {
11+ const [doCopy, didCopy] = useCopiable(children);
12+
13+ return (
14+ <As
15+ onClick={doCopy}
16+ className={cn(className, 'mono', {
17+ pointer: !didCopy,
18+ green3: didCopy,
19+ })}>
20+ {children}
21+ </As>
22+ );
23+}
src/components/InlineEthereumTransaction.js+14−3
@@ -21,6 +21,7 @@ import Condition from 'form/Condition';
21import { GenerateButton, ForwardButton, RestartButton } from './Buttons';
21import { GenerateButton, ForwardButton, RestartButton } from './Buttons';
22import WarningBox from './WarningBox';
22import WarningBox from './WarningBox';
23import CopyButton from './CopyButton';
23import CopyButton from './CopyButton';
24+import ProgressButton from './ProgressButton';
24
25
25export default function InlineEthereumTransaction({
26export default function InlineEthereumTransaction({
26 // from useEthereumTransaction.bind
27 // from useEthereumTransaction.bind
@@ -42,6 +43,7 @@ export default function InlineEthereumTransaction({
42 chainId,
43 chainId,
43 needFunds,
44 needFunds,
44 signedTransaction,
45 signedTransaction,
46+ confirmationProgress,
45
47
46 // additional from parent
48 // additional from parent
47 label = 'Generate & Sign Transaction',
49 label = 'Generate & Sign Transaction',
@@ -53,9 +55,9 @@ export default function InlineEthereumTransaction({
53 // show configure controls pre-broadcast
55 // show configure controls pre-broadcast
54 const showConfigureInput = !(signed || broadcasted || confirmed || completed);
56 const showConfigureInput = !(signed || broadcasted || confirmed || completed);
55 // show the send/loading button while signed, broadcasting, or confirme
57 // show the send/loading button while signed, broadcasting, or confirme
56− const showBroadcastButton = signed || broadcasted || confirmed;
58+ const showBroadcastButton = signed;
59+ const showLoadingButton = broadcasted || confirmed;
57 const canBroadcast = signed && !needFunds;
60 const canBroadcast = signed && !needFunds;
58− const isLoading = broadcasted || confirmed;
59 // show signed tx only when signing (for offline usage)
61 // show signed tx only when signing (for offline usage)
60 const showSignedTx = signed;
62 const showSignedTx = signed;
61
63
@@ -100,11 +102,20 @@ export default function InlineEthereumTransaction({
100 solid
102 solid
101 success
103 success
102 disabled={!canBroadcast}
104 disabled={!canBroadcast}
103− loading={isLoading}
104 onClick={() => broadcast()}>
105 onClick={() => broadcast()}>
105 Send Transaction
106 Send Transaction
106 </Grid.Item>
107 </Grid.Item>
107 );
108 );
109+ } else if (showLoadingButton) {
110+ return (
111+ <Grid.Item
112+ full
113+ as={ProgressButton}
114+ disabled={true}
115+ progress={confirmationProgress}>
116+ Sending Transaction
117+ </Grid.Item>
118+ );
108 } else {
119 } else {
109 return (
120 return (
110 <Grid.Item
121 <Grid.Item
src/components/LoadableButton.jsdeleted−7
@@ -1,7 +0,0 @@
1−import React from 'react';
2−import { Button } from 'indigo-react';
3−
4−// TODO: implement loading bar behind text
5−export default function LoadableButton({ ...rest }) {
6− return <Button {...rest}></Button>;
7−}
src/components/LoadingBar.js+2−2
@@ -2,11 +2,11 @@ import React from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3
3
4// progrss is [0, 1]
4// progrss is [0, 1]
5−export default function LoadingBar({ className, progress = 1 }) {
5+export default function LoadingBar({ className, progress = 1.0 }) {
6 return (
6 return (
7 <div className={cn('rel bg-gray2', className)} style={{ height: '4px' }}>
7 <div className={cn('rel bg-gray2', className)} style={{ height: '4px' }}>
8 <div
8 <div
9− className="abs bg-green3"
9+ className="abs bg-green3 animated-width"
10 style={{
10 style={{
11 top: 0,
11 top: 0,
12 bottom: 0,
12 bottom: 0,
src/components/ProgressButton.jsnew file+32
@@ -0,0 +1,32 @@
1+import React from 'react';
2+import cn from 'classnames';
3+import { Button } from 'indigo-react';
4+import { blinkIf } from './Blinky';
5+
6+export default function ProgressButton({
7+ as: As = Button,
8+ progress = 0.0,
9+ color = 'green4',
10+ success = true,
11+ ...rest
12+}) {
13+ return (
14+ <As
15+ {...rest}
16+ solid
17+ success={success}
18+ accessory={blinkIf(progress < 1.0, '->')}
19+ background={
20+ <span
21+ className={cn('abs z1 animated-width', `bg-${color}`)}
22+ style={{
23+ left: 0,
24+ top: 0,
25+ right: 0,
26+ bottom: 0,
27+ width: `${progress * 100.0}%`,
28+ }}></span>
29+ }
30+ />
31+ );
32+}
src/indigo-react/components/Button.js+6−4
@@ -15,6 +15,7 @@ export default function Button({
15 className,
15 className,
16 accessory = '→',
16 accessory = '→',
17 onClick,
17 onClick,
18+ background,
18 children,
19 children,
19 ...rest
20 ...rest
20}) {
21}) {
@@ -23,7 +24,7 @@ export default function Button({
23 as={As}
24 as={As}
24 gap={1}
25 gap={1}
25 className={cn(
26 className={cn(
26− 'pointer pv4 truncate flex-row justify-between us-none',
27+ 'rel pointer pv4 truncate flex-row justify-between us-none',
27 {
28 {
28 p4: solid,
29 p4: solid,
29 },
30 },
@@ -49,17 +50,18 @@ export default function Button({
49 }}
50 }}
50 onClick={!disabled && onClick ? onClick : undefined}
51 onClick={!disabled && onClick ? onClick : undefined}
51 {...rest}>
52 {...rest}>
53+ {background}
52− <Grid.Item full as={Flex} justify="between">
54+ <Grid.Item full as={Flex} justify="between" className="z2">
53 <span>{children}</span>
55 <span>{children}</span>
54 <div className={cn('pl4')}>{accessory}</div>
56 <div className={cn('pl4')}>{accessory}</div>
55 </Grid.Item>
57 </Grid.Item>
56 {detail && (
58 {detail && (
57− <Grid.Item full as={HelpText}>
59+ <Grid.Item full as={HelpText} className="z2">
58 {detail}
60 {detail}
59 </Grid.Item>
61 </Grid.Item>
60 )}
62 )}
61 {disabled && disabledDetail && (
63 {disabled && disabledDetail && (
62− <Grid.Item full className="f6 black mt1">
64+ <Grid.Item full className="f6 black mt1 z2">
63 {disabledDetail}
65 {disabledDetail}
64 </Grid.Item>
66 </Grid.Item>
65 )}
67 )}
src/lib/useEthereumTransaction.js+19−1
@@ -17,6 +17,7 @@ import * as need from 'lib/need';
17import { ensureFundsFor } from 'lib/tank';
17import { ensureFundsFor } from 'lib/tank';
18import useDeepEqualReference from './useDeepEqualReference';
18import useDeepEqualReference from './useDeepEqualReference';
19import useGasPrice from './useGasPrice';
19import useGasPrice from './useGasPrice';
20+import timeout from './timeout';
20
21
21const STATE = {
22const STATE = {
22 NONE: 'NONE',
23 NONE: 'NONE',
@@ -65,6 +66,7 @@ export default function useEthereumTransaction(
65 const [signedTransaction, setSignedTransaction] = useState();
66 const [signedTransaction, setSignedTransaction] = useState();
66 const [txHash, setTxHash] = useState();
67 const [txHash, setTxHash] = useState();
67 const [needFunds, setNeedFunds] = useState();
68 const [needFunds, setNeedFunds] = useState();
69+ const [confirmationProgress, setConfirmationProgress] = useState(0.0);
68
70
69 const initializing = nonce === undefined || chainId === undefined;
71 const initializing = nonce === undefined || chainId === undefined;
70 const constructed = !!unsignedTransaction;
72 const constructed = !!unsignedTransaction;
@@ -126,6 +128,7 @@ export default function useEthereumTransaction(
126
128
127 const broadcast = useCallback(async () => {
129 const broadcast = useCallback(async () => {
128 try {
130 try {
131+ setConfirmationProgress(0.0);
129 setError(undefined);
132 setError(undefined);
130
133
131 const rawTx = hexify(signedTransaction.serialize());
134 const rawTx = hexify(signedTransaction.serialize());
@@ -156,8 +159,14 @@ export default function useEthereumTransaction(
156 setState(STATE.BROADCASTED);
159 setState(STATE.BROADCASTED);
157 setTxHash(txHash);
160 setTxHash(txHash);
158
161
162+ await timeout(500); // wait for .animated-width to complete
163+
164+ setConfirmationProgress(0.2);
165+
159 await waitForTransactionConfirm(_web3, txHash);
166 await waitForTransactionConfirm(_web3, txHash);
160
167
168+ setConfirmationProgress(0.9);
169+
161 setState(STATE.CONFIRMED);
170 setState(STATE.CONFIRMED);
162 } catch (error) {
171 } catch (error) {
163 setError(error);
172 setError(error);
@@ -181,6 +190,7 @@ export default function useEthereumTransaction(
181 setState(STATE.NONE);
190 setState(STATE.NONE);
182 setError(undefined);
191 setError(undefined);
183 setNeedFunds(undefined);
192 setNeedFunds(undefined);
193+ setConfirmationProgress(0.0);
184 }, [resetGasPrice, setError]);
194 }, [resetGasPrice, setError]);
185
195
186 useEffect(() => {
196 useEffect(() => {
@@ -236,6 +246,14 @@ export default function useEthereumTransaction(
236 return;
246 return;
237 }
247 }
238
248
249+ setConfirmationProgress(1.0);
250+
251+ await timeout(500); // wait for .animated-width to complete
252+
253+ if (!mounted) {
254+ return;
255+ }
256+
239 setState(STATE.COMPLETED);
257 setState(STATE.COMPLETED);
240 })();
258 })();
241 }
259 }
@@ -259,7 +277,7 @@ export default function useEthereumTransaction(
259 reset,
277 reset,
260 error,
278 error,
261 inputsLocked,
279 inputsLocked,
262− //
280+ confirmationProgress,
263 txHash,
281 txHash,
264 signedTransaction,
282 signedTransaction,
265 gasPrice,
283 gasPrice,
src/style/bridge.scss+6
@@ -66,3 +66,9 @@ input[type='range'] {
66 background-color: #e6e6e6;
66 background-color: #e6e6e6;
67 }
67 }
68}
68}
69+
70+// animated width
71+
72+.animated-width {
73+ transition: width 500ms ease-out;
74+}
src/views/Invite/InviteEmail.js+2−2
@@ -41,7 +41,7 @@ import useSetState from 'lib/useSetState';
41import pluralize from 'lib/pluralize';
41import pluralize from 'lib/pluralize';
42import useMailer from 'lib/useMailer';
42import useMailer from 'lib/useMailer';
43
43
44−import LoadableButton from 'components/LoadableButton';
44+import ProgressButton from 'components/ProgressButton';
45import Highlighted from 'components/Highlighted';
45import Highlighted from 'components/Highlighted';
46import BridgeForm from 'form/BridgeForm';
46import BridgeForm from 'form/BridgeForm';
47import { Field } from 'react-final-form';
47import { Field } from 'react-final-form';
@@ -518,7 +518,7 @@ export default function InviteEmail() {
518 ) : (
518 ) : (
519 <Grid.Item
519 <Grid.Item
520 full
520 full
521− as={LoadableButton}
521+ as={ProgressButton}
522 className="mt4"
522 className="mt4"
523 disabled={!canSend}
523 disabled={!canSend}
524 accessory={`${visualProgress}/${fields.length}`}
524 accessory={`${visualProgress}/${fields.length}`}