diff --git a/CHANGELOG.md b/CHANGELOG.md index e4252ed1cce..9eb995f00c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - fixed: AddressTile2 touchable area states - fixed: Cases where it was possible to create duplicate custom tokens - fixed: Clear previous swap errors when new amounts are entered or swap assets are changed in `SwapCreateScene` +- fixed: Handle race condition when navigating to a token's transaction list which requires token activation (XRP, Algorand, etc) - fixed: Message about overriding a built-in token contract, which is not possible to do - fixed: Round Kado-provided amounts during sell diff --git a/src/actions/WalletActions.tsx b/src/actions/WalletActions.tsx index fae1fcd4233..6abcfeb8b81 100644 --- a/src/actions/WalletActions.tsx +++ b/src/actions/WalletActions.tsx @@ -11,10 +11,10 @@ import { Airship, showError, showToast } from '../components/services/AirshipIns import { getSpecialCurrencyInfo, SPECIAL_CURRENCY_INFO } from '../constants/WalletAndCurrencyConstants' import { lstrings } from '../locales/strings' import { selectDisplayDenomByCurrencyCode } from '../selectors/DenominationSelectors' -import { Dispatch, RootState, ThunkAction } from '../types/reduxTypes' +import { ThunkAction } from '../types/reduxTypes' import { NavigationBase } from '../types/routerTypes' import { MapObject } from '../types/types' -import { getCurrencyCode, getToken, isKeysOnlyPlugin } from '../util/CurrencyInfoHelpers' +import { getCurrencyCode, isKeysOnlyPlugin } from '../util/CurrencyInfoHelpers' import { getWalletName } from '../util/CurrencyWalletHelpers' import { fetchInfo } from '../util/network' import { convertCurrencyFromExchangeRates } from '../util/utils' @@ -53,7 +53,7 @@ export function selectWalletToken({ navigation, walletId, tokenId, alwaysActivat if (tokenId != null) { const { unactivatedTokenIds } = wallet if (unactivatedTokenIds.find(unactivatedTokenId => unactivatedTokenId === tokenId) != null) { - await activateWalletTokens(dispatch, state, navigation, wallet, [tokenId]) + await dispatch(activateWalletTokens(navigation, wallet, [tokenId])) return false } if (walletId !== currentWalletId || currencyCode !== currentWalletCurrencyCode) { @@ -147,101 +147,97 @@ export function updateMostRecentWalletsSelected(walletId: string, tokenId: EdgeT } } -const activateWalletTokens = async ( - dispatch: Dispatch, - state: RootState, - navigation: NavigationBase, - wallet: EdgeCurrencyWallet, - tokenIds?: string[] -): Promise => { - if (tokenIds == null) throw new Error('Activating mainnet wallets unsupported') - const { account } = state.core - const { defaultIsoFiat, defaultFiat } = state.ui.settings - const { assetOptions } = await account.getActivationAssets({ activateWalletId: wallet.id, activateTokenIds: tokenIds }) - const { pluginId } = wallet.currencyInfo - - // See if there is only one wallet option for activation - if (assetOptions.length === 1 && assetOptions[0].paymentWalletId != null) { - const { paymentWalletId, tokenId } = assetOptions[0] - const activationQuote = await account.activateWallet({ - activateWalletId: wallet.id, - activateTokenIds: tokenIds, - paymentInfo: { - walletId: paymentWalletId, - tokenId - } - }) - const tokensText = tokenIds.map(tokenId => { - const { currencyCode, displayName } = getToken(wallet, tokenId) ?? {} - return `${displayName} (${currencyCode})` - }) - const tileTitle = tokenIds.length > 1 ? lstrings.activate_wallet_tokens_scene_tile_title : lstrings.activate_wallet_token_scene_tile_title - const tileBody = tokensText.join(', ') - - const { networkFee } = activationQuote - const { nativeAmount: nativeFee, currencyPluginId, tokenId: feeTokenId } = networkFee - if (currencyPluginId !== pluginId) throw new Error('Internal Error: Fee asset mismatch.') - - const paymentCurrencyCode = getCurrencyCode(wallet, feeTokenId) - - const exchangeNetworkFee = await wallet.nativeToDenomination(nativeFee, paymentCurrencyCode) - const feeDenom = selectDisplayDenomByCurrencyCode(state, wallet.currencyConfig, paymentCurrencyCode) - const displayFee = div(nativeFee, feeDenom.multiplier, log10(feeDenom.multiplier)) - let fiatFee = convertCurrencyFromExchangeRates(state.exchangeRates, paymentCurrencyCode, defaultIsoFiat, exchangeNetworkFee) - if (lt(fiatFee, '0.001')) fiatFee = '<0.001' - else fiatFee = round(fiatFee, -3) - const feeString = `${displayFee} ${feeDenom.name} (${fiatFee} ${defaultFiat})` - let bodyText = lstrings.activate_wallet_token_scene_body - - const { tokenActivationAdditionalReserveText } = SPECIAL_CURRENCY_INFO[pluginId] ?? {} - if (tokenActivationAdditionalReserveText != null) { - bodyText += '\n\n' + tokenActivationAdditionalReserveText - } - - navigation.navigate('confirmScene', { - titleText: lstrings.activate_wallet_token_scene_title, - bodyText, - infoTiles: [ - { label: tileTitle, value: tileBody }, - { label: lstrings.mining_fee, value: feeString } - ], - onConfirm: (resetSlider: () => void) => { - if (lt(wallet.balanceMap.get(feeTokenId) ?? '0', nativeFee)) { - const msg = tokenIds.length > 1 ? lstrings.activate_wallet_tokens_insufficient_funds_s : lstrings.activate_wallet_token_insufficient_funds_s - Airship.show<'ok' | undefined>(bridge => ( - - )).catch(err => showError(err)) - navigation.pop() - return +export function activateWalletTokens(navigation: NavigationBase, wallet: EdgeCurrencyWallet, tokenIds: EdgeTokenId[]): ThunkAction> { + return async (_dispatch, getState) => { + const state = getState() + const { account } = state.core + const { defaultIsoFiat, defaultFiat } = state.ui.settings + const { assetOptions } = await account.getActivationAssets({ activateWalletId: wallet.id, activateTokenIds: tokenIds }) + const { pluginId } = wallet.currencyInfo + + // See if there is only one wallet option for activation + if (assetOptions.length === 1 && assetOptions[0].paymentWalletId != null) { + const { paymentWalletId, tokenId } = assetOptions[0] + const activationQuote = await account.activateWallet({ + activateWalletId: wallet.id, + activateTokenIds: tokenIds, + paymentInfo: { + walletId: paymentWalletId, + tokenId } + }) + const tokensText = tokenIds.map(tokenId => { + const { currencyCode, displayName } = tokenId != null ? wallet.currencyConfig.allTokens[tokenId] : wallet.currencyInfo + return `${displayName} (${currencyCode})` + }) + const tileTitle = tokenIds.length > 1 ? lstrings.activate_wallet_tokens_scene_tile_title : lstrings.activate_wallet_token_scene_tile_title + const tileBody = tokensText.join(', ') + + const { networkFee } = activationQuote + const { nativeAmount: nativeFee, currencyPluginId, tokenId: feeTokenId } = networkFee + if (currencyPluginId !== pluginId) throw new Error('Internal Error: Fee asset mismatch.') + + const paymentCurrencyCode = getCurrencyCode(wallet, feeTokenId) + + const exchangeNetworkFee = await wallet.nativeToDenomination(nativeFee, paymentCurrencyCode) + const feeDenom = selectDisplayDenomByCurrencyCode(state, wallet.currencyConfig, paymentCurrencyCode) + const displayFee = div(nativeFee, feeDenom.multiplier, log10(feeDenom.multiplier)) + let fiatFee = convertCurrencyFromExchangeRates(state.exchangeRates, paymentCurrencyCode, defaultIsoFiat, exchangeNetworkFee) + if (lt(fiatFee, '0.001')) fiatFee = '<0.001' + else fiatFee = round(fiatFee, -3) + const feeString = `${displayFee} ${feeDenom.name} (${fiatFee} ${defaultFiat})` + let bodyText = lstrings.activate_wallet_token_scene_body + + const { tokenActivationAdditionalReserveText } = SPECIAL_CURRENCY_INFO[pluginId] ?? {} + if (tokenActivationAdditionalReserveText != null) { + bodyText += '\n\n' + tokenActivationAdditionalReserveText + } - const name = activateWalletName[pluginId]?.name ?? lstrings.activate_wallet_token_transaction_name_category_generic - const notes = activateWalletName[pluginId]?.notes ?? lstrings.activate_wallet_token_transaction_notes_generic - activationQuote - .approve({ - metadata: { - name, - category: `Expense:${lstrings.activate_wallet_token_transaction_name_category_generic}`, - notes - } - }) - .then(result => { - showToast(lstrings.activate_wallet_token_success, ACTIVATION_TOAST_AUTO_HIDE_MS) - navigation.pop() - }) - .catch(e => { + navigation.navigate('confirmScene', { + titleText: lstrings.activate_wallet_token_scene_title, + bodyText, + infoTiles: [ + { label: tileTitle, value: tileBody }, + { label: lstrings.mining_fee, value: feeString } + ], + onConfirm: (resetSlider: () => void) => { + if (lt(wallet.balanceMap.get(feeTokenId) ?? '0', nativeFee)) { + const msg = tokenIds.length > 1 ? lstrings.activate_wallet_tokens_insufficient_funds_s : lstrings.activate_wallet_token_insufficient_funds_s + Airship.show<'ok' | undefined>(bridge => ( + + )).catch(err => showError(err)) navigation.pop() - showError(e) - }) - } - }) - } else { - throw new Error('Activation with multiple wallet options not supported yet') + return + } + + const name = activateWalletName[pluginId]?.name ?? lstrings.activate_wallet_token_transaction_name_category_generic + const notes = activateWalletName[pluginId]?.notes ?? lstrings.activate_wallet_token_transaction_notes_generic + activationQuote + .approve({ + metadata: { + name, + category: `Expense:${lstrings.activate_wallet_token_transaction_name_category_generic}`, + notes + } + }) + .then(result => { + showToast(lstrings.activate_wallet_token_success, ACTIVATION_TOAST_AUTO_HIDE_MS) + navigation.pop() + }) + .catch(e => { + navigation.pop() + showError(e) + }) + } + }) + } else { + throw new Error('Activation with multiple wallet options not supported yet') + } } } diff --git a/src/components/scenes/ConfirmScene.tsx b/src/components/scenes/ConfirmScene.tsx index 04e9b568e53..6e38530ed18 100644 --- a/src/components/scenes/ConfirmScene.tsx +++ b/src/components/scenes/ConfirmScene.tsx @@ -6,13 +6,13 @@ import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { useHandler } from '../../hooks/useHandler' import { lstrings } from '../../locales/strings' import { EdgeSceneProps } from '../../types/routerTypes' +import { EdgeButton } from '../buttons/EdgeButton' import { SceneWrapper } from '../common/SceneWrapper' import { EdgeRow } from '../rows/EdgeRow' import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' import { EdgeText } from '../themed/EdgeText' -import { MainButton } from '../themed/MainButton' import { SafeSlider } from '../themed/SafeSlider' -import { SceneHeader } from '../themed/SceneHeader' +import { SceneHeaderUi4 } from '../themed/SceneHeaderUi4' interface Props extends EdgeSceneProps<'confirmScene'> {} @@ -24,7 +24,7 @@ export interface ConfirmSceneParams { onBack?: () => void } -const ConfirmComponent = (props: Props) => { +const ConfirmSceneComponent = (props: Props) => { const { navigation, route } = props const theme = useTheme() const styles = getStyles(theme) @@ -52,9 +52,9 @@ const ConfirmComponent = (props: Props) => { }, [onBack]) return ( - + - + {bodyText} @@ -63,14 +63,14 @@ const ConfirmComponent = (props: Props) => { {renderInfoTiles()} - + ) } -export const ConfirmScene = React.memo(ConfirmComponent) +export const ConfirmScene = React.memo(ConfirmSceneComponent) const getStyles = cacheStyles((theme: Theme) => ({ titleText: { @@ -79,8 +79,7 @@ const getStyles = cacheStyles((theme: Theme) => ({ body: { alignItems: 'center', justifyContent: 'center', - margin: theme.rem(1), - marginTop: theme.rem(1.5) + margin: theme.rem(0.5) }, footer: { margin: theme.rem(1), diff --git a/src/components/scenes/TransactionListScene.tsx b/src/components/scenes/TransactionListScene.tsx index 62b379a7855..4643098b6ba 100644 --- a/src/components/scenes/TransactionListScene.tsx +++ b/src/components/scenes/TransactionListScene.tsx @@ -4,8 +4,10 @@ import { ListRenderItemInfo, Platform, RefreshControl, View } from 'react-native import Animated from 'react-native-reanimated' import { useSafeAreaFrame } from 'react-native-safe-area-context' +import { activateWalletTokens } from '../../actions/WalletActions' import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { SPECIAL_CURRENCY_INFO } from '../../constants/WalletAndCurrencyConstants' +import { useAsyncEffect } from '../../hooks/useAsyncEffect' import { useHandler } from '../../hooks/useHandler' import { useIconColor } from '../../hooks/useIconColor' import { useTransactionList } from '../../hooks/useTransactionList' @@ -14,7 +16,7 @@ import { lstrings } from '../../locales/strings' import { getExchangeDenomByCurrencyCode } from '../../selectors/DenominationSelectors' import { FooterRender } from '../../state/SceneFooterState' import { useSceneScrollHandler } from '../../state/SceneScrollState' -import { useSelector } from '../../types/reactRedux' +import { useDispatch, useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' import { infoServerData } from '../../util/network' import { calculateSpamThreshold, darkenHexColor, unixToLocaleDateTime, zeroString } from '../../util/utils' @@ -47,6 +49,7 @@ function TransactionListComponent(props: Props) { const { navigation, route, wallet } = props const theme = useTheme() const styles = getStyles(theme) + const dispatch = useDispatch() const { width: screenWidth } = useSafeAreaFrame() @@ -70,6 +73,7 @@ function TransactionListComponent(props: Props) { // Watchers: const enabledTokenIds = useWatch(wallet, 'enabledTokenIds') + const unactivatedTokenIds = useWatch(wallet, 'unactivatedTokenIds') // --------------------------------------------------------------------------- // Derived values @@ -139,6 +143,21 @@ function TransactionListComponent(props: Props) { } }, [enabledTokenIds, navigation, tokenId]) + // Automatically navigate to the token activation confirmation scene if + // the token appears in the unactivatedTokenIds list once the wallet loads + // this state. + useAsyncEffect( + async () => { + if (unactivatedTokenIds.length > 0) { + if (unactivatedTokenIds.some(unactivatedTokenId => unactivatedTokenId === tokenId)) { + await dispatch(activateWalletTokens(navigation, wallet, [tokenId])) + } + } + }, + [unactivatedTokenIds], + 'TransactionListScene unactivatedTokenIds check' + ) + // // Handlers // diff --git a/src/components/themed/MainButton.tsx b/src/components/themed/MainButton.tsx index faa8acd349e..77143f7b4b0 100644 --- a/src/components/themed/MainButton.tsx +++ b/src/components/themed/MainButton.tsx @@ -28,13 +28,13 @@ interface Props { // Which visual style to use. Defaults to primary (solid): type?: MainButtonType - // From ButtonUi4 + // From EdgeButton layout?: 'row' | 'column' | 'solo' } /** * @deprecated - * Use ButtonUi4 instead, and consider whether there is a genuine need for + * Use EdgeButton instead, and consider whether there is a genuine need for * special margins in MainButton use cases from a UI4 design perspective. */ export function MainButton(props: Props) { diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 2ab66a7804c..801b4ed855f 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -323,7 +323,6 @@ const strings = { activate_wallet_token_transaction_name_xrp: 'XRP Ledger', activate_wallet_token_transaction_notes_xrp: 'Activate XRP token by enabling Trust Line to issuer', activate_wallet_token_scene_title: 'Activate Token', - activate_wallet_tokens_scene_title: 'Activate Tokens', activate_wallet_token_scene_tile_title: 'Token to Activate', activate_wallet_tokens_scene_tile_title: 'Tokens to Activate', activate_wallet_token_scene_body: diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index c0c4b14d681..179e01e50d0 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -261,7 +261,6 @@ "activate_wallet_token_transaction_name_xrp": "XRP Ledger", "activate_wallet_token_transaction_notes_xrp": "Activate XRP token by enabling Trust Line to issuer", "activate_wallet_token_scene_title": "Activate Token", - "activate_wallet_tokens_scene_title": "Activate Tokens", "activate_wallet_token_scene_tile_title": "Token to Activate", "activate_wallet_tokens_scene_tile_title": "Tokens to Activate", "activate_wallet_token_scene_body": "To send and receive the selected token you will first need to activate it with a blockchain transaction. This transaction will cost the following fee.\n\nPlease confirm using the slider below.",