Skip to content

Commit 98b7830

Browse files
committed
frontend: fix nouncheckedindexedaccess errors
Introduced a TS helper function NonEmptyArray to type that the array is not empty and contains at least one item of given type.
1 parent 7205fa3 commit 98b7830

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+238
-123
lines changed

frontends/web/src/api/account.ts

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

1717
import type { LineData } from 'lightweight-charts';
18-
import { apiGet, apiPost } from '@/utils/request';
18+
import type { Slip24 } from 'request-address';
1919
import type { TDetailStatus } from './bitsurance';
2020
import type { SuccessResponse } from './response';
21-
import { Slip24 } from 'request-address';
21+
import type { NonEmptyArray } from '@/utils/types';
22+
import { apiGet, apiPost } from '@/utils/request';
2223

2324
export type NativeCoinCode = 'btc' | 'tbtc' | 'rbtc' | 'ltc' | 'tltc' | 'eth' | 'sepeth';
2425

@@ -304,11 +305,11 @@ export interface IReceiveAddress {
304305

305306
export interface ReceiveAddressList {
306307
scriptType: ScriptType | null;
307-
addresses: IReceiveAddress[];
308+
addresses: NonEmptyArray<IReceiveAddress>;
308309
}
309310

310311
export const getReceiveAddressList = (code: AccountCode) => {
311-
return (): Promise<ReceiveAddressList[] | null> => {
312+
return (): Promise<NonEmptyArray<ReceiveAddressList> | null> => {
312313
return apiGet(`account/${code}/receive-addresses`);
313314
};
314315
};

frontends/web/src/app.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,13 @@ export const App = () => {
155155
// We don't bother implementing the same for the bitbox01.
156156
// The bb02 bootloader screen is not full screen, so we don't mount it globally and instead
157157
// route to it.
158-
const productName = devices[newDeviceIDList[0]];
159-
if (productName === 'bitbox' || productName === 'bitbox02-bootloader') {
160-
navigate(`settings/device-settings/${newDeviceIDList[0]}`);
161-
return;
158+
const firstNewDevice = newDeviceIDList[0];
159+
if (firstNewDevice) {
160+
const productName = devices[firstNewDevice];
161+
if (productName === 'bitbox' || productName === 'bitbox02-bootloader') {
162+
navigate(`settings/device-settings/${newDeviceIDList[0]}`);
163+
return;
164+
}
162165
}
163166
}
164167
maybeRoute();
@@ -173,7 +176,8 @@ export const App = () => {
173176
const deviceIDs: string[] = Object.keys(devices);
174177
const activeAccounts = accounts.filter(acct => acct.active);
175178

176-
const isBitboxBootloader = devices[deviceIDs[0]] === 'bitbox02-bootloader';
179+
const firstDevice = deviceIDs[0];
180+
const isBitboxBootloader = firstDevice && devices[firstDevice] === 'bitbox02-bootloader';
177181
const showBottomNavigation = (deviceIDs.length > 0 || activeAccounts.length > 0) && !isBitboxBootloader;
178182

179183

frontends/web/src/components/badge/badge.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const Badge = ({
3838
<span
3939
className={`${style.badge} ${style[type]} ${withChildrenStyle} ${iconOnlyStyle} ${className || ''}`}
4040
{...props}>
41-
{icon && icon({ className: style.badgeIcon })}
41+
{icon && style.badgeIcon && icon({ className: style.badgeIcon })}
4242
{children}
4343
</span>
4444
);

frontends/web/src/components/banners/banner.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type TBannerProps = {
2828

2929
export const Banner = ({ msgKey }: TBannerProps) => {
3030
const { i18n, t } = useTranslation();
31+
const { fallbackLng } = i18n.options;
3132
const [banner, setBanner] = useState<TBannerInfo>();
3233

3334
useEffect(() => {
@@ -37,18 +38,24 @@ export const Banner = ({ msgKey }: TBannerProps) => {
3738

3839
if (
3940
!banner
40-
|| !i18n.options.fallbackLng
41+
|| !fallbackLng
4142
|| !i18n.resolvedLanguage
4243
) {
4344
return null;
4445
}
4546
const { message, link } = banner;
4647

48+
const maybeFallbackLng: string = (
49+
Array.isArray(fallbackLng) && fallbackLng.length > 0
50+
? fallbackLng[0]
51+
: fallbackLng
52+
);
53+
4754
return (
4855
<Status
4956
dismissible={banner.dismissible ? `banner-${msgKey}-${banner.id}` : ''}
5057
type={banner.type ? banner.type : 'warning'}>
51-
{message[i18n.resolvedLanguage] || message[(i18n.options.fallbackLng as string[])[0]]}
58+
{message[i18n.resolvedLanguage] || message[maybeFallbackLng || 'en']}
5259
&nbsp;
5360
{link && (
5461
<A href={link.href} className={style.link}>

frontends/web/src/components/bottom-navigation/bottom-navigation.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const BottomNavigation = ({ activeAccounts, devices }: Props) => {
3333
const { t } = useTranslation();
3434
const { pathname } = useLocation();
3535
const deviceID = Object.keys(devices)[0];
36-
const isBitBox02 = devices[deviceID] === 'bitbox02';
36+
const isBitBox02 = deviceID && devices[deviceID] === 'bitbox02';
3737
const versionInfo = useLoad(isBitBox02 ? () => getVersion(deviceID) : null, [deviceID]);
3838
const canUpgrade = versionInfo ? versionInfo.canUpgrade : false;
3939

@@ -61,7 +61,10 @@ export const BottomNavigation = ({ activeAccounts, devices }: Props) => {
6161
{onlyHasOneAccount ? t('account.account') : t('account.accounts')}
6262
</Link>
6363
<Link
64-
className={`${styles.link} ${pathname.startsWith('/market/') ? styles.active : ''}`}
64+
className={`
65+
${styles.link as string}
66+
${pathname.startsWith('/market/') && styles.active || ''}
67+
`}
6568
to="/market/info"
6669
>
6770
<MarketIconSVG />

frontends/web/src/components/dialog/dialog-legacy.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,15 @@ class DialogLegacy extends Component<Props, State> {
7676

7777
private focusFirst = () => {
7878
const focusables = this.focusableChildren;
79-
if (focusables.length && focusables[0].getAttribute('autofocus') !== 'false') {
79+
if (focusables.length && focusables[0] && focusables[0].getAttribute('autofocus') !== 'false') {
8080
focusables[0].focus();
8181
}
8282
};
8383

8484
private updateIndex = (isNext: boolean) => {
8585
const target = this.getNextIndex(isNext);
8686
this.setState({ currentTab: target }, () => {
87-
this.focusableChildren[target].focus();
87+
this.focusableChildren[target]?.focus();
8888
});
8989
};
9090

@@ -119,8 +119,12 @@ class DialogLegacy extends Component<Props, State> {
119119
if (!this.modal.current || !this.overlay.current) {
120120
return;
121121
}
122-
this.modal.current.classList.remove(style.activeModal);
123-
this.overlay.current.classList.remove(style.activeOverlay);
122+
if (style.activeModal) {
123+
this.modal.current.classList.remove(style.activeModal);
124+
}
125+
if (style.activeOverlay) {
126+
this.overlay.current.classList.remove(style.activeOverlay);
127+
}
124128
this.setState({ active: false, currentTab: 0 }, () => {
125129
document.removeEventListener('keydown', this.handleKeyDown);
126130
if (this.props.onClose) {
@@ -134,8 +138,12 @@ class DialogLegacy extends Component<Props, State> {
134138
if (!this.modal.current || !this.overlay.current) {
135139
return;
136140
}
137-
this.overlay.current.classList.add(style.activeOverlay);
138-
this.modal.current.classList.add(style.activeModal);
141+
if (style.activeOverlay) {
142+
this.overlay.current.classList.add(style.activeOverlay);
143+
}
144+
if (style.activeModal) {
145+
this.modal.current.classList.add(style.activeModal);
146+
}
139147
this.focusWithin();
140148
this.focusFirst();
141149
});

frontends/web/src/components/dialog/dialog.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,17 +99,17 @@ export const Dialog = ({
9999
}
100100

101101
const modalClass = `
102-
${style.modal}
103-
${small ? style.small : ''}
104-
${medium ? style.medium : ''}
105-
${large ? style.large : ''}
106-
${status === 'open' || status === 'opening' ? style.open : ''}
102+
${style.modal as string}
103+
${small && style.small || ''}
104+
${medium && style.medium || ''}
105+
${large && style.large || ''}
106+
${(status === 'open' || status === 'opening') && style.open || ''}
107107
`.trim();
108108

109109
const overlayClass = `
110-
${style.overlay}
111-
${status === 'opening' || status === 'open' ? style.activeOverlay : ''}
112-
${status === 'closing' ? style.closingOverlay : ''}
110+
${style.overlay as string}
111+
${(status === 'opening' || status === 'open') && style.activeOverlay || ''}
112+
${status === 'closing' && style.closingOverlay || ''}
113113
`.trim();
114114

115115
const headerClass = `

frontends/web/src/components/forms/input-number.test.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { describe, expect, it, vi } from 'vitest';
1818
import { fireEvent, render, screen } from '@testing-library/react';
1919
import { NumberInput } from './input-number';
2020

21+
2122
describe('components/forms/input-number', () => {
2223
it('should preserve type attribute', () => {
2324
const { container } = render(<NumberInput defaultValue="" />);
@@ -60,102 +61,127 @@ describe('components/forms/input-number', () => {
6061

6162
fireEvent.paste(input, { clipboardData: { getData: () => '100.50' } });
6263
expect(mockCallback).toHaveBeenCalledTimes(2);
64+
// @ts-ignore noUncheckedIndexedAccess
6365
expect(mockCallback.mock.calls[1][0].target.value).toBe('100.50');
6466

6567
mockCallback.mockClear();
6668
fireEvent.paste(input, { clipboardData: { getData: () => '1' } });
69+
// @ts-ignore noUncheckedIndexedAccess
6770
expect(mockCallback.mock.calls[0][0].target.value).toBe('1');
6871

6972
mockCallback.mockClear();
7073
fireEvent.paste(input, { clipboardData: { getData: () => '1,0' } });
74+
// @ts-ignore noUncheckedIndexedAccess
7175
expect(mockCallback.mock.calls[0][0].target.value).toBe('1.0');
7276

7377
mockCallback.mockClear();
7478
fireEvent.paste(input, { clipboardData: { getData: () => '1,0' } });
79+
// @ts-ignore noUncheckedIndexedAccess
7580
expect(mockCallback.mock.calls[0][0].target.value).toBe('1.0');
7681

7782
mockCallback.mockClear();
7883
fireEvent.paste(input, { clipboardData: { getData: () => '1,00' } });
84+
// @ts-ignore noUncheckedIndexedAccess
7985
expect(mockCallback.mock.calls[0][0].target.value).toBe('1.00');
8086

8187
mockCallback.mockClear();
8288
fireEvent.paste(input, { clipboardData: { getData: () => '1.00' } });
89+
// @ts-ignore noUncheckedIndexedAccess
8390
expect(mockCallback.mock.calls[0][0].target.value).toBe('1.00');
8491

8592
mockCallback.mockClear();
8693
fireEvent.paste(input, { clipboardData: { getData: () => '1,000' } });
94+
// @ts-ignore noUncheckedIndexedAccess
8795
expect(mockCallback.mock.calls[0][0].target.value).toBe('1.000');
8896

8997
mockCallback.mockClear();
9098
fireEvent.paste(input, { clipboardData: { getData: () => '1,000.00' } });
99+
// @ts-ignore noUncheckedIndexedAccess
91100
expect(mockCallback.mock.calls[0][0].target.value).toBe('1000.00');
92101

93102
mockCallback.mockClear();
94103
fireEvent.paste(input, { clipboardData: { getData: () => '1.000,00' } });
104+
// @ts-ignore noUncheckedIndexedAccess
95105
expect(mockCallback.mock.calls[0][0].target.value).toBe('1000.00');
96106

97107
mockCallback.mockClear();
98108
fireEvent.paste(input, { clipboardData: { getData: () => '100,50' } });
109+
// @ts-ignore noUncheckedIndexedAccess
99110
expect(mockCallback.mock.calls[0][0].target.value).toBe('100.50');
100111

101112
mockCallback.mockClear();
102113
fireEvent.paste(input, { clipboardData: { getData: () => '100.00' } });
114+
// @ts-ignore noUncheckedIndexedAccess
103115
expect(mockCallback.mock.calls[0][0].target.value).toBe('100.00');
104116

105117
mockCallback.mockClear();
106118
fireEvent.paste(input, { clipboardData: { getData: () => '100,000' } });
119+
// @ts-ignore noUncheckedIndexedAccess
107120
expect(mockCallback.mock.calls[0][0].target.value).toBe('100.000');
108121

109122
mockCallback.mockClear();
110123
fireEvent.paste(input, { clipboardData: { getData: () => '100.000' } });
124+
// @ts-ignore noUncheckedIndexedAccess
111125
expect(mockCallback.mock.calls[0][0].target.value).toBe('100.000');
112126

113127
mockCallback.mockClear();
114128
fireEvent.paste(input, { clipboardData: { getData: () => '.99' } });
129+
// @ts-ignore noUncheckedIndexedAccess
115130
expect(mockCallback.mock.calls[0][0].target.value).toBe('.99');
116131

117132
mockCallback.mockClear();
118133
fireEvent.paste(input, { clipboardData: { getData: () => ',99' } });
134+
// @ts-ignore noUncheckedIndexedAccess
119135
expect(mockCallback.mock.calls[0][0].target.value).toBe('.99');
120136

121137
mockCallback.mockClear();
122138
fireEvent.paste(input, { clipboardData: { getData: () => '0.0000000000000000001' } });
139+
// @ts-ignore noUncheckedIndexedAccess
123140
expect(mockCallback.mock.calls[0][0].target.value).toBe('0.0000000000000000001');
124141

125142
mockCallback.mockClear();
126143
fireEvent.paste(input, { clipboardData: { getData: () => '0,0000000000000000001' } });
144+
// @ts-ignore noUncheckedIndexedAccess
127145
expect(mockCallback.mock.calls[0][0].target.value).toBe('0.0000000000000000001');
128146

129147
mockCallback.mockClear();
130148
fireEvent.paste(input, { clipboardData: { getData: () => '1\'000\'000.50' } });
149+
// @ts-ignore noUncheckedIndexedAccess
131150
expect(mockCallback.mock.calls[0][0].target.value).toBe('1000000.50');
132151

133152
mockCallback.mockClear();
134153
fireEvent.paste(input, { clipboardData: { getData: () => '1\'000\'000,50' } });
154+
// @ts-ignore noUncheckedIndexedAccess
135155
expect(mockCallback.mock.calls[0][0].target.value).toBe('1000000.50');
136156

137157
mockCallback.mockClear();
138158
fireEvent.paste(input, { clipboardData: { getData: () => '1 000 000.50' } });
159+
// @ts-ignore noUncheckedIndexedAccess
139160
expect(mockCallback.mock.calls[0][0].target.value).toBe('1000000.50');
140161

141162
mockCallback.mockClear();
142163
fireEvent.paste(input, { clipboardData: { getData: () => '1.000.000,50' } });
164+
// @ts-ignore noUncheckedIndexedAccess
143165
expect(mockCallback.mock.calls[0][0].target.value).toBe('1000000.50');
144166

145167
mockCallback.mockClear();
146168
fireEvent.paste(input, { clipboardData: { getData: () => '1 000.50' } });
169+
// @ts-ignore noUncheckedIndexedAccess
147170
expect(mockCallback.mock.calls[0][0].target.value).toBe('1000.50');
148171

149172
mockCallback.mockClear();
150173
fireEvent.paste(input, { clipboardData: { getData: () => '1.000,50' } });
174+
// @ts-ignore noUncheckedIndexedAccess
151175
expect(mockCallback.mock.calls[0][0].target.value).toBe('1000.50');
152176

153177
mockCallback.mockClear();
154178
fireEvent.paste(input, { clipboardData: { getData: () => '1,000.50' } });
179+
// @ts-ignore noUncheckedIndexedAccess
155180
expect(mockCallback.mock.calls[0][0].target.value).toBe('1000.50');
156181

157182
mockCallback.mockClear();
158183
fireEvent.paste(input, { clipboardData: { getData: () => '1,000,000.50' } });
184+
// @ts-ignore noUncheckedIndexedAccess
159185
expect(mockCallback.mock.calls[0][0].target.value).toBe('1000000.50');
160186
});
161187

@@ -171,6 +197,7 @@ describe('components/forms/input-number', () => {
171197

172198
fireEvent.paste(input, { clipboardData: { getData: () => '100,' } });
173199
expect(mockCallback).toHaveBeenCalledTimes(1);
200+
// @ts-ignore noUncheckedIndexedAccess
174201
expect(mockCallback.mock.calls[0][0].target.value).toBe('');
175202

176203
mockCallback.mockClear();
@@ -183,6 +210,7 @@ describe('components/forms/input-number', () => {
183210

184211
mockCallback.mockClear();
185212
fireEvent.paste(input, { clipboardData: { getData: () => '1,000' } });
213+
// @ts-ignore noUncheckedIndexedAccess
186214
expect(mockCallback.mock.calls[0][0].target.value).toBe('1.000');
187215

188216
});

frontends/web/src/components/forms/input-number.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export const NumberInput = (({
6262
&& dividedByDot[dividedByDot.length - 1]?.includes(',')
6363
) {
6464
target.value = [
65-
dividedByComma[0].replace(/[.]/g, ''), // replace dot in whole coins 1.000.000
65+
dividedByComma[0]?.replace(/[.]/g, ''), // replace dot in whole coins 1.000.000
6666
dividedByComma[1], // rest i.e. 50
6767
].join('.');
6868
} else if (
@@ -72,7 +72,7 @@ export const NumberInput = (({
7272
&& dividedByComma[dividedByComma.length - 1]?.includes('.')
7373
) {
7474
target.value = [
75-
dividedByDot[0].replace(/[,]/g, ''), // replace comma in 1,000,000
75+
dividedByDot[0]?.replace(/[,]/g, ''), // replace comma in 1,000,000
7676
dividedByDot[1], // rest i.e. 50
7777
].join('.');
7878
} else {

frontends/web/src/components/groupedaccountselector/groupedaccountselector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export const GroupedAccountSelector = ({ title, disabled, selected, onChange, on
161161
SingleValue: SelectSingleValue,
162162
IndicatorSeparator: () => null
163163
}}
164-
defaultValue={options[0].options[0]}
164+
defaultValue={options[0]?.options[0]}
165165
/>
166166
<div className="buttons text-center">
167167
<Button

0 commit comments

Comments
 (0)