Skip to content

Commit 096ad4f

Browse files
committed
frontend: use backbutton component with navigate -1 and enable-esc
Back buttons should not add new history entries and behave same as native back buttons by going one step back in history. Send and receive routes listen to esc key to go back, this is implemented via optional enableEsc prop.
1 parent 76be7da commit 096ad4f

File tree

14 files changed

+110
-108
lines changed

14 files changed

+110
-108
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Copyright 2024 Shift Crypto AG
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { ReactNode, useCallback } from 'react';
18+
import { useNavigate } from 'react-router-dom';
19+
import { useEsc } from '@/hooks/keyboard';
20+
import { Button } from '@/components/forms/button';
21+
22+
type TBackButton = {
23+
children: ReactNode;
24+
className?: string;
25+
disabled?: boolean;
26+
enableEsc?: boolean;
27+
};
28+
29+
export const BackButton = ({
30+
children,
31+
className,
32+
disabled,
33+
enableEsc,
34+
}: TBackButton) => {
35+
const navigate = useNavigate();
36+
37+
const handleBack = useCallback(() => {
38+
if (!disabled) {
39+
navigate(-1);
40+
}
41+
}, [disabled, navigate]);
42+
43+
useEsc(useCallback(() => enableEsc && handleBack(), [enableEsc, handleBack]));
44+
45+
return (
46+
<Button
47+
disabled={disabled}
48+
className={className}
49+
onClick={handleBack}
50+
secondary
51+
>
52+
{children}
53+
</Button>
54+
);
55+
};

