Skip to content

Commit

Permalink
feat(points): add number ticker for balance (#5345)
Browse files Browse the repository at this point in the history
### 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)
  • Loading branch information
kathaypacific committed Apr 29, 2024
1 parent 2bbc26b commit 3846796
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 3 deletions.
114 changes: 114 additions & 0 deletions src/components/NumberTicker.tsx
Original file line number Diff line number Diff line change
@@ -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<TextStyle>
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 (
<View style={[styles.tickText, { height: textHeight }]}>
<Text style={[styles.text, textStyles]}>{value}</Text>
</View>
)
}

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 (
<Animated.View style={[transformStyle]}>
{numberRange.map((number, index) => {
return (
<TickText key={index} textHeight={textHeight} textStyles={textStyles} value={number} />
)
})}
</Animated.View>
)
}

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 (
<View style={[styles.container, { height: textHeight }]} testID={testID}>
{finalValueArray.map((value, index) => {
// If the character is not a digit, render it as a static text element
if (!value.match(/\d/)) {
return (
<TickText key={index} textHeight={textHeight} textStyles={textStyles} value={value} />
)
}

const endValue = parseInt(value, 10)
const startValue = parseInt(startValueArray[index], 10)
return (
<Tick
key={`${value}-${index}-${startValueArray[index]}`}
startValue={startValue}
endValue={endValue}
textHeight={textHeight}
textStyles={textStyles}
animationDuration={animationDuration}
/>
)
})}
</View>
)
}

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,
},
})
2 changes: 1 addition & 1 deletion src/points/PointsHome.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
10 changes: 8 additions & 2 deletions src/points/PointsHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<BottomSheetRefType>(null)
const activityCardBottomSheetRef = useRef<BottomSheetRefType>(null)
Expand Down Expand Up @@ -121,7 +122,12 @@ export default function PointsHome({ route, navigation }: Props) {
/>
</View>
<View style={styles.balanceRow}>
<Text style={styles.balance}>{pointsBalance}</Text>
<NumberTicker
testID="PointsBalance"
textStyles={styles.balance}
textHeight={48}
finalValue={pointsBalance.toString()}
/>
<LogoHeart size={28} />
</View>

Expand Down

0 comments on commit 3846796

Please sign in to comment.