diff --git a/suite-common/formatters/src/formatters/prepareDateTimeFormatter.ts b/suite-common/formatters/src/formatters/prepareDateTimeFormatter.ts index 78aeb2aa8f54..ba855d508771 100644 --- a/suite-common/formatters/src/formatters/prepareDateTimeFormatter.ts +++ b/suite-common/formatters/src/formatters/prepareDateTimeFormatter.ts @@ -1,5 +1,3 @@ -import { A, pipe } from '@mobily/ts-belt'; - import { makeFormatter } from '../makeFormatter'; import { FormatterConfig } from '../types'; import { prepareDateFormatter } from './prepareDateFormatter'; @@ -11,5 +9,5 @@ export const prepareDateTimeFormatter = (config: FormatterConfig) => const DateFormatter = prepareDateFormatter(config); const TimeFormatter = prepareTimeFormatter(config); - return pipe([TimeFormatter.format(value), DateFormatter.format(value)], A.join(' ')); + return `${DateFormatter.format(value)}, ${TimeFormatter.format(value)}`; }); diff --git a/suite-common/graph/src/graphDataFetching.ts b/suite-common/graph/src/graphDataFetching.ts index a4c7257c562f..89b8f06e9345 100644 --- a/suite-common/graph/src/graphDataFetching.ts +++ b/suite-common/graph/src/graphDataFetching.ts @@ -22,21 +22,24 @@ export const addBalanceForAccountMovementHistory = ( data: AccountMovementHistory[], symbol: NetworkSymbol, ): AccountHistoryBalancePoint[] => { - let balance = '0'; + let balance = new BigNumber('0'); const historyWithBalance = data.map(dataPoint => { // subtract sentToSelf field as we don't want to include amounts received/sent to the same account const normalizedReceived = dataPoint.sentToSelf - ? new BigNumber(dataPoint.received).minus(dataPoint.sentToSelf || 0).toFixed() + ? new BigNumber(dataPoint.received).minus(dataPoint.sentToSelf || 0) : dataPoint.received; const normalizedSent = dataPoint.sentToSelf - ? new BigNumber(dataPoint.sent).minus(dataPoint.sentToSelf || 0).toFixed() + ? new BigNumber(dataPoint.sent).minus(dataPoint.sentToSelf || 0) : dataPoint.sent; - balance = new BigNumber(balance).plus(normalizedReceived).minus(normalizedSent).toFixed(); + balance = new BigNumber(balance).plus(normalizedReceived).minus(normalizedSent); + + // for some coins like ETH, simple sum of received and sent is not enough and could result in nonsense like negative balance + balance = balance.isNegative() ? new BigNumber('0') : balance; return { time: dataPoint.time, - cryptoBalance: formatNetworkAmount(balance, symbol), + cryptoBalance: formatNetworkAmount(balance.toFixed(), symbol), }; }); @@ -61,17 +64,26 @@ export const getAccountBalanceHistory = async ({ return accountBalanceHistoryCache[cacheKey]; } - const accountMovementHistory = await TrezorConnect.blockchainGetAccountBalanceHistory({ - coin, - descriptor, - to: endTimeFrameTimestamp, - // we don't need currencies at all here, this will just reduce transferred data size - // TODO: doesn't work at all, fix it in connect or blockchain-link? - currencies: ['usd'], - }); + const [accountMovementHistory, accountInfo] = await Promise.all([ + TrezorConnect.blockchainGetAccountBalanceHistory({ + coin, + descriptor, + to: endTimeFrameTimestamp, + // we don't need currencies at all here, this will just reduce transferred data size + // TODO: doesn't work at all, fix it in connect or blockchain-link? + currencies: ['usd'], + }), + TrezorConnect.getAccountInfo({ coin, descriptor }), + ]); if (!accountMovementHistory?.success) { - throw new Error(`Get account balance error: ${accountMovementHistory.payload.error}`); + throw new Error( + `Get account balance movement error: ${accountMovementHistory.payload.error}`, + ); + } + + if (!accountInfo?.success) { + throw new Error(`Get account balance info error: ${accountInfo.payload.error}`); } const accountMovementHistoryWithBalance = addBalanceForAccountMovementHistory( @@ -79,6 +91,13 @@ export const getAccountBalanceHistory = async ({ coin, ); + // Last point must be balance from getAccountInfo because blockchainGetAccountBalanceHistory it's not always reliable for coins like ETH. + // TODO: We can get value from redux store instead of fetching it again? + accountMovementHistoryWithBalance.push({ + time: endTimeFrameTimestamp, + cryptoBalance: formatNetworkAmount(accountInfo.payload.balance, coin), + }); + accountBalanceHistoryCache[cacheKey] = accountMovementHistoryWithBalance; return accountMovementHistoryWithBalance; @@ -166,11 +185,12 @@ export const getMultipleAccountBalanceHistoryWithFiat = async ({ ); } - const timestamps = getTimestampsInTimeFrame( - startOfTimeFrameDate, - endOfTimeFrameDate, - numberOfPoints, - ); + // Last timestamp must be endOfTimeFrameDate because blockchainGetAccountBalanceHistory it's not always reliable for coins like ETH. + // So we manually add balance from getAccountInfo for last point in getAccountBalanceHistory. + const timestamps = [ + ...getTimestampsInTimeFrame(startOfTimeFrameDate, endOfTimeFrameDate, numberOfPoints - 1), + getUnixTime(endOfTimeFrameDate), + ]; const coins = pipe( accounts, diff --git a/suite-native/formatters/src/components/FiatAmountFormatter.tsx b/suite-native/formatters/src/components/FiatAmountFormatter.tsx index 99a99a40170c..d964b2a92a9f 100644 --- a/suite-native/formatters/src/components/FiatAmountFormatter.tsx +++ b/suite-native/formatters/src/components/FiatAmountFormatter.tsx @@ -9,7 +9,7 @@ import { FormatterProps } from '../types'; import { EmptyAmountText } from './EmptyAmountText'; import { AmountText } from './AmountText'; -type CryptoToFiatAmountFormatterProps = FormatterProps & +type FiatAmountFormatterProps = FormatterProps & TextProps & { network?: NetworkSymbol; isDiscreetText?: boolean; @@ -20,7 +20,7 @@ export const FiatAmountFormatter = ({ value, isDiscreetText = true, ...textProps -}: CryptoToFiatAmountFormatterProps) => { +}: FiatAmountFormatterProps) => { const { FiatAmountFormatter: formatter } = useFormatters(); const isTestnetValue = !!network && isTestnet(network); diff --git a/suite-native/graph/package.json b/suite-native/graph/package.json index 6b50a1c51c45..d8a42873849a 100644 --- a/suite-native/graph/package.json +++ b/suite-native/graph/package.json @@ -13,14 +13,17 @@ "dependencies": { "@mobily/ts-belt": "^3.13.1", "@shopify/react-native-skia": "0.1.173", + "@suite-common/formatters": "workspace:*", "@suite-common/graph": "workspace:*", "@suite-common/wallet-core": "workspace:*", "@suite-common/wallet-types": "workspace:*", "@suite-native/atoms": "workspace:*", "@suite-native/formatters": "workspace:*", "@suite-native/react-native-graph": "workspace:*", + "@trezor/icons": "workspace:*", "@trezor/styles": "workspace:*", "date-fns": "^2.29.3", + "jotai": "1.9.1", "react": "18.2.0", "react-native": "0.71.3", "react-native-reanimated": "2.14.4", diff --git a/suite-native/graph/src/components/AxisLabel.tsx b/suite-native/graph/src/components/AxisLabel.tsx index d0cf8265e455..7d9f48f3b2d3 100644 --- a/suite-native/graph/src/components/AxisLabel.tsx +++ b/suite-native/graph/src/components/AxisLabel.tsx @@ -23,7 +23,7 @@ export const AxisLabel = ({ x, value }: AxisLabelProps) => { return ( - + ); }; diff --git a/suite-native/graph/src/components/GraphDateFormatter.tsx b/suite-native/graph/src/components/GraphDateFormatter.tsx new file mode 100644 index 000000000000..4620a051f0c5 --- /dev/null +++ b/suite-native/graph/src/components/GraphDateFormatter.tsx @@ -0,0 +1,51 @@ +import React from 'react'; + +import { differenceInDays, isSameDay } from 'date-fns'; +import { Atom, useAtomValue } from 'jotai'; + +import { useFormatters } from '@suite-common/formatters'; + +import { EnhancedGraphPoint } from '../utils'; + +type SelectedPointAtom = Atom; + +type GraphDateFormatterProps = { + firstPointDate: Date; + selectedPointAtom: Atom; +}; + +const SameDayFormatter = ({ selectedPointAtom }: { selectedPointAtom: SelectedPointAtom }) => { + const { TimeFormatter } = useFormatters(); + const point = useAtomValue(selectedPointAtom); + return ; +}; + +const WeekFormatter = ({ selectedPointAtom }: { selectedPointAtom: SelectedPointAtom }) => { + const { DateTimeFormatter, TimeFormatter } = useFormatters(); + const { originalDate: value } = useAtomValue(selectedPointAtom); + if (isSameDay(value, new Date())) { + return ; + } + return ; +}; + +const OtherDateFormatter = ({ selectedPointAtom }: { selectedPointAtom: SelectedPointAtom }) => { + const { DateFormatter } = useFormatters(); + + const { originalDate: value } = useAtomValue(selectedPointAtom); + return ; +}; + +export const GraphDateFormatter = ({ + firstPointDate, + selectedPointAtom, +}: GraphDateFormatterProps) => { + if (isSameDay(firstPointDate, new Date())) { + return ; + } + if (differenceInDays(firstPointDate, new Date()) < 7) { + return ; + } + + return ; +}; diff --git a/suite-native/graph/src/components/PriceChangeIndicator.tsx b/suite-native/graph/src/components/PriceChangeIndicator.tsx new file mode 100644 index 000000000000..0d5686135063 --- /dev/null +++ b/suite-native/graph/src/components/PriceChangeIndicator.tsx @@ -0,0 +1,92 @@ +import React from 'react'; + +import { Atom, useAtom } from 'jotai'; + +import { Box, Text } from '@suite-native/atoms'; +import { Icon, IconName } from '@trezor/icons'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; + +type PriceChangeIndicatorProps = { + percentageChangeAtom: PercentageChangeAtom; + hasPriceIncreasedAtom: HasPriceIncreasedAtom; +}; + +const getColorForPercentageChange = (hasIncreased: boolean) => + hasIncreased ? 'textPrimaryDefault' : 'textAlertRed'; + +type PercentageChangeAtom = Atom; +type HasPriceIncreasedAtom = Atom; + +type PercentageChangeProps = { + percentageChangeAtom: PercentageChangeAtom; + hasPriceIncreasedAtom: HasPriceIncreasedAtom; +}; + +const PercentageChange = ({ + percentageChangeAtom, + hasPriceIncreasedAtom, +}: PercentageChangeProps) => { + const [percentageChange] = useAtom(percentageChangeAtom); + const [hasPriceIncreased] = useAtom(hasPriceIncreasedAtom); + + return ( + + {percentageChange.toFixed(2)}% + + ); +}; + +const PercentageChangeArrow = ({ + hasPriceIncreasedAtom, +}: { + hasPriceIncreasedAtom: HasPriceIncreasedAtom; +}) => { + const [hasPriceIncreased] = useAtom(hasPriceIncreasedAtom); + + const iconName: IconName = hasPriceIncreased ? 'arrowUp' : 'arrowDown'; + + return ( + + ); +}; + +const arrowStyle = prepareNativeStyle(() => ({ + marginRight: 4, +})); + +const priceIncreaseWrapperStyle = prepareNativeStyle<{ hasPriceIncreased: boolean }>( + (utils, { hasPriceIncreased }) => ({ + backgroundColor: hasPriceIncreased + ? utils.colors.backgroundPrimarySubtleOnElevation0 + : utils.colors.backgroundAlertRedSubtleOnElevation0, + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: utils.spacings.small, + paddingVertical: utils.spacings.small / 4, + borderRadius: utils.borders.radii.round, + }), +); + +export const PriceChangeIndicator = ({ + hasPriceIncreasedAtom, + percentageChangeAtom, +}: PriceChangeIndicatorProps) => { + const { applyStyle } = useNativeStyles(); + const [hasPriceIncreased] = useAtom(hasPriceIncreasedAtom); + + return ( + + + + + + + ); +}; diff --git a/suite-native/graph/src/index.ts b/suite-native/graph/src/index.ts index eb9785bd6628..e9378dff1714 100644 --- a/suite-native/graph/src/index.ts +++ b/suite-native/graph/src/index.ts @@ -2,3 +2,5 @@ export * from './components/Graph'; export * from './components/TimeSwitch'; export * from './utils'; export * from './hooks'; +export * from './components/GraphDateFormatter'; +export * from './components/PriceChangeIndicator'; diff --git a/suite-native/graph/src/utils.ts b/suite-native/graph/src/utils.ts index 805cf1903220..c1d3ecf5deeb 100644 --- a/suite-native/graph/src/utils.ts +++ b/suite-native/graph/src/utils.ts @@ -74,3 +74,8 @@ export const getExtremaFromGraphPoints = (points: EnhancedGraphPoint[]) => { }; } }; + +export const percentageDiff = (a: number, b: number) => { + if (a === 0 || b === 0) return 0; + return 100 * ((b - a) / ((b + a) / 2)); +}; diff --git a/suite-native/graph/tsconfig.json b/suite-native/graph/tsconfig.json index 50fc91acc269..0b41067795ec 100644 --- a/suite-native/graph/tsconfig.json +++ b/suite-native/graph/tsconfig.json @@ -2,6 +2,9 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "libDev" }, "references": [ + { + "path": "../../suite-common/formatters" + }, { "path": "../../suite-common/graph" }, { "path": "../../suite-common/wallet-core" @@ -12,6 +15,7 @@ { "path": "../atoms" }, { "path": "../formatters" }, { "path": "../react-native-graph" }, + { "path": "../../packages/icons" }, { "path": "../../packages/styles" } ] } diff --git a/suite-native/module-accounts/src/components/AccountBalance.tsx b/suite-native/module-accounts/src/components/AccountBalance.tsx deleted file mode 100644 index 8e16edd24c18..000000000000 --- a/suite-native/module-accounts/src/components/AccountBalance.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; - -import { atom, useAtom } from 'jotai'; - -import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; -import { Box, Divider } from '@suite-native/atoms'; -import { CryptoIcon } from '@trezor/icons'; -import { AccountsRootState, selectAccountByKey } from '@suite-common/wallet-core'; -import { emptyGraphPoint, EnhancedGraphPointWithCryptoBalance } from '@suite-native/graph'; -import { CryptoAmountFormatter, FiatAmountFormatter } from '@suite-native/formatters'; - -type AccountBalanceProps = { - accountKey: string; -}; - -const cryptoIconStyle = prepareNativeStyle(utils => ({ - marginRight: utils.spacings.small / 2, -})); - -const selectedPointAtom = atom(emptyGraphPoint); - -// reference is usually first point, same as Revolut does in their app -const referencePointAtom = atom(emptyGraphPoint); - -export const writeOnlySelectedPointAtom = atom( - null, // it's a convention to pass `null` for the first argument - (_get, set, updatedPoint) => { - set(selectedPointAtom, updatedPoint); - }, -); -export const writeOnlyReferencePointAtom = atom( - null, - (_get, set, updatedPoint) => { - set(referencePointAtom, updatedPoint); - }, -); - -export const AccountBalance = ({ accountKey }: AccountBalanceProps) => { - const { applyStyle } = useNativeStyles(); - const account = useSelector((state: AccountsRootState) => - selectAccountByKey(state, accountKey), - ); - const [selectedPoint] = useAtom(selectedPointAtom); - - if (!account) return null; - - return ( - - - - - - - - - - - - - - - - - ); -}; diff --git a/suite-native/module-accounts/src/components/AccountDetailGraph.tsx b/suite-native/module-accounts/src/components/AccountDetailGraph.tsx index 926649f4531a..68419fffc630 100644 --- a/suite-native/module-accounts/src/components/AccountDetailGraph.tsx +++ b/suite-native/module-accounts/src/components/AccountDetailGraph.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { A } from '@mobily/ts-belt'; -import { useAtom } from 'jotai'; +import { useSetAtom } from 'jotai'; import { useGraphForSingleAccount, @@ -14,7 +14,7 @@ import { import { Box, Divider } from '@suite-native/atoms'; import { selectFiatCurrency } from '@suite-native/module-settings'; -import { writeOnlyReferencePointAtom, writeOnlySelectedPointAtom } from './AccountBalance'; +import { referencePointAtom, selectedPointAtom } from './AccountDetailGraphHeader'; type AccountDetailGraphProps = { accountKey: string; @@ -31,8 +31,8 @@ export const AccountDetailGraph = ({ accountKey }: AccountDetailGraphProps) => { () => enhanceGraphPoints(graphPoints) as EnhancedGraphPointWithCryptoBalance[], [graphPoints], ); - const [_, setSelectedPoint] = useAtom(writeOnlySelectedPointAtom); - const [__, setReferencePoint] = useAtom(writeOnlyReferencePointAtom); + const setSelectedPoint = useSetAtom(selectedPointAtom); + const setReferencePoint = useSetAtom(referencePointAtom); const lastPoint = A.last(enhancedPoints); const firstPoint = A.head(enhancedPoints); diff --git a/suite-native/module-accounts/src/components/AccountDetailGraphHeader.tsx b/suite-native/module-accounts/src/components/AccountDetailGraphHeader.tsx new file mode 100644 index 000000000000..fdcad89b3b3e --- /dev/null +++ b/suite-native/module-accounts/src/components/AccountDetailGraphHeader.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { atom, useAtomValue } from 'jotai'; + +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; +import { Box, Divider, Text } from '@suite-native/atoms'; +import { CryptoIcon } from '@trezor/icons'; +import { AccountsRootState, selectAccountByKey } from '@suite-common/wallet-core'; +import { + emptyGraphPoint, + EnhancedGraphPointWithCryptoBalance, + GraphDateFormatter, + percentageDiff, + PriceChangeIndicator, +} from '@suite-native/graph'; +import { CryptoAmountFormatter, FiatAmountFormatter } from '@suite-native/formatters'; +import { NetworkSymbol } from '@suite-common/wallet-config'; + +type AccountBalanceProps = { + accountKey: string; +}; + +const cryptoIconStyle = prepareNativeStyle(utils => ({ + marginRight: utils.spacings.small / 2, +})); + +export const selectedPointAtom = atom(emptyGraphPoint); + +// reference is usually first point, same as Revolut does in their app +export const referencePointAtom = atom(emptyGraphPoint); + +const percentageChangeAtom = atom(get => { + const selectedPoint = get(selectedPointAtom); + const referencePoint = get(referencePointAtom); + return percentageDiff(referencePoint.value, selectedPoint.value); +}); + +const hasPriceIncreasedAtom = atom(get => { + const percentageChange = get(percentageChangeAtom); + return percentageChange >= 0; +}); + +const CryptoBalance = ({ accountSymbol }: { accountSymbol: NetworkSymbol }) => { + const selectedPoint = useAtomValue(selectedPointAtom); + + return ; +}; + +const FiatBalance = ({ accountSymbol }: { accountSymbol: NetworkSymbol }) => { + const selectedPoint = useAtomValue(selectedPointAtom); + + return ( + + ); +}; + +export const AccountDetailGraphHeader = ({ accountKey }: AccountBalanceProps) => { + const { applyStyle } = useNativeStyles(); + const account = useSelector((state: AccountsRootState) => + selectAccountByKey(state, accountKey), + ); + const { originalDate: firstPointDate } = useAtomValue(referencePointAtom); + + if (!account) return null; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/suite-native/module-accounts/src/components/TransactionListHeader.tsx b/suite-native/module-accounts/src/components/TransactionListHeader.tsx index 5daca89be448..324c06f4c77a 100644 --- a/suite-native/module-accounts/src/components/TransactionListHeader.tsx +++ b/suite-native/module-accounts/src/components/TransactionListHeader.tsx @@ -17,7 +17,7 @@ import { } from '@suite-native/navigation'; import { AccountDetailGraph } from './AccountDetailGraph'; -import { AccountBalance } from './AccountBalance'; +import { AccountDetailGraphHeader } from './AccountDetailGraphHeader'; type AccountDetailHeaderProps = { accountKey: AccountKey; @@ -38,7 +38,7 @@ export const TransactionListHeader = memo(({ accountKey }: AccountDetailHeaderPr }; return ( <> - + {accountHasTransactions && ( <> {!isTestnetAccount && } diff --git a/suite-native/module-home/src/components/PortfolioGraph.tsx b/suite-native/module-home/src/components/PortfolioGraph.tsx index ef2ea18acb27..7d80447ecc52 100644 --- a/suite-native/module-home/src/components/PortfolioGraph.tsx +++ b/suite-native/module-home/src/components/PortfolioGraph.tsx @@ -1,15 +1,15 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; -import { useAtom } from 'jotai'; +import { useSetAtom } from 'jotai'; import { useGraphForAllAccounts, enhanceGraphPoints, Graph, TimeSwitch } from '@suite-native/graph'; import { selectFiatCurrency } from '@suite-native/module-settings'; import { PortfolioGraphHeader, - writeOnlyReferencePointAtom, - writeOnlySelectedPointAtom, + referencePointAtom, + selectedPointAtom, } from './PortfolioGraphHeader'; export const PortfolioGraph = () => { @@ -19,8 +19,8 @@ export const PortfolioGraph = () => { fiatCurrency: fiatCurrency.label, }); const enhancedPoints = useMemo(() => enhanceGraphPoints(graphPoints), [graphPoints]); - const [_, setSelectedPoint] = useAtom(writeOnlySelectedPointAtom); - const [__, setReferencePoint] = useAtom(writeOnlyReferencePointAtom); + const setSelectedPoint = useSetAtom(selectedPointAtom); + const setReferencePoint = useSetAtom(referencePointAtom); const lastPoint = enhancedPoints[enhancedPoints.length - 1]; const firstPoint = enhancedPoints[0]; diff --git a/suite-native/module-home/src/components/PortfolioGraphHeader.tsx b/suite-native/module-home/src/components/PortfolioGraphHeader.tsx index abc3118c9c27..c995cb605399 100644 --- a/suite-native/module-home/src/components/PortfolioGraphHeader.tsx +++ b/suite-native/module-home/src/components/PortfolioGraphHeader.tsx @@ -1,42 +1,24 @@ import React from 'react'; -import { atom, useAtom } from 'jotai'; +import { atom, useAtomValue } from 'jotai'; -import { useFormatters } from '@suite-common/formatters'; -import { FiatAmountFormatter } from '@suite-native/formatters'; import { Box, Text } from '@suite-native/atoms'; +import { FiatAmountFormatter } from '@suite-native/formatters'; +import { + emptyGraphPoint, + EnhancedGraphPoint, + GraphDateFormatter, + percentageDiff, + PriceChangeIndicator, +} from '@suite-native/graph'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; -import { Icon, IconName } from '@trezor/icons'; -import { emptyGraphPoint, EnhancedGraphPoint } from '@suite-native/graph'; - -const getColorForPercentageChange = (hasIncreased: boolean) => - hasIncreased ? 'textPrimaryDefault' : 'textAlertRed'; - -const percentageDiff = (a: number, b: number) => { - if (a === 0 || b === 0) return 0; - return 100 * ((b - a) / ((b + a) / 2)); -}; // use atomic jotai structure for absolute minimum re-renders and maximum performance // otherwise graph will be freezing on slower device while point swipe gesture -const selectedPointAtom = atom(emptyGraphPoint); +export const selectedPointAtom = atom(emptyGraphPoint); // reference is usually first point, same as Revolut does in their app -const referencePointAtom = atom(emptyGraphPoint); - -export const writeOnlySelectedPointAtom = atom( - null, // it's a convention to pass `null` for the first argument - (_get, set, updatedPoint) => { - // LineGraphPoint should never happen, but we need it to satisfy typescript because of originalDate - set(selectedPointAtom, updatedPoint); - }, -); -export const writeOnlyReferencePointAtom = atom( - null, - (_get, set, updatedPoint) => { - set(referencePointAtom, updatedPoint); - }, -); +export const referencePointAtom = atom(emptyGraphPoint); const percentageChangeAtom = atom(get => { const selectedPoint = get(selectedPointAtom); @@ -54,77 +36,17 @@ const headerStyle = prepareNativeStyle(utils => ({ color: utils.colors.textSubdued, })); -const PercentageChange = () => { - const [percentageChange] = useAtom(percentageChangeAtom); - const [hasPriceIncreased] = useAtom(hasPriceIncreasedAtom); - - return ( - - {percentageChange.toFixed(2)}% - - ); -}; - -const PercentageChangeArrow = () => { - const [hasPriceIncreased] = useAtom(hasPriceIncreasedAtom); - - const iconName: IconName = hasPriceIncreased ? 'arrowUp' : 'arrowDown'; - - return ( - - ); -}; - -const arrowStyle = prepareNativeStyle(() => ({ - marginRight: 4, -})); - -const priceIncreaseWrapperStyle = prepareNativeStyle<{ hasPriceIncreased: boolean }>( - (utils, { hasPriceIncreased }) => ({ - backgroundColor: hasPriceIncreased - ? utils.colors.backgroundPrimarySubtleOnElevation0 - : utils.colors.backgroundAlertRedSubtleOnElevation0, - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: utils.spacings.small, - paddingVertical: utils.spacings.small / 4, - borderRadius: utils.borders.radii.round, - }), -); - -const PriceIncreaseIndicator = () => { - const { applyStyle } = useNativeStyles(); - const [hasPriceIncreased] = useAtom(hasPriceIncreasedAtom); +const Balance = () => { + const point = useAtomValue(selectedPointAtom); return ( - - - - - - + ); }; -const Balance = () => { - const [point] = useAtom(selectedPointAtom); - - return ; -}; - -export const GraphTimeIndicator = () => { - const [point] = useAtom(selectedPointAtom); - const { DateFormatter } = useFormatters(); - - return ; -}; - export const PortfolioGraphHeader = () => { const { applyStyle } = useNativeStyles(); + const { originalDate: firstPointDate } = useAtomValue(referencePointAtom); return ( @@ -138,10 +60,16 @@ export const PortfolioGraphHeader = () => { - + - + diff --git a/suite-native/react-native-graph/src/CreateGraphPath.ts b/suite-native/react-native-graph/src/CreateGraphPath.ts index 21414979022e..15a1d7735f57 100644 --- a/suite-native/react-native-graph/src/CreateGraphPath.ts +++ b/suite-native/react-native-graph/src/CreateGraphPath.ts @@ -87,6 +87,7 @@ export const getXInRange = (width: number, date: Date, xRange: GraphXRange): num Math.floor(width * getXPositionInRange(date, xRange)); export const getYPositionInRange = (value: number, yRange: GraphYRange): number => { + if (yRange.min === yRange.max) return 0.5; // Prevent division by zero (NaN) const diff = yRange.max - yRange.min; const y = value; diff --git a/yarn.lock b/yarn.lock index e644bb2012f6..219b2957d59d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6700,15 +6700,18 @@ __metadata: dependencies: "@mobily/ts-belt": ^3.13.1 "@shopify/react-native-skia": 0.1.173 + "@suite-common/formatters": "workspace:*" "@suite-common/graph": "workspace:*" "@suite-common/wallet-core": "workspace:*" "@suite-common/wallet-types": "workspace:*" "@suite-native/atoms": "workspace:*" "@suite-native/formatters": "workspace:*" "@suite-native/react-native-graph": "workspace:*" + "@trezor/icons": "workspace:*" "@trezor/styles": "workspace:*" date-fns: ^2.29.3 jest: ^26.6.3 + jotai: 1.9.1 react: 18.2.0 react-native: 0.71.3 react-native-reanimated: 2.14.4