frontends/web/src/routes/account/add/add.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export const AddAccount = ({ accounts }: TAddAccountGuide) => {
8888
const back = () => {
8989
switch (step) {
9090
case 'select-coin':
91-
navigate('/settings/manage-accounts');
91+
navigate(-1);
9292
break;
9393
case 'choose-name':
9494
setStep('select-coin');

frontends/web/src/routes/account/info/info.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* Copyright 2018 Shift Devices AG
3-
* Copyright 2022 Shift Crypto AG
3+
* Copyright 2022-2024 Shift Crypto AG
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -16,14 +16,12 @@
1616
*/
1717

1818
import { useState } from 'react';
19-
import { useNavigate } from 'react-router-dom';
2019
import { useTranslation } from 'react-i18next';
2120
import { useLoad } from '@/hooks/api';
22-
import { useEsc } from '@/hooks/keyboard';
2321
import { getInfo, IAccount, AccountCode } from '@/api/account';
2422
import { isBitcoinBased } from '@/routes/account/utils';
25-
import { ButtonLink } from '@/components/forms';
2623
import { Header } from '@/components/layout';
24+
import { BackButton } from '@/components/backbutton/backbutton';
2725
import { SigningConfiguration } from './signingconfiguration';
2826
import { BitcoinBasedAccountInfoGuide } from './guide';
2927
import style from './info.module.css';
@@ -37,14 +35,11 @@ export const Info = ({
3735
accounts,
3836
code,
3937
}: TProps) => {
40-
const navigate = useNavigate();
4138
const { t } = useTranslation();
4239
const info = useLoad(getInfo(code));
4340
const [viewXPub, setViewXPub] = useState<number>(0);
4441
const account = accounts.find(({ code: accountCode }) => accountCode === code);
4542

46-
useEsc(() => navigate(`/account/${code}`));
47-
4843
if (!account || !info) {
4944
return null;
5045
}
@@ -91,11 +86,9 @@ export const Info = ({
9186
code={code}
9287
info={config}
9388
signingConfigIndex={viewXPub}>
94-
<ButtonLink
95-
secondary
96-
to={`/account/${code}`}>
89+
<BackButton enableEsc>
9790
{t('button.back')}
98-
</ButtonLink>
91+
</BackButton>
9992
</SigningConfiguration>
10093
</div>
10194
</div>

frontends/web/src/routes/account/receive/receive-bb01.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,15 @@
1616
*/
1717

1818
import React, { useEffect, useRef, useState } from 'react';
19-
import { useNavigate } from 'react-router-dom';
2019
import { useTranslation } from 'react-i18next';
2120
import { useLoad } from '@/hooks/api';
22-
import { useEsc } from '@/hooks/keyboard';
2321
import * as accountApi from '@/api/account';
2422
import { getScriptName, isEthereumBased } from '@/routes/account/utils';
2523
import { alertUser } from '@/components/alert/Alert';
2624
import { CopyableInput } from '@/components/copy/Copy';
2725
import { Dialog, DialogButtons } from '@/components/dialog/dialog';
28-
import { Button, ButtonLink, Radio } from '@/components/forms';
26+
import { Button, Radio } from '@/components/forms';
27+
import { BackButton } from '@/components/backbutton/backbutton';
2928
import { Message } from '@/components/message/message';
3029
import { ReceiveGuide } from './components/guide';
3130
import { Header } from '@/components/layout';
@@ -63,7 +62,6 @@ export const Receive = ({
6362
code,
6463
deviceID,
6564
}: TProps) => {
66-
const navigate = useNavigate();
6765
const { t } = useTranslation();
6866
const [verifying, setVerifying] = useState<boolean>(false);
6967
const [activeIndex, setActiveIndex] = useState<number>(0);
@@ -80,8 +78,6 @@ export const Receive = ({
8078
const receiveAddresses = useLoad(accountApi.getReceiveAddressList(code));
8179
const secureOutput = useLoad(accountApi.hasSecureOutput(code));
8280

83-
useEsc(() => !verifying && navigate(`/account/${code}`));
84-
8581
const availableScriptTypes = useRef<accountApi.ScriptType[]>();
8682

8783
useEffect(() => {
@@ -241,11 +237,9 @@ export const Receive = ({
241237
disabled={verifying || secureOutput === undefined}
242238
forceVerification={forceVerification}
243239
onClick={() => verifyAddress(currentAddressIndex)}/>
244-
<ButtonLink
245-
secondary
246-
to={`/account/${code}`}>
240+
<BackButton enableEsc={!verifying}>
247241
{t('button.back')}
248-
</ButtonLink>
242+
</BackButton>
249243
</div>
250244
{ forceVerification && verifying && (
251245
<div className={style.hide}></div>

frontends/web/src/routes/account/receive/receive.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* Copyright 2018 Shift Devices AG
3-
* Copyright 2023 Shift Crypto AG
3+
* Copyright 2023-2024 Shift Crypto AG
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -16,15 +16,14 @@
1616
*/
1717

1818
import React, { useEffect, useRef, useState } from 'react';
19-
import { useNavigate } from 'react-router-dom';
2019
import { useTranslation } from 'react-i18next';
2120
import { useLoad } from '@/hooks/api';
22-
import { useEsc } from '@/hooks/keyboard';
2321
import * as accountApi from '@/api/account';
2422
import { getScriptName, isEthereumBased } from '@/routes/account/utils';
2523
import { CopyableInput } from '@/components/copy/Copy';
2624
import { Dialog, DialogButtons } from '@/components/dialog/dialog';
27-
import { Button, ButtonLink, Radio } from '@/components/forms';
25+
import { Button, Radio } from '@/components/forms';
26+
import { BackButton } from '@/components/backbutton/backbutton';
2827
import { Message } from '@/components/message/message';
2928
import { ReceiveGuide } from './components/guide';
3029
import { Header } from '@/components/layout';
@@ -114,7 +113,6 @@ export const Receive = ({
114113
accounts,
115114
code,
116115
}: TProps) => {
117-
const navigate = useNavigate();
118116
const { t } = useTranslation();
119117
const [verifying, setVerifying] = useState<false | 'secure' | 'insecure'>(false);
120118
const [activeIndex, setActiveIndex] = useState<number>(0);
@@ -134,8 +132,6 @@ export const Receive = ({
134132

135133
const hasManyScriptTypes = availableScriptTypes.current && availableScriptTypes.current.length > 1;
136134

137-
useEsc(() => !addressTypeDialog && !verifying && navigate(`/account/${code}`));
138-
139135
useEffect(() => {
140136
if (receiveAddresses) {
141137
// All script types that are present in the addresses delivered by the backend. Will be empty for if there are no such addresses, e.g. in Ethereum.
@@ -280,11 +276,9 @@ export const Receive = ({
280276
primary>
281277
{t('receive.verifyBitBox02')}
282278
</Button>
283-
<ButtonLink
284-
secondary
285-
to={`/account/${code}`}>
279+
<BackButton enableEsc={!addressTypeDialog && !verifying}>
286280
{t('button.back')}
287-
</ButtonLink>
281+
</BackButton>
288282
</div>
289283
{ verifying && (
290284
<div className={style.hide}></div>

frontends/web/src/routes/account/send/send.tsx

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ import { getDeviceInfo } from '@/api/bitbox01';
2525
import { alertUser } from '@/components/alert/Alert';
2626
import { Balance } from '@/components/balance/balance';
2727
import { HideAmountsButton } from '@/components/hideamountsbutton/hideamountsbutton';
28-
import { Button, ButtonLink } from '@/components/forms';
28+
import { Button } from '@/components/forms';
29+
import { BackButton } from '@/components/backbutton/backbutton';
2930
import { Column, ColumnButtons, Grid, GuideWrapper, GuidedContent, Header, Main } from '@/components/layout';
3031
import { Status } from '@/components/status/status';
3132
import { translate, TranslateProps } from '@/decorators/translate';
3233
import { getConfig } from '@/utils/config';
3334
import { FeeTargets } from './feetargets';
34-
import { route } from '@/utils/route';
3535
import { signConfirm, signProgress, TSignProgress } from '@/api/devicessync';
3636
import { UnsubscribeList, unsubscribe } from '@/utils/subscriptions';
3737
import { isBitcoinBased, findAccount } from '@/routes/account/utils';
@@ -166,26 +166,14 @@ class Send extends Component<Props, State> {
166166
}
167167
}),
168168
];
169-
170-
this.registerEvents();
171169
}
172170

173171
public componentWillUnmount() {
174-
this.unregisterEvents();
175172
unsubscribe(this.unsubscribeList);
176173
// Wipe proposed tx note.
177174
accountApi.proposeTxNote(this.getAccount()!.code, '');
178175
}
179176

180-
private registerEvents = () => document.addEventListener('keydown', this.handleKeyDown);
181-
private unregisterEvents = () => document.removeEventListener('keydown', this.handleKeyDown);
182-
183-
private handleKeyDown = (e: KeyboardEvent) => {
184-
if (e.key === 'Escape' && !this.state.activeCoinControl && !this.state.activeScanQR) {
185-
route(`/account/${this.props.code}`);
186-
}
187-
};
188-
189177
private send = async () => {
190178
if (this.state.noMobileChannelError) {
191179
alertUser(this.props.t('warning.sendPairing'));
@@ -330,7 +318,7 @@ class Send extends Component<Props, State> {
330318
this.convertToFiat(result.amount.amount);
331319
}
332320
} else {
333-
const errorHandling = txProposalErrorHandling(this.registerEvents, this.unregisterEvents, result.errorCode);
321+
const errorHandling = txProposalErrorHandling(result.errorCode);
334322
this.setState({ ...errorHandling, isUpdatingProposal: false });
335323
}
336324
};
@@ -484,7 +472,7 @@ class Send extends Component<Props, State> {
484472
};
485473

486474
public render() {
487-
const { t, code } = this.props;
475+
const { t } = this.props;
488476
const {
489477
balance,
490478
proposedFee,
@@ -637,11 +625,11 @@ class Send extends Component<Props, State> {
637625
disabled={!this.getValidTxInputData() || !valid || isUpdatingProposal}>
638626
{t('send.button')}
639627
</Button>
640-
<ButtonLink
641-
secondary
642-
to={`/account/${code}`}>
628+
<BackButton
629+
disabled={activeCoinControl || activeScanQR}
630+
enableEsc>
643631
{t('button.back')}
644-
</ButtonLink>
632+
</BackButton>
645633
</ColumnButtons>
646634
</Column>
647635
</Grid>

frontends/web/src/routes/account/send/services.test.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,38 +25,35 @@ vi.mock('@/components/alert/Alert', () => ({
2525
describe('send services', () => {
2626
describe('txProposalErrorHandling', () => {
2727

28-
const mockRegisterEvents = vi.fn();
29-
const mockUnregisterEvents = vi.fn();
30-
3128
it('returns invalid address message on invalidAddress error', () => {
32-
const result = txProposalErrorHandling(mockRegisterEvents, mockUnregisterEvents, 'invalidAddress');
29+
const result = txProposalErrorHandling('invalidAddress');
3330
expect(result).toEqual({ addressError: 'send.error.invalidAddress' });
3431
});
3532

3633
it('returns invalid amount message on invalidAmount error', () => {
37-
const result = txProposalErrorHandling(mockRegisterEvents, mockUnregisterEvents, 'invalidAmount');
34+
const result = txProposalErrorHandling('invalidAmount');
3835
expect(result).toEqual({ amountError: 'send.error.invalidAmount', proposedFee: undefined });
3936
});
4037

4138
it('returns insufficient funds message on insufficientFunds error', () => {
42-
const result = txProposalErrorHandling(mockRegisterEvents, mockUnregisterEvents, 'insufficientFunds');
39+
const result = txProposalErrorHandling('insufficientFunds');
4340
expect(result).toEqual({ amountError: 'send.error.insufficientFunds', proposedFee: undefined });
4441
});
4542

4643
it('returns fee too low message on feeTooLow error', () => {
47-
const result = txProposalErrorHandling(mockRegisterEvents, mockUnregisterEvents, 'feeTooLow');
44+
const result = txProposalErrorHandling('feeTooLow');
4845
expect(result).toEqual({ feeError: 'send.error.feeTooLow' });
4946
});
5047

5148
it('returns fees not available message on feesNotAvailable error', () => {
52-
const result = txProposalErrorHandling(mockRegisterEvents, mockUnregisterEvents, 'feesNotAvailable');
49+
const result = txProposalErrorHandling('feesNotAvailable');
5350
expect(result).toEqual({ feeError: 'send.error.feesNotAvailable' });
5451
});
5552

5653
it('returns proposed fee undefined and alerts the user when error is unknown', () => {
57-
const result = txProposalErrorHandling(mockRegisterEvents, mockUnregisterEvents, 'unknownError');
54+
const result = txProposalErrorHandling('unknownError');
5855
expect(result).toEqual({ proposedFee: undefined });
59-
expect(alertUser).toHaveBeenCalledWith('unknownError', { callback: mockRegisterEvents });
56+
expect(alertUser).toHaveBeenCalledWith('unknownError');
6057
});
6158

6259
});

frontends/web/src/routes/account/send/services.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export type TProposalError = {
2424
feeError: string;
2525
}
2626

27-
export const txProposalErrorHandling = (registerEvents: () => void, unregisterEvents: () => void, errorCode?: string) => {
27+
export const txProposalErrorHandling = (errorCode?: string) => {
2828
const { t } = i18n;
2929
switch (errorCode) {
3030
case 'invalidAddress':
@@ -37,8 +37,7 @@ export const txProposalErrorHandling = (registerEvents: () => void, unregisterEv
3737
return { feeError: t(`send.error.${errorCode}`) };
3838
default:
3939
if (errorCode) {
40-
unregisterEvents();
41-
alertUser(errorCode, { callback: registerEvents });
40+
alertUser(errorCode);
4241
}
4342
return { proposedFee: undefined };
4443
}

0 commit comments

Comments
 (0)