diff --git a/CHANGELOG.md b/CHANGELOG.md index ea74eb21598..2bf66ca9477 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased (develop) +- changed: Send scene says why a recipient can't receive a token instead of showing a network error - fixed: Exchange info, gift card info, Houdini chain, and Zcash migration status refreshes running twice per interval ## 4.53.0 (staging) diff --git a/src/__tests__/util/recipientNotReady.test.ts b/src/__tests__/util/recipientNotReady.test.ts new file mode 100644 index 00000000000..39d45afe0ea --- /dev/null +++ b/src/__tests__/util/recipientNotReady.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from '@jest/globals' + +import { I18nError } from '../../components/cards/ErrorCard' +import { describeRecipientNotReady } from '../../util/recipientNotReady' + +/** The error an engine throws, as it arrives through the core bridge. */ +const makeRefusal = (reason: string): Error => { + const error = new Error('The engine message') + return Object.assign(error, { + name: 'RecipientNotReadyError', + reason, + currencyCode: 'USDC' + }) +} + +describe('describeRecipientNotReady', () => { + it('says the recipient account is not activated', () => { + const error = describeRecipientNotReady( + makeRefusal('accountNotActivated'), + 'Stellar' + ) + expect(error).toBeInstanceOf(I18nError) + expect(error?.title).toBe('Transaction Failure') + expect(error?.message).toBe( + "The recipient's Stellar account isn't activated, so it can't receive USDC yet." + ) + }) + + it('says the recipient has not added the token', () => { + expect( + describeRecipientNotReady(makeRefusal('tokenNotAdded'), 'Stellar') + ?.message + ).toBe( + "The recipient hasn't added USDC to their Stellar wallet, so it can't receive USDC yet." + ) + }) + + it('says the issuer has not authorized the recipient', () => { + expect( + describeRecipientNotReady(makeRefusal('notAuthorized'), 'Stellar') + ?.message + ).toBe("The USDC issuer hasn't authorized the recipient to receive USDC.") + }) + + it("shows the engine's message for a reason it does not know", () => { + expect( + describeRecipientNotReady(makeRefusal('somethingNew'), 'Stellar')?.message + ).toBe('The engine message') + }) + + it('leaves other errors alone', () => { + expect( + describeRecipientNotReady(new Error('Network down'), 'Stellar') + ).toBeUndefined() + expect(describeRecipientNotReady(undefined, 'Stellar')).toBeUndefined() + }) +}) diff --git a/src/components/scenes/SendScene2.tsx b/src/components/scenes/SendScene2.tsx index 16950034b4c..c7be074f458 100644 --- a/src/components/scenes/SendScene2.tsx +++ b/src/components/scenes/SendScene2.tsx @@ -124,6 +124,7 @@ import { parseOwnNetworkPayment, peekPaymentUri } from '../../util/paymentUri' +import { describeRecipientNotReady } from '../../util/recipientNotReady' import { disableAssetsCover, getStealthDisableAssets, @@ -3987,6 +3988,14 @@ const SendComponent: React.FC = props => { ) } + // The recipient can't receive this asset yet, so say what they need + // to do instead of blaming the network: + const recipientNotReady = describeRecipientNotReady( + error, + coreWallet.currencyInfo.displayName + ) + if (recipientNotReady != null) error = recipientNotReady + // Only set hasPendingTx to true when pending tx error occurs; // don't clear it for other errors as it may have been legitimately // set by handleTxUpdate or updatePendingTxState diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 4ba6461f43d..5c37dfe2e6a 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -1781,6 +1781,12 @@ const strings = { send_address_expired_error_message: 'Payment Address Expired', send_funds_not_spendable_error_message: 'These funds are not spendable yet. Wait for the wallet to finish syncing, then try again.', + send_recipient_account_not_activated_2s: + "The recipient's %1$s account isn't activated, so it can't receive %2$s yet.", + send_recipient_token_not_added_2s: + "The recipient hasn't added %1$s to their %2$s wallet, so it can't receive %1$s yet.", + send_recipient_not_authorized_1s: + "The %1$s issuer hasn't authorized the recipient to receive %1$s.", // Request Scene request_balance: 'You have %s', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 2263dcb9d22..fdfa1fc6a47 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -1386,6 +1386,9 @@ "send_address_expire_title": "Payment Address Expiration Time", "send_address_expired_error_message": "Payment Address Expired", "send_funds_not_spendable_error_message": "These funds are not spendable yet. Wait for the wallet to finish syncing, then try again.", + "send_recipient_account_not_activated_2s": "The recipient's %1$s account isn't activated, so it can't receive %2$s yet.", + "send_recipient_token_not_added_2s": "The recipient hasn't added %1$s to their %2$s wallet, so it can't receive %1$s yet.", + "send_recipient_not_authorized_1s": "The %1$s issuer hasn't authorized the recipient to receive %1$s.", "request_balance": "You have %s", "exchange_same_address_upgrade": "Asset Upgrade", "exchange_generic_error_title": "Exchange Error", diff --git a/src/util/recipientNotReady.ts b/src/util/recipientNotReady.ts new file mode 100644 index 00000000000..dfff6c74e1e --- /dev/null +++ b/src/util/recipientNotReady.ts @@ -0,0 +1,53 @@ +import { asObject, asString, asValue } from 'cleaners' +import { sprintf } from 'sprintf-js' + +import { I18nError } from '../components/cards/ErrorCard' +import { lstrings } from '../locales/strings' + +/** + * An engine refused a spend because the recipient can't receive the asset + * yet: an account that isn't activated, a token the recipient hasn't added, + * or an issuer that hasn't authorized them. + */ +const asRecipientNotReadyError = asObject({ + name: asValue('RecipientNotReadyError'), + reason: asString, + currencyCode: asString, + message: asString +}) + +/** + * Turns an engine's recipient refusal into a localized error the send scene + * can show, naming what the recipient has to do. Returns undefined for any + * other error. + */ +export function describeRecipientNotReady( + error: unknown, + chainName: string +): I18nError | undefined { + let refusal: ReturnType + try { + refusal = asRecipientNotReadyError(error) + } catch (cleanerError: unknown) { + return undefined + } + + const { currencyCode, reason } = refusal + const message = + reason === 'accountNotActivated' + ? sprintf( + lstrings.send_recipient_account_not_activated_2s, + chainName, + currencyCode + ) + : reason === 'tokenNotAdded' + ? sprintf( + lstrings.send_recipient_token_not_added_2s, + currencyCode, + chainName + ) + : reason === 'notAuthorized' + ? sprintf(lstrings.send_recipient_not_authorized_1s, currencyCode) + : refusal.message + return new I18nError(lstrings.transaction_failure, message) +}