Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
53516ac
feat: add useHardwareWalletConnection Hook
montelaidev Jan 20, 2026
7a27345
fix: test
montelaidev Jan 20, 2026
dc9d947
fix: device id reference
montelaidev Jan 20, 2026
61b5f88
fix: remove test because of missing adapters
montelaidev Jan 20, 2026
bf59bef
fix: refactor use mock adapter
montelaidev Jan 20, 2026
e148ca9
fix: test
montelaidev Jan 20, 2026
6c1ecf4
chore: fix message
montelaidev Jan 21, 2026
46e78d2
fix: refactor remove reason from error state, and user hardware error
montelaidev Jan 22, 2026
0df5923
fix: remove perido
montelaidev Jan 22, 2026
39e1177
fix: use abort controller
montelaidev Jan 22, 2026
1932d24
fix: update to use abort controller
montelaidev Jan 22, 2026
f37da82
fix: test
montelaidev Jan 22, 2026
ac0b07e
refactor: make mock simple
montelaidev Jan 22, 2026
5404731
fix: lint
montelaidev Jan 22, 2026
ecce850
fix: race conditons
montelaidev Jan 22, 2026
544cd8a
fix: use connectingPromiseRef for race-safe connection mutex
montelaidev Jan 23, 2026
2226321
fix: remove error patterns
montelaidev Jan 23, 2026
2794ea0
fix: remove incorrect typeguard
montelaidev Jan 23, 2026
5d7a203
Merge remote-tracking branch 'origin/main' into feat/hardware-wallet-…
montelaidev Jan 23, 2026
ec14495
fix: lint
montelaidev Jan 23, 2026
c2cdaab
fix: update errors
montelaidev Jan 23, 2026
4c0fb81
fix: tests
montelaidev Jan 23, 2026
bc42ebd
fix: remove try catch
montelaidev Jan 23, 2026
05c1a14
fix: ref
montelaidev Jan 23, 2026
6edb80b
fix: ref usage and test
montelaidev Jan 23, 2026
aa269d2
fix: lint
montelaidev Jan 23, 2026
b6721eb
fix: add additiaonl check
montelaidev Jan 23, 2026
cd4025d
Merge branch 'main' into feat/hardware-wallet-state-manager-5
montelaidev Jan 26, 2026
8898972
Merge remote-tracking branch 'origin/main' into feat/hardware-wallet-…
montelaidev Jan 27, 2026
9984c33
feat: hardware wallet state manager
montelaidev Jan 27, 2026
be7027b
fix: missing setters and duplicate previousWalletTypeRef
montelaidev Jan 27, 2026
2a58b8b
fix: rebase on state context
montelaidev Jan 27, 2026
6e029c8
fix: address comments
montelaidev Jan 27, 2026
b901b8a
fix: lint
montelaidev Jan 27, 2026
1b6dca3
fix: update console baseline
montelaidev Jan 27, 2026
9e83464
fix: lint
montelaidev Jan 27, 2026
dfbac92
fix: lint
montelaidev Jan 27, 2026
f8ca382
fix: add counter
montelaidev Jan 27, 2026
4b18a97
fix: null reference if there is another connect while connecting
montelaidev Jan 27, 2026
618553f
Merge remote-tracking branch 'origin/feat/hardware-wallet-state-manag…
montelaidev Jan 27, 2026
eab7d11
fix:race condition
montelaidev Jan 27, 2026
a0359bc
fix: update mock
montelaidev Jan 28, 2026
2751f53
fix: remove ref from dep array
montelaidev Jan 28, 2026
77fa31a
fix: add more guards, and reuse promise
montelaidev Jan 28, 2026
86a97da
fix: lint
montelaidev Jan 28, 2026
fbe4615
Merge remote-tracking branch 'origin/feat/hardware-wallet-state-manag…
montelaidev Jan 28, 2026
2a16d7a
Merge remote-tracking branch 'origin/main' into feat/hardware-wallet-…
montelaidev Jan 29, 2026
1295fcd
Merge remote-tracking branch 'origin/feat/hardware-wallet-state-manag…
montelaidev Jan 29, 2026
7acf3e0
fix: lint
montelaidev Jan 29, 2026
a8b09c8
fix: refs and add reset
montelaidev Jan 29, 2026
6f1bc76
Merge remote-tracking branch 'origin/feat/hardware-wallet-state-manag…
montelaidev Jan 29, 2026
653228a
fix: tests
montelaidev Jan 30, 2026
c7571ef
Merge remote-tracking branch 'origin/main' into feat/hardware-wallet-…
montelaidev Jan 30, 2026
f4bc32d
fix: refactor to have reset function and add rpc utils
montelaidev Jan 30, 2026
eb9889d
fix:add constant name
montelaidev Jan 30, 2026
421c9d5
fix: lint
montelaidev Jan 30, 2026
c675486
fix: snapshot
montelaidev Jan 30, 2026
4604558
fix: relax struct
montelaidev Jan 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ describe('HardwareWalletContext', () => {
wrapper: createWrapper(store),
});

