Skip to content

feat: improve Keystone USB error handling for homepage requirement#750

Open
Qkin-Keystone wants to merge 3 commits intoava-labs:mainfrom
KeystoneHQ:feat/keystone-usb-error-handling
Open

feat: improve Keystone USB error handling for homepage requirement#750
Qkin-Keystone wants to merge 3 commits intoava-labs:mainfrom
KeystoneHQ:feat/keystone-usb-error-handling

Conversation

@Qkin-Keystone
Copy link
Copy Markdown
Contributor

Description

Improves Keystone USB error handling when device is not on homepage. Adds clear user guidance and stops ineffective auto-retry behavior.

Changes

  • Added specific error detection for "not on homepage" state
  • Enhanced UI to display actionable instructions when error occurs
  • Disabled auto-retry when device needs manual navigation to homepage
  • Propagated error state through approval flow for better UX

Testing

  1. Connect Keystone 3 Pro via USB, navigate away from homepage
  2. Attempt transaction signing
  3. Verify clear error message and instructions appear
  4. Return device to homepage and verify transaction succeeds

Screenshots:

N/A - Error handling improvements, no visual design changes

Checklist for the author

Tick each of them when done or if not applicable.

  • I've covered new/modified business logic with Jest test cases.
  • I've tested the changes myself before sending it to code review and QA.

Copilot AI review requested due to automatic review settings February 4, 2026 05:10
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves error handling for Keystone USB hardware wallets when the device is not on its homepage. The changes prevent ineffective auto-retry behavior and provide clear instructions to users.

Changes:

  • Added detection and propagation of "not on homepage" error state through the approval flow
  • Enhanced UI to display actionable error messages when Keystone device requires manual navigation
  • Disabled automatic retry mechanism when device needs to be manually returned to homepage

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/service-worker/src/services/keystone/constants/error.ts Defines the constant for the Keystone homepage error message
packages/service-worker/src/vmModules/ApprovalController.ts Adds logic to detect and propagate homepage error instead of treating it as a user rejection
packages/service-worker/src/services/actions/handlers/updateAction.ts Wraps async response in try-catch to properly return errors
packages/ui/src/hooks/useApproveAction.ts Captures homepage error and sets error state instead of closing window
apps/next/src/pages/Approve/components/hardware/keystone-usb/types.ts Adds error property to state component props
apps/next/src/pages/Approve/components/hardware/keystone-usb/KeystoneUSBApprovalOverlay.tsx Changes state to 'disconnected' when homepage error occurs and passes error to components
apps/next/src/pages/Approve/components/hardware/keystone-usb/components/Disconnected.tsx Displays homepage-specific error message and disables reconnect button and auto-retry
apps/next/src/pages/Approve/components/hardware/HardwareApprovalOverlay.tsx Passes error prop through component hierarchy
apps/next/src/pages/Approve/TransactionApprovalScreen.tsx Adds note for Keystone users and passes error to overlay component

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/service-worker/src/services/keystone/constants/error.ts
resolve({
error: providerErrors.userRejectedRequest(),
});
this.#requests.delete(action.actionId);
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The code deletes the request in both the user rejection case (line 233) and the default error case (line 243). This duplication could be simplified by moving the delete operation outside the conditional blocks to a single location that handles all non-homepage error cases.

Copilot uses AI. Check for mistakes.
},
}),
});
this.#requests.delete(action.actionId);
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The code deletes the request in both the user rejection case (line 233) and the default error case (line 243). This duplication could be simplified by moving the delete operation outside the conditional blocks to a single location that handles all non-homepage error cases.

Copilot uses AI. Check for mistakes.
> = ({ action, reject, approve }) => {
const state = useKeystoneUsbApprovalState();
> = ({ action, reject, approve, error }) => {
let state = useKeystoneUsbApprovalState();
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

Using let to reassign a hook's return value is not a typical React pattern. Consider using a derived state variable or a useMemo hook instead. For example: const derivedState = useMemo(() => state === 'pending' && error === KEYSTONE_NOT_IN_HOMEPAGE_ERROR ? 'disconnected' : state, [state, error]);

Copilot uses AI. Check for mistakes.
}}
>
<strong>Note: </strong>
{'Ensure your Keystone 3 Pro is on the homepage'}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The message text is hardcoded in a JSX expression using curly braces. For consistency with other translatable strings in the component and to support internationalization, this should use the t() function: t('Ensure your Keystone 3 Pro is on the homepage')

Suggested change
{'Ensure your Keystone 3 Pro is on the homepage'}
{t('Ensure your Keystone 3 Pro is on the homepage')}

Copilot uses AI. Check for mistakes.
Comment on lines +104 to +112
if (errorMessage.includes(KEYSTONE_NOT_IN_HOMEPAGE_ERROR)) {
setError(KEYSTONE_NOT_IN_HOMEPAGE_ERROR);
return false;
} else {
if (shouldCloseAfterUpdate) {
globalThis.close();
}
return false;
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

Both branches of the if-else statement return false, making the conditional close behavior the only difference. This can be simplified by handling the window close logic first, then returning false unconditionally: check the error type, set error state if needed, close window if not homepage error and shouldCloseAfterUpdate is true, then return false.

Suggested change
if (errorMessage.includes(KEYSTONE_NOT_IN_HOMEPAGE_ERROR)) {
setError(KEYSTONE_NOT_IN_HOMEPAGE_ERROR);
return false;
} else {
if (shouldCloseAfterUpdate) {
globalThis.close();
}
return false;
}
const isKeystoneNotInHomepageError = errorMessage.includes(
KEYSTONE_NOT_IN_HOMEPAGE_ERROR,
);
if (!isKeystoneNotInHomepageError && shouldCloseAfterUpdate) {
globalThis.close();
}
if (isKeystoneNotInHomepageError) {
setError(KEYSTONE_NOT_IN_HOMEPAGE_ERROR);
}
return false;

Copilot uses AI. Check for mistakes.
@Qkin-Keystone Qkin-Keystone force-pushed the feat/keystone-usb-error-handling branch from c0051b7 to 38660dd Compare February 4, 2026 05:12
Copy link
Copy Markdown
Collaborator

@meeh0w meeh0w left a comment

Choose a reason for hiding this comment

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

This PR kind of goes against the grain if you look at other wallets' implementations:

  1. With other devices, the Approve on <<device>> message is only shown in the UI when the device is in the correct state. This here allows the user to sign, only to then show an error message. Don't get me wrong -- it's better than just erroring out without any context, but the end result is very similar and still annoying -- the user has to manually click reject and retry.
  2. The PR also relies on ApprovalController.requestApproval() throwing an error, which it should never do. We rely on this method to always resolve (albeit with { error } property) in other places.
  3. It also creates a very Keystone-specific if-clauses in places where I would not expect them. If we want to add this, we should create a util that is more generic (i.e. isErrorRetryable, or something along those lines, and allow users to retry signing without forcing them out of the approval flow).
  4. This PR creates a constant KEYSTONE_NOT_IN_HOMEPAGE_ERROR in our repository that is meant to represent the error message returned by an external SDK/device. I'd say this is not the way to go -- your SDK should either export a specific error class or error code that we can test the error against. Otherwise this is very fragile (if you change the error message on the device, everything fails here).

I'd say let's leave it as it is for now, or add a way to detect the device being on the wrong screen before sending the signing request to the device.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants