From 2b5bc666d129d2d1c73da4c6da96fe27e84c2a5a Mon Sep 17 00:00:00 2001 From: Jonathan Tzeng Date: Fri, 24 Jul 2026 01:06:31 -0700 Subject: [PATCH] Improve unstake scene error handling Replace the popup error alert and generic "unknown error occurred" on the StakeModify scene with the real error surfaced in the existing on-scene error field. When the failure is insufficient funds during an unstake, show a clear message that the wallet needs a native-asset balance to cover the network fee. --- CHANGELOG.md | 1 + src/__tests__/stakeErrorUtils.test.ts | 38 +++++++++++++++++++ .../scenes/Staking/StakeModifyScene.tsx | 23 ++++++++--- src/locales/en_US.ts | 2 + src/locales/strings/enUS.json | 1 + src/util/stakeErrorUtils.ts | 12 ++++++ 6 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/stakeErrorUtils.test.ts create mode 100644 src/util/stakeErrorUtils.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e81d870504..c8477038e08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased (develop) - changed: Sign MoonPay buy/sell widget URLs and bind them to the customer's IP via the info server, for MoonPay's on-ramp IP-matching security upgrade. +- fixed: Improve the unstake error experience by replacing the popup alert and generic "unknown error occurred" with the real error in the scene's error field, and showing a clear message when the wallet lacks the balance to cover the unstaking network fee. ## 4.50.0 (2026-07-21) diff --git a/src/__tests__/stakeErrorUtils.test.ts b/src/__tests__/stakeErrorUtils.test.ts new file mode 100644 index 00000000000..ff520ae1c61 --- /dev/null +++ b/src/__tests__/stakeErrorUtils.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, test } from '@jest/globals' +import { DustSpendError, InsufficientFundsError } from 'edge-core-js' + +import { lstrings } from '../locales/strings' +import { getDisplayErrorMessage } from '../util/stakeErrorUtils' + +describe('getDisplayErrorMessage', () => { + test('returns the message of an Error that carries one', () => { + expect(getDisplayErrorMessage(new Error('Insufficient funds'))).toBe( + 'Insufficient funds' + ) + }) + + test('fishes out the message from edge-core-js error subclasses', () => { + expect( + getDisplayErrorMessage(new InsufficientFundsError({ tokenId: null })) + ).not.toBe('') + expect(getDisplayErrorMessage(new DustSpendError())).not.toBe('') + }) + + test('falls back to the generic string for an Error with no message', () => { + expect(getDisplayErrorMessage(new Error(''))).toBe( + lstrings.unknown_error_occurred_fragment + ) + }) + + test('falls back to the generic string for non-Error values', () => { + expect(getDisplayErrorMessage('some string')).toBe( + lstrings.unknown_error_occurred_fragment + ) + expect(getDisplayErrorMessage(undefined)).toBe( + lstrings.unknown_error_occurred_fragment + ) + expect(getDisplayErrorMessage(null)).toBe( + lstrings.unknown_error_occurred_fragment + ) + }) +}) diff --git a/src/components/scenes/Staking/StakeModifyScene.tsx b/src/components/scenes/Staking/StakeModifyScene.tsx index 092c7760b95..26f14efcd01 100644 --- a/src/components/scenes/Staking/StakeModifyScene.tsx +++ b/src/components/scenes/Staking/StakeModifyScene.tsx @@ -31,6 +31,7 @@ import { useDispatch, useSelector } from '../../../types/reactRedux' import type { EdgeAppSceneProps } from '../../../types/routerTypes' import { getCurrencyIconUris } from '../../../util/CdnUris' import { getWalletName } from '../../../util/CurrencyWalletHelpers' +import { getDisplayErrorMessage } from '../../../util/stakeErrorUtils' import { enableStakeTokens, getPolicyIconUris, @@ -227,15 +228,26 @@ const StakeModifySceneComponent: React.FC = props => { ) setErrorMessage(errMessage) } else if (err instanceof InsufficientFundsError) { - setErrorMessage(lstrings.exchange_insufficient_funds_title) + // The unstake network fee is paid in the wallet's native asset, so + // tell the user which balance they need instead of a bare + // "Insufficient Funds". + setErrorMessage( + changeQuoteRequest.action === 'unstake' + ? sprintf( + lstrings.stake_error_insufficient_funds_unstake_s, + wallet.currencyInfo.currencyCode + ) + : lstrings.exchange_insufficient_funds_title + ) } else if ( err instanceof HumanFriendlyError || err instanceof DustSpendError ) { setErrorMessage(err.message) } else { - showError(err) - setErrorMessage(lstrings.unknown_error_occurred_fragment) + // Show the real error in the on-scene error field rather than a + // scary popup alert plus a generic "unknown error occurred". + setErrorMessage(getDisplayErrorMessage(err)) } }) .finally(() => { @@ -329,8 +341,9 @@ const StakeModifySceneComponent: React.FC = props => { }, 10000) }) .catch((err: unknown) => { - showError(err) - setErrorMessage(lstrings.unknown_error_occurred_fragment) + // Surface the real error in the on-scene error field instead of a + // scary popup alert plus a generic "unknown error occurred". + setErrorMessage(getDisplayErrorMessage(err)) }) .finally(() => { setSliderLocked(false) diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index a6006046045..d9b958995ae 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -1745,6 +1745,8 @@ const strings = { stake_modal_modify_stake_title: 'Stake from %s', stake_modal_modify_unstake_title: 'Unstake from %s', stake_error_insufficient_s: 'Insufficient %s', + stake_error_insufficient_funds_unstake_s: + 'This wallet needs a %s balance to cover the network fee for unstaking.', stake_error_stake_below_minimum: 'Stake amount below minimum', stake_error_unstake_below_minimum: 'Unstake amount below minimum', state_error_pool_full_s: diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 39c1368be78..deba8f10ac7 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -1368,6 +1368,7 @@ "stake_modal_modify_stake_title": "Stake from %s", "stake_modal_modify_unstake_title": "Unstake from %s", "stake_error_insufficient_s": "Insufficient %s", + "stake_error_insufficient_funds_unstake_s": "This wallet needs a %s balance to cover the network fee for unstaking.", "stake_error_stake_below_minimum": "Stake amount below minimum", "stake_error_unstake_below_minimum": "Unstake amount below minimum", "state_error_pool_full_s": "The %1$s asset pool is currency full. Please try again later.", diff --git a/src/util/stakeErrorUtils.ts b/src/util/stakeErrorUtils.ts new file mode 100644 index 00000000000..3a4fdc122bf --- /dev/null +++ b/src/util/stakeErrorUtils.ts @@ -0,0 +1,12 @@ +import { lstrings } from '../locales/strings' + +/** + * Extract a user-presentable message from an unknown error thrown while + * fetching or approving a stake change quote. Falls back to a generic string + * when the error carries no message, so the on-scene error field can show the + * real reason instead of a scary popup alert plus "unknown error occurred". + */ +export const getDisplayErrorMessage = (err: unknown): string => + err instanceof Error && err.message !== '' + ? err.message + : lstrings.unknown_error_occurred_fragment