Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
57 changes: 57 additions & 0 deletions src/__tests__/util/recipientNotReady.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
9 changes: 9 additions & 0 deletions src/components/scenes/SendScene2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ import {
parseOwnNetworkPayment,
peekPaymentUri
} from '../../util/paymentUri'
import { describeRecipientNotReady } from '../../util/recipientNotReady'
import {
disableAssetsCover,
getStealthDisableAssets,
Expand Down Expand Up @@ -3987,6 +3988,14 @@ const SendComponent: React.FC<Props> = 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
Expand Down
6 changes: 6 additions & 0 deletions src/locales/en_US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions src/locales/strings/enUS.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
53 changes: 53 additions & 0 deletions src/util/recipientNotReady.ts
Original file line number Diff line number Diff line change
@@ -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<typeof asRecipientNotReadyError>
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)
}
Loading