From 3846796664a626e8a68d66c9a56a2c7ba2e158e5 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Mon, 29 Apr 2024 16:43:04 +0200 Subject: [PATCH] feat(points): add number ticker for balance (#5345) ### Description As the title. The approach was largely inspired by [a nice man on youtube](https://www.youtube.com/watch?v=nhshusc5ya0&list=PL7ycojg9_ZoJwnayMF8n-VlhdXnE_grdg&index=1). I've added support for decimals in case we want to use this for the wallet balance. The ticker animation will run any time the value changes, so it will work automatically for when the points balance changes and the user remains on the screen. The downside with animations is that there's a lot of smoke and mirrors that mess with the dom. For this ticker, we're basically animating columns of text up and down. From the dom it no longer looks like the actual balance and we lose the ability to assert on its value from the unit tests. ### Test plan Without decimals: https://github.com/valora-inc/wallet/assets/20150449/a9ce407e-544e-4a69-85c1-9219f629360b With decimals: https://github.com/valora-inc/wallet/assets/20150449/4128f00a-0949-4321-98ba-8dda0cd23c89 ### Related issues - Fixes RET-1072 ### Backwards compatibility Y ### Network scalability If a new NetworkId and/or Network are added in the future, the changes in this PR will: - [x] Continue to work without code changes, OR trigger a compilation error (guaranteeing we find it when a new network is added) --- src/components/NumberTicker.tsx | 114 ++++++++++++++++++++++++++++++++ src/points/PointsHome.test.tsx | 2 +- src/points/PointsHome.tsx | 10 ++- 3 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 src/components/NumberTicker.tsx diff --git a/src/components/NumberTicker.tsx b/src/components/NumberTicker.tsx new file mode 100644 index 00000000000..3dfb6cf3509 --- /dev/null +++ b/src/components/NumberTicker.tsx @@ -0,0 +1,114 @@ +import * as React from 'react' +import { Animated, StyleProp, StyleSheet, Text, TextStyle, View } from 'react-native' +import { typeScale } from 'src/styles/fonts' + +interface CommonProps { + textHeight: number + textStyles?: StyleProp + animationDuration?: number +} +interface Props extends CommonProps { + finalValue: string + testID?: string +} + +interface TickProps extends CommonProps { + startValue: number + endValue: number +} + +interface TickTextProps extends CommonProps { + value: string +} + +const numberRange = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] + +function TickText({ value, textHeight, textStyles }: TickTextProps) { + return ( + + {value} + + ) +} + +function Tick({ startValue, endValue, textHeight, textStyles, animationDuration }: TickProps) { + const animatedValue = new Animated.Value(startValue * textHeight * -1) + const transformStyle = { transform: [{ translateY: animatedValue }] } + const duration = animationDuration ?? 1300 + + Animated.timing(animatedValue, { + toValue: endValue * textHeight * -1, + duration, + useNativeDriver: true, + }).start() + + return ( + + {numberRange.map((number, index) => { + return ( + + ) + })} + + ) +} + +export default function NumberTicker({ + finalValue, + textStyles, + textHeight, + animationDuration, + testID, +}: Props) { + const finalValueArray = finalValue.toString().split('') + + // For the startValueArray, map over each character in the finalValueArray to + // replace digits with random digits, do not change non-digit characters (e.g. + // decimal separator) + const startValueArray = finalValueArray.map((char) => { + return char.match(/\d/) ? Math.floor(Math.random() * 10).toString() : char + }) + + return ( + + {finalValueArray.map((value, index) => { + // If the character is not a digit, render it as a static text element + if (!value.match(/\d/)) { + return ( + + ) + } + + const endValue = parseInt(value, 10) + const startValue = parseInt(startValueArray[index], 10) + return ( + + ) + })} + + ) +} + +const styles = StyleSheet.create({ + container: { + overflow: 'hidden', + flexDirection: 'row', + // This negative gap is a hack to bring the numbers closer together, + // otherwise they feel unnatural and far apart + gap: -2, + }, + tickText: { + alignItems: 'center', + justifyContent: 'center', + }, + text: { + ...typeScale.displaySmall, + }, +}) diff --git a/src/points/PointsHome.test.tsx b/src/points/PointsHome.test.tsx index 2179c146719..4631cab4835 100644 --- a/src/points/PointsHome.test.tsx +++ b/src/points/PointsHome.test.tsx @@ -108,7 +108,7 @@ describe(PointsHome, () => { const { getByTestId, getByText, queryByText } = renderPointsHome('success', {}) expect(getByText('points.title')).toBeTruthy() - expect(getByText('50')).toBeTruthy() // balance + expect(getByTestId('PointsBalance')).toBeTruthy() // balance is animated so we cannot properly test the value programatically expect(getByTestId('PointsActivityButton')).toBeTruthy() expect(getByText('points.noActivities.title')).toBeTruthy() expect(getByText('points.noActivities.body')).toBeTruthy() diff --git a/src/points/PointsHome.tsx b/src/points/PointsHome.tsx index 0a7bff1495d..bcc0a6dfa54 100644 --- a/src/points/PointsHome.tsx +++ b/src/points/PointsHome.tsx @@ -11,6 +11,7 @@ import { BottomSheetParams, PointsActivityId } from 'src/points/types' import ValoraAnalytics from 'src/analytics/ValoraAnalytics' import { PointsEvents } from 'src/analytics/Events' import InLineNotification, { NotificationVariant } from 'src/components/InLineNotification' +import NumberTicker from 'src/components/NumberTicker' import CustomHeader from 'src/components/header/CustomHeader' import PointsHistoryBottomSheet from 'src/points/PointsHistoryBottomSheet' import AttentionIcon from 'src/icons/Attention' @@ -36,7 +37,7 @@ export default function PointsHome({ route, navigation }: Props) { const pointsConfigStatus = useSelector(pointsConfigStatusSelector) // TODO: Use real points balance - const pointsBalance = 50 + const pointsBalance = 562 const historyBottomSheetRef = useRef(null) const activityCardBottomSheetRef = useRef(null) @@ -121,7 +122,12 @@ export default function PointsHome({ route, navigation }: Props) { /> - {pointsBalance} +