From 1acef492ec80a434791eda89845875bc5adbb966 Mon Sep 17 00:00:00 2001 From: Juanky Date: Tue, 10 Nov 2026 11:00:33 -0700 Subject: [PATCH 1/6] feat(swap): only the coins travel on a flip, the provider row opens routes, fees under it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The flip starts on the press, rewound before the state changes, so the flipped layout no longer paints its end state first. Reduce motion is read once. - Only the coin chips travel, in a layer over both cards; words and numbers fade in place, so nothing hides at the seam and the height holds. - A flip keeps the chosen coin when the other direction's catalog has it, instead of falling back to BTC. - The whole Provider row is the button, with a chevron and no underline; the fees sit under it, said as Review says them (routeFeeValue), with swap.tip-fees on the info icon. - While a price is on its way the ring keeps its place and Provider reads "Getting a price…". Co-Authored-By: Claude Opus 5.5 --- __tests__/swap.useSwapForm.unit.tsx | 29 ++++++ __tests__/swap/routeFeeValue.test.ts | 52 ++++++++++ screens/Swap/Swap.tsx | 144 +++++++++++++++++--------- screens/Swap/components/PairCards.tsx | 131 +++++++++++++++++------ screens/Swap/routeFeeValue.ts | 51 +++++++++ screens/Swap/useSwapForm.ts | 5 +- screens/SwapReview/SwapReview.tsx | 40 ++----- 7 files changed, 338 insertions(+), 114 deletions(-) create mode 100644 __tests__/swap/routeFeeValue.test.ts create mode 100644 screens/Swap/routeFeeValue.ts diff --git a/__tests__/swap.useSwapForm.unit.tsx b/__tests__/swap.useSwapForm.unit.tsx index d2a39f6776..d0b6ea2e31 100644 --- a/__tests__/swap.useSwapForm.unit.tsx +++ b/__tests__/swap.useSwapForm.unit.tsx @@ -262,3 +262,32 @@ test('Tests that a sale asked about again fills the coin received, the amount an expect(result.current.amount).toBe('0.01'); expect(result.current.activeAddress).toBe(BTC_ADDRESS); }); + +test('Tests that the chosen coin stays when a flip loads the other direction’s catalog', async () => { + const USDT: TokenEntryType = { + chain: 'ETH', + chainId: '1', + ticker: 'USDT', + identifier: 'ETH.USDT-0xdAC17F958D2ee523a2206206994597C13D831ec7', + symbol: 'USDT', + name: 'Tether', + decimals: 6, + }; + const { swapService } = service(); + ( + swapService.listRoutableTokens as jest.MockedFunction< + SwapService['listRoutableTokens'] + > + ).mockImplementation(async () => [BTC, USDT]); + const { result } = render(swapService); + await waitFor(() => expect(result.current.selectedToken).toEqual(BTC)); + act(() => result.current.setSelectedToken(USDT)); + + act(() => result.current.flip()); + + await waitFor(() => expect(result.current.isOutbound).toBe(false)); + await waitFor(() => + expect(swapService.listRoutableTokens).toHaveBeenCalledTimes(2), + ); + expect(result.current.selectedToken).toEqual(USDT); +}); diff --git a/__tests__/swap/routeFeeValue.test.ts b/__tests__/swap/routeFeeValue.test.ts new file mode 100644 index 0000000000..b0e33ba33f --- /dev/null +++ b/__tests__/swap/routeFeeValue.test.ts @@ -0,0 +1,52 @@ +jest.mock('@app/RPCModule', () => + require('../../__mocks__/rpcModuleProxy').rpcModuleProxyMock(), +); + +import type { + FiatValueBasisType, + QuoteInput, + RouteOptionType, +} from '@app/swap'; +import { SwapKitProviderEnum } from '@app/swap/enums/SwapKitProviderEnum'; +import { routeFeeValue } from '@screens/Swap/routeFeeValue'; + +const route = (overrides: Partial = {}): RouteOptionType => ({ + routeId: 'route-near', + provider: SwapKitProviderEnum.Near, + expectedReceiveAmount: '0.98', + minReceiveAmount: '0.97', + ...overrides, +}); + +const quoteInput = { + sellAsset: { ticker: 'ZEC', symbol: 'ZEC' }, + receiveAsset: { ticker: 'USDT', symbol: 'USDT' }, + sellAmountHumanDecimal: '2', +} as unknown as QuoteInput; + +const prices = { + sellUsdUnitPrice: 50, + receiveUsdUnitPrice: 1, +} as FiatValueBasisType; + +test('Tests that the fee reads as a share of the sale with its dollars when it is in the sold asset', () => { + expect( + routeFeeValue({ + route: route({ totalFeesInSellAsset: '0.02' }), + quoteInput, + fiatValueBasis: prices, + isOutbound: true, + }), + ).toBe('1% · ≈ $1.00'); +}); + +test('Tests that the fee reads as the amount when no share can be told', () => { + expect( + routeFeeValue({ + route: route(), + quoteInput, + fiatValueBasis: prices, + isOutbound: true, + }), + ).toBe('—'); +}); diff --git a/screens/Swap/Swap.tsx b/screens/Swap/Swap.tsx index 7c3d8b5dee..ca209b7f34 100644 --- a/screens/Swap/Swap.tsx +++ b/screens/Swap/Swap.tsx @@ -91,6 +91,7 @@ import SwapSettingsSheet from './components/SwapSettingsSheet'; import SwapHistorySheet, { swapPairLabel } from './components/SwapHistorySheet'; import { tokenToSwapAsset } from './swapAssets'; import { useCoinPrice } from './useCoinPrice'; +import { routeFeeValue } from './routeFeeValue'; import { onTabScroll } from '@app/navigation/tabBarScroll'; import { useScrollEdges } from '@ui/widgets/ScrollEdges'; import { PANEL_TOP } from '@ui/widgets/Header/Header'; @@ -282,6 +283,22 @@ const Swap: React.FunctionComponent = ({ route }) => { form.chosenRoute && receiveUsdUnitPrice > 0 ? `≈ ${formatUsd(parseFloat(form.chosenRoute.expectedReceiveAmount) * receiveUsdUnitPrice)}` : ''; + const feeValue = + form.chosenRoute && form.quoteContext + ? routeFeeValue({ + route: form.chosenRoute, + quoteInput: form.quoteContext.quoteInput, + fiatValueBasis: form.quoteContext.fiatValueBasis, + isOutbound: form.isOutbound, + }) + : ''; + const providerRow = { + flexDirection: 'row' as const, + alignItems: 'center' as const, + columnGap: space8, + minHeight: 44, + paddingHorizontal: 4, + }; // The newest swap in progress. A late payment on its way back is not one; // it has no card. @@ -623,7 +640,7 @@ const Swap: React.FunctionComponent = ({ route }) => { receivePlaceholder="0" receiveAltText={receiveUsd} ring={ - form.routes?.length + form.routes?.length || form.quoting ? { resetKey: form.refreshedAtMs, durationMs: QUOTE_REFRESH_MS, @@ -714,46 +731,29 @@ const Swap: React.FunctionComponent = ({ route }) => { )} - {!!form.selectedToken && ( - - - {translate('swap.provider') as string} - - {form.chosenRoute ? ( - <> - routesSheetRef.current?.present()} - hitSlop={8} - style={{ minHeight: 32, justifyContent: 'center' }} + {!!form.selectedToken && + (form.chosenRoute ? ( + <> + routesSheetRef.current?.present()} + style={providerRow} + > + - - {providerShortLabel(form.chosenRoute.provider)} - - + {translate('swap.provider') as string} + + + {providerShortLabel(form.chosenRoute.provider)} + {shielded.isShielded(form.chosenRoute.provider) && ( = ({ route }) => { {form.chosenRoute.estimatedTimeText} )} - - ) : ( + + + {!!feeValue && ( + + + + {translate('swap.review-fees') as string} + + + <> + + + + {feeValue} + + + )} + + ) : ( + + + {translate('swap.provider') as string} + { translate( - form.cta === 'noRoutes' - ? 'swap.provider-none' - : 'swap.provider-pending', + form.quoting + ? 'swap.cta-quoting' + : form.cta === 'noRoutes' + ? 'swap.provider-none' + : 'swap.provider-pending', ) as string } - )} - - )} + + ))} {form.rateLimited && ( ({ x, y, height }); +/** A chip on its way, centred on a row's height as the row centres it. */ +const travellingChip = (row: RowBox) => ({ + position: 'absolute' as const, + top: 0, + left: row.x, + height: row.height, + justifyContent: 'center' as const, +}); + export type PairCardsTexts = { youSend: string; youReceive: string; @@ -130,19 +144,26 @@ export default function PairCards({ testID="swap.pick-asset" /> ); + /** The picker as it looks, edge and chevron included, for the copy that travels. */ + const tokenChip = (surface: string) => ( + + ); const [sendH, setSendH] = useState(0); - const progress = useRef(new Animated.Value(1)).current; - const flipped = useRef(isOutbound); + const [sendRow, setSendRow] = useState(NO_ROW); + const [receiveRow, setReceiveRow] = useState(NO_ROW); + const [progress] = useState(() => new Animated.Value(1)); + /** + * Read once: asked on the press, the answer came after the flipped layout + * had painted, which showed the end state before the animation. + */ + const [reduceMotion, setReduceMotion] = useState(false); useEffect(() => { - if (flipped.current === isOutbound) { - return; - } - flipped.current = isOutbound; - AccessibilityInfo.isReduceMotionEnabled().then(reduce => { - if (reduce) { - return; - } + AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion); + }, []); + /** Rewound before the state changes, so the flipped layout first paints at the start. */ + const flip = () => { + if (!reduceMotion) { progress.setValue(0); Animated.timing(progress, { toValue: 1, @@ -150,20 +171,31 @@ export default function PairCards({ easing: Easing.bezier(0.2, 0, 0, 1), useNativeDriver: true, }).start(); - }); - }, [isOutbound, progress]); - /** Each row starts where the other one was, a card and the seam away. */ - const travel = sendH + SEAM; - const sendRowShift = progress.interpolate({ + } + onFlip(); + }; + /** The receive card's coin row, measured from the top of the send card. */ + const receiveTop = sendH + SEAM + receiveRow.y; + const sendChipShift = progress.interpolate({ inputRange: [0, 1], - outputRange: [travel, 0], + outputRange: [receiveTop, sendRow.y], }); - const receiveRowShift = progress.interpolate({ + const receiveChipShift = progress.interpolate({ inputRange: [0, 1], - outputRange: [-travel, 0], + outputRange: [sendRow.y, receiveTop], + }); + /** The chips in the rows stand in for the travelling ones once they land. */ + const landed = progress.interpolate({ + inputRange: [0, 0.999, 1], + outputRange: [0, 0, 1], }); + const travelling = progress.interpolate({ + inputRange: [0, 0.999, 1], + outputRange: [1, 1, 0], + }); + /** The words and the numbers cross in place: out at once, in after a beat. */ const words = progress.interpolate({ - inputRange: [0, 0.35, 1], + inputRange: [0, 0.3, 1], outputRange: [0, 0, 1], }); const spin = progress.interpolate({ @@ -221,7 +253,6 @@ export default function PairCards({ alignItems: 'center' as const, columnGap: space12, marginTop: space8, - zIndex: 3, }; const underRow = { flexDirection: 'row' as const, @@ -265,11 +296,14 @@ export default function PairCards({ )} - setSendRow(boxOf(e.nativeEvent.layout))} > - {isOutbound ? zecChip : pickChip(sendSurface)} - + {isOutbound ? zecChip : pickChip(sendSurface)} + + {unit === 'usd' && ( @@ -303,8 +338,8 @@ export default function PairCards({ ...singleLineInput(textVariants.balance.lineHeight), }} /> - - + + {onToggleUnit ? ( - setReceiveRow(boxOf(e.nativeEvent.layout))} > - {isOutbound ? pickChip(receiveSurface) : zecChip} - + {isOutbound ? pickChip(receiveSurface) : zecChip} + + {!!ring && ( @@ -445,14 +484,40 @@ export default function PairCards({ > {receiveText || receivePlaceholder} - - + + {receiveAltText} + + {/* Only the coins travel, over both cards, so neither hides behind + the other at the seam. */} + + + {isOutbound ? zecChip : tokenChip(sendSurface)} + + + {isOutbound ? tokenChip(receiveSurface) : zecChip} + + ); } diff --git a/screens/Swap/routeFeeValue.ts b/screens/Swap/routeFeeValue.ts new file mode 100644 index 0000000000..3fb205a4e1 --- /dev/null +++ b/screens/Swap/routeFeeValue.ts @@ -0,0 +1,51 @@ +import { formatAmountForDisplay } from '@app/swap'; +import type { + FiatValueBasisType, + QuoteInput, + RouteOptionType, +} from '@app/swap'; +import { formatPercent, formatUsd } from '@app/utils/formatAmount'; + +/** + * A route's fees as a share of what is sold, with its dollars; the amount + * when no share can be told. Review and the swap screen say it the same way. + */ +export function routeFeeValue(args: { + route: RouteOptionType; + quoteInput: QuoteInput; + fiatValueBasis: FiatValueBasisType; + isOutbound: boolean; +}): string { + const { route, quoteInput, fiatValueBasis, isOutbound } = args; + const sellSymbol = quoteInput.sellAsset.ticker ?? quoteInput.sellAsset.symbol; + const receiveSymbol = + quoteInput.receiveAsset.ticker ?? quoteInput.receiveAsset.symbol; + const feeText = isOutbound + ? route.totalFeesInSellAsset + ? `${formatAmountForDisplay(route.totalFeesInSellAsset)} ${sellSymbol}` + : '—' + : route.totalFeesInReceiveAsset + ? `${formatAmountForDisplay(route.totalFeesInReceiveAsset)} ${receiveSymbol}` + : '—'; + + const sellAmount = parseFloat(quoteInput.sellAmountHumanDecimal); + const feesInSell = parseFloat(route.totalFeesInSellAsset ?? ''); + const feesInReceive = parseFloat(route.totalFeesInReceiveAsset ?? ''); + const receiveAmount = parseFloat(route.expectedReceiveAmount); + const feeShare = Number.isFinite(feesInSell) + ? feesInSell / sellAmount + : Number.isFinite(feesInReceive) + ? feesInReceive / (receiveAmount + feesInReceive) + : NaN; + const feeUsd = Number.isFinite(feesInSell) + ? feesInSell * fiatValueBasis.sellUsdUnitPrice + : feesInReceive * fiatValueBasis.receiveUsdUnitPrice; + return Number.isFinite(feeShare) + ? [ + `${formatPercent(feeShare * 100)}%`, + Number.isFinite(feeUsd) && feeUsd > 0 ? `≈ ${formatUsd(feeUsd)}` : '', + ] + .filter(Boolean) + .join(' · ') + : feeText; +} diff --git a/screens/Swap/useSwapForm.ts b/screens/Swap/useSwapForm.ts index 61afb8d79d..0ea62aaa25 100644 --- a/screens/Swap/useSwapForm.ts +++ b/screens/Swap/useSwapForm.ts @@ -235,8 +235,11 @@ export function useSwapForm({ setCatalogError(''); setCatalogEdgeBlocked(false); setCatalogUnreachable(false); + // A flip asks the catalog again; the coin the user chose stays when it is there. setSelectedToken( - catalog.find(t => t.identifier === DEFAULT_TOKEN_IDENTIFIER) ?? + current => + catalog.find(t => t.identifier === current?.identifier) ?? + catalog.find(t => t.identifier === DEFAULT_TOKEN_IDENTIFIER) ?? catalog[0] ?? null, ); diff --git a/screens/SwapReview/SwapReview.tsx b/screens/SwapReview/SwapReview.tsx index 400ddb9683..b95b6fdbfd 100644 --- a/screens/SwapReview/SwapReview.tsx +++ b/screens/SwapReview/SwapReview.tsx @@ -39,15 +39,12 @@ import SwapFlowScreen from '@screens/Swap/components/SwapFlowScreen'; import PairHero from '@screens/Swap/components/PairHero'; import NymNote from '@screens/Swap/components/NymNote'; import DetailRows from '@screens/Swap/components/DetailRows'; +import { routeFeeValue } from '@screens/Swap/routeFeeValue'; import { useShieldedRoutes } from '@screens/Swap/useShieldedRoutes'; import Card from '@ui/widgets/Card'; import Avatar, { avatarCategoryOf } from '@ui/widgets/Avatar'; import { formatSlippagePercent } from '@screens/Swap/components/SwapSettingsSheet'; -import { - formatAmount, - formatPercent, - formatUsd, -} from '@app/utils/formatAmount'; +import { formatAmount, formatUsd } from '@app/utils/formatAmount'; type SwapReviewProps = NativeStackScreenProps< AppDrawerParamList, @@ -137,13 +134,6 @@ const SwapReview: React.FunctionComponent = ({ const memoAckNeeded = paysWithMemo && isTokenAsset(quoteInput.sellAsset); const slippage = quoteInput.slippageBps ?? 0; const feeReserve = isOutbound ? zecNetworkFeeReserve(chosen.provider) : 0; - const feeText = isOutbound - ? chosen.totalFeesInSellAsset - ? `${formatAmountForDisplay(chosen.totalFeesInSellAsset)} ${sellSymbol}` - : '—' - : chosen.totalFeesInReceiveAsset - ? `${formatAmountForDisplay(chosen.totalFeesInReceiveAsset)} ${receiveSymbol}` - : '—'; // Inbound, the far side is the source: where the sold asset returns. const address = isOutbound ? quoteInput.destinationAddress @@ -282,27 +272,13 @@ const SwapReview: React.FunctionComponent = ({ commit(); }; - /** The fee as a share of what is sold, with its dollars; the amount when no share can be told. */ - const sellAmount = parseFloat(quoteInput.sellAmountHumanDecimal); - const feesInSell = parseFloat(chosen.totalFeesInSellAsset ?? ''); - const feesInReceive = parseFloat(chosen.totalFeesInReceiveAsset ?? ''); + const feeValue = routeFeeValue({ + route: chosen, + quoteInput, + fiatValueBasis, + isOutbound, + }); const receiveAmount = parseFloat(chosen.expectedReceiveAmount); - const feeShare = Number.isFinite(feesInSell) - ? feesInSell / sellAmount - : Number.isFinite(feesInReceive) - ? feesInReceive / (receiveAmount + feesInReceive) - : NaN; - const feeUsd = Number.isFinite(feesInSell) - ? feesInSell * fiatValueBasis.sellUsdUnitPrice - : feesInReceive * fiatValueBasis.receiveUsdUnitPrice; - const feeValue = Number.isFinite(feeShare) - ? [ - `${formatPercent(feeShare * 100)}%`, - Number.isFinite(feeUsd) && feeUsd > 0 ? `≈ ${formatUsd(feeUsd)}` : '', - ] - .filter(Boolean) - .join(' · ') - : feeText; const receiveUsd = receiveAmount * fiatValueBasis.receiveUsdUnitPrice; const counterpartyChain = counterparty.chain; From 58306eb3763a326c231b5ee6fe3e985a3c2f9b6d Mon Sep 17 00:00:00 2001 From: Juanky Date: Tue, 10 Nov 2026 11:00:33 -0700 Subject: [PATCH 2/6] feat(history): an empty wallet shows a sprout that tries to grow, and Receive ZEC No activity yet: the sprout tries to stand up, can't, and settles back down, while the fallen leaves breathe and one that got away drifts. Still under reduce motion. Receive ZEC opens the Receive tab. A filter that empties the list keeps "Nothing here yet." Co-Authored-By: Claude Opus 5.5 --- __tests__/History.empty.unit.tsx | 54 ++++ app/theme/emptyHistoryArt.ts | 20 ++ app/translations/en.json | 3 + app/translations/es.json | 3 + app/translations/pt.json | 3 + app/translations/ru.json | 3 + app/translations/tr.json | 3 + screens/History/History.tsx | 41 +++ ui/widgets/EmptySprout.stories.tsx | 15 ++ ui/widgets/EmptySprout.tsx | 420 +++++++++++++++++++++++++++++ 10 files changed, 565 insertions(+) create mode 100644 __tests__/History.empty.unit.tsx create mode 100644 app/theme/emptyHistoryArt.ts create mode 100644 ui/widgets/EmptySprout.stories.tsx create mode 100644 ui/widgets/EmptySprout.tsx diff --git a/__tests__/History.empty.unit.tsx b/__tests__/History.empty.unit.tsx new file mode 100644 index 0000000000..236941465e --- /dev/null +++ b/__tests__/History.empty.unit.tsx @@ -0,0 +1,54 @@ +/** + * @format + */ + +import 'react-native'; +import React from 'react'; + +import { act, fireEvent, render } from '@testing-library/react-native'; +import { useNavigation } from '@react-navigation/native'; +import History from '@screens/History'; +import { + defaultAppContextLoaded, + ContextAppLoadedProvider, +} from '@app/context'; +import { Route } from '@app/AppState'; +import { mockInfo } from '../__mocks__/dataMocks/mockInfo'; +import { mockTotalBalance } from '../__mocks__/dataMocks/mockTotalBalance'; +import { mockTranslate } from '../__mocks__/dataMocks/mockTranslate'; +import { mockAddresses } from '../__mocks__/dataMocks/mockAddresses'; +import { HomeTabScreenProps } from '@app/types'; +import mockNavigation from '../__mocks__/dataMocks/mockNavigation'; + +const props: HomeTabScreenProps<'History'> = { + navigation: mockNavigation, + route: { key: 'Key-1', name: Route.History, params: undefined }, +}; + +describe('History when the wallet has no activity', () => { + beforeEach(() => { + (useNavigation as jest.Mock).mockReturnValue(mockNavigation); + }); + + test('Tests that the sprout and Receive ZEC show when the wallet has no transfers', async () => { + const state = { + ...defaultAppContextLoaded, + valueTransfers: [], + addresses: mockAddresses, + translate: mockTranslate, + info: mockInfo, + totalBalance: mockTotalBalance, + }; + const screen = render( + + + , + ); + await act(async () => {}); + expect(screen.getByTestId('history.empty')).toBeTruthy(); + + fireEvent.press(screen.getByTestId('history.empty.receive')); + + expect(mockNavigation.navigate).toHaveBeenCalledWith(Route.Receive); + }); +}); diff --git a/app/theme/emptyHistoryArt.ts b/app/theme/emptyHistoryArt.ts new file mode 100644 index 0000000000..459cccb895 --- /dev/null +++ b/app/theme/emptyHistoryArt.ts @@ -0,0 +1,20 @@ +/** + * The colors of the empty history illustration (ui/widgets/EmptySprout), + * drawn by design: its gradients and shades are art, and name no role a token + * could. + */ +export const emptyHistoryArt = { + groundTop: '#141D1C', + groundBottom: '#031124', + moundStroke: '#314533', + stemOuter: '#0A4004', + stemDeep: '#0D540C', + stemLight: '#3CC52A', + leafLight: '#3CC52A', + leafMid: '#149D05', + leafDeep: '#0D540C', + vein: '#05234C', + fallenDeep: '#16700B', + fallenMid: '#149D05', + away: '#1B3C6B', +} as const; diff --git a/app/translations/en.json b/app/translations/en.json index 2ff7c6ae7e..6fb5b31a1d 100644 --- a/app/translations/en.json +++ b/app/translations/en.json @@ -1130,6 +1130,9 @@ "filter-withfunds": "With funds", "filters": "Filters", "empty": "Nothing here yet.", + "empty-title": "No activity yet", + "empty-text": "Receive your first ZEC and your history will start to grow.", + "empty-receive": "Receive ZEC", "remove": "Delete", "remove-title": "Delete", "remove-error": "Error Removing :", diff --git a/app/translations/es.json b/app/translations/es.json index 0abe5152f6..418b29bae6 100644 --- a/app/translations/es.json +++ b/app/translations/es.json @@ -1130,6 +1130,9 @@ "filter-withfunds": "Con fondos", "filters": "Filtros", "empty": "Aún no hay nada aquí.", + "empty-title": "Aún no hay movimientos", + "empty-text": "Recibe tus primeros ZEC y tu historial empezará a crecer.", + "empty-receive": "Recibir ZEC", "remove": "Eliminar", "remove-title": "Eliminar", "remove-error": "Error Eliminando :", diff --git a/app/translations/pt.json b/app/translations/pt.json index a3ff1bde78..413342ea3b 100644 --- a/app/translations/pt.json +++ b/app/translations/pt.json @@ -1130,6 +1130,9 @@ "filter-withfunds": "Com fundos", "filters": "Filtros", "empty": "Ainda não há nada aqui.", + "empty-title": "Ainda não há movimentos", + "empty-text": "Receba seus primeiros ZEC e seu histórico vai começar a crescer.", + "empty-receive": "Receber ZEC", "remove": "Excluir", "remove-title": "Excluir", "remove-error": "Error ao remover :", diff --git a/app/translations/ru.json b/app/translations/ru.json index d261bf4521..cf477353a3 100644 --- a/app/translations/ru.json +++ b/app/translations/ru.json @@ -1130,6 +1130,9 @@ "filter-withfunds": "С помощью средств", "filters": "Фильтры", "empty": "Пока здесь ничего нет.", + "empty-title": "Пока нет операций", + "empty-text": "Получите первые ZEC, и ваша история начнёт расти.", + "empty-receive": "Получить ZEC", "remove": "Удалить", "remove-title": "Удалить", "remove-error": "Error удаления :", diff --git a/app/translations/tr.json b/app/translations/tr.json index b76090b60e..feec4276eb 100644 --- a/app/translations/tr.json +++ b/app/translations/tr.json @@ -1130,6 +1130,9 @@ "filter-withfunds": "Varlıklarla birlikte", "filters": "Filtreler", "empty": "Burada henüz bir şey yok.", + "empty-title": "Henüz hareket yok", + "empty-text": "İlk ZEC'ini al, geçmişin büyümeye başlasın.", + "empty-receive": "ZEC al", "remove": "Sil", "remove-title": "Sil", "remove-error": "Error Kaldırma hatası:", diff --git a/screens/History/History.tsx b/screens/History/History.tsx index 26c2b2afaa..f24fda88f6 100644 --- a/screens/History/History.tsx +++ b/screens/History/History.tsx @@ -29,6 +29,7 @@ import { space8, space16, space20, + space24, } from '@app/theme'; import { @@ -44,6 +45,7 @@ import { } from '@app/AppState'; import { HomeTabNavigation, HomeTabScreenProps } from '@app/types'; import FadeText from '@ui/primitives/FadeText'; +import EmptySprout from '@ui/widgets/EmptySprout'; import AppSheet from '@ui/primitives/AppSheet'; import AppSheetModal from '@ui/primitives/AppSheetModal'; import RingBorder from '@ui/primitives/RingBorder'; @@ -88,6 +90,7 @@ import { RPCValueTransfersStatusEnum } from '@app/walletBackend/enums/RPCValueTr import BottomSheet, { BottomSheetModal } from '@gorhom/bottom-sheet'; import Filters, { HISTORY_FILTERS, filterOf } from './components/Filters'; import Text from '@ui/primitives/Text'; +import Button from '@ui/primitives/Button'; import FreshGlow from './components/FreshGlow'; import { useFreshRows } from './useFreshRows'; import { FiltersIcon } from '@ui/primitives/Icons/FiltersIcon'; @@ -1068,6 +1071,44 @@ const History: React.FunctionComponent = () => { {fades} + ) : mergedValueTransfers.length === 0 ? ( + + + + {translate('history.empty-title') as string} + + + {translate('history.empty-text') as string} + +