expect(result.current.deviceId).toBe(null);
expect(result.current.deviceId).toBeNull();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think toBeDefined would sounds better giving the test description

});
});

Expand Down
267 changes: 267 additions & 0 deletions ui/contexts/hardware-wallets/HardwareWalletErrorProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import React from 'react';
import { render, act } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { KeyringTypes } from '@metamask/keyring-controller';
import { ErrorCode } from '@metamask/hw-wallet-sdk';
import {
showModal,
hideModal,
setPendingHardwareWalletSigning,
closeCurrentNotificationWindow,
} from '../../store/actions';
import { createHardwareWalletError } from './errors';
import {
HardwareWalletErrorProvider,
useHardwareWalletError,
} from './HardwareWalletErrorProvider';
import { HardwareWalletType } from './types';
import { HARDWARE_WALLET_ERROR_MODAL_NAME } from './constants';

const mockStore = configureStore([]);

jest.mock('../../store/actions');
jest.mock('./HardwareWalletContext', () => ({
HardwareWalletProvider: ({ children }: { children: React.ReactNode }) =>
children,
useHardwareWalletConfig: () => ({ isHardwareWalletAccount: true }),
useHardwareWalletState: () => ({
connectionState: { status: 'ready' },
}),
useHardwareWalletActions: () => ({
ensureDeviceReady: jest.fn().mockResolvedValue(true),
clearError: jest.fn(),
}),
}));

const mockShowModal = showModal as jest.Mock;
const mockHideModal = hideModal as jest.Mock;
const mocksetPendingHardwareWalletSigning =
setPendingHardwareWalletSigning as jest.Mock;
const mockCloseCurrentNotificationWindow =
closeCurrentNotificationWindow as jest.Mock;

// Mock showModal to return a proper action object
mockShowModal.mockImplementation((payload) => ({
type: 'MODAL_OPEN',
payload,
}));

// Mock hideModal to return a proper action object
mockHideModal.mockImplementation(() => ({
type: 'MODAL_CLOSE',
}));

mocksetPendingHardwareWalletSigning.mockImplementation((payload) => ({
type: 'SET_PENDING_HARDWARE_SIGNING',
payload,
}));

mockCloseCurrentNotificationWindow.mockImplementation(() => ({
type: 'CLOSE_NOTIFICATION_WINDOW',
}));

const createMockState = (
keyringType: string | null = KeyringTypes.ledger,
address = '0x123',
) => ({
metamask: {
internalAccounts: {
accounts: {
'account-1': {
id: 'account-1',
address,
metadata: {
keyring: {
type: keyringType,
},
},
},
},
selectedAccount: 'account-1',
},
},
});

const createWrapper =
(store: ReturnType<typeof mockStore>) =>
({ children }: { children: React.ReactNode }) => (
<Provider store={store}>
<HardwareWalletErrorProvider>{children}</HardwareWalletErrorProvider>
</Provider>
);

const renderHardwareWalletErrorHook = (store: ReturnType<typeof mockStore>) =>
renderHook(() => useHardwareWalletError(), {
wrapper: createWrapper(store),
});

describe('HardwareWalletErrorProvider', () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe('useHardwareWalletError', () => {
it('throws error when used outside provider', () => {
const HookConsumer = () => {
useHardwareWalletError();
return null;
};

const consoleError = jest
.spyOn(console, 'error')
.mockImplementation(() => undefined);

try {
expect(() => render(<HookConsumer />)).toThrow(
'useHardwareWalletError must be used within HardwareWalletErrorProvider',
);
} finally {
consoleError.mockRestore();
}
});
});

describe('error modal functionality', () => {
it('shows error modal when showErrorModal is called', () => {
const store = mockStore(createMockState());
const { result } = renderHardwareWalletErrorHook(store);

const error = createHardwareWalletError(
ErrorCode.AuthenticationDeviceLocked,
HardwareWalletType.Ledger,
'Device is locked',
);

act(() => {
result.current.showErrorModal(error);
});

expect(mockShowModal).toHaveBeenCalledWith({
name: HARDWARE_WALLET_ERROR_MODAL_NAME,
error,
onRetry: expect.any(Function),
onCancel: expect.any(Function),
isOpen: true,
});
});

it('dismisses error modal when dismissErrorModal is called', () => {
const store = mockStore(createMockState());
const { result } = renderHardwareWalletErrorHook(store);

const error = createHardwareWalletError(
ErrorCode.AuthenticationDeviceLocked,
HardwareWalletType.Ledger,
'Device is locked',
);

// First show a modal
act(() => {
result.current.showErrorModal(error);
});

// Then dismiss it
act(() => {
result.current.dismissErrorModal();
});

expect(mockHideModal).toHaveBeenCalled();
});

it('reports modal visibility state', () => {
const store = mockStore(createMockState());
const { result } = renderHardwareWalletErrorHook(store);

const error = createHardwareWalletError(
ErrorCode.AuthenticationDeviceLocked,
HardwareWalletType.Ledger,
'Device is locked',
);

expect(result.current.isErrorModalVisible).toBe(false);

act(() => {
result.current.showErrorModal(error);
});

expect(result.current.isErrorModalVisible).toBe(true);

act(() => {
result.current.dismissErrorModal();
});

expect(result.current.isErrorModalVisible).toBe(false);
});

it('calls onRetry callback when retry is triggered', async () => {
const store = mockStore(createMockState());
const { result } = renderHardwareWalletErrorHook(store);

const error = createHardwareWalletError(
ErrorCode.AuthenticationDeviceLocked,
HardwareWalletType.Ledger,
'Device is locked',
);

act(() => {
result.current.showErrorModal(error);
});

const { onRetry } = (showModal as jest.Mock).mock.calls[0][0];

await act(async () => {
await onRetry();
});

expect(hideModal).toHaveBeenCalled();
});

it('calls onCancel callback when cancel is triggered', () => {
const store = mockStore(createMockState());
const { result } = renderHardwareWalletErrorHook(store);

const error = createHardwareWalletError(
ErrorCode.AuthenticationDeviceLocked,
HardwareWalletType.Ledger,
'Device is locked',
);

act(() => {
result.current.showErrorModal(error);
});

const { onCancel } = (showModal as jest.Mock).mock.calls[0][0];

act(() => {
onCancel();
});

expect(hideModal).toHaveBeenCalled();
});

it('shows modal for user cancellation errors when called manually', () => {
const store = mockStore(createMockState());
const { result } = renderHardwareWalletErrorHook(store);

const userCancelError = createHardwareWalletError(
ErrorCode.UserCancelled,
HardwareWalletType.Ledger,
'User cancelled',
);

act(() => {
result.current.showErrorModal(userCancelError);
});

expect(mockShowModal).toHaveBeenCalledWith({
name: HARDWARE_WALLET_ERROR_MODAL_NAME,
error: userCancelError,
onRetry: expect.any(Function),
onCancel: expect.any(Function),
isOpen: true,
});
});
});
});
Loading
Loading