Skip to content

Commit 549feb0

Browse files
CP-12508 - Balance loading indicator animation (#3328)
1 parent 18dad83 commit 549feb0

File tree

6 files changed

+171
-15
lines changed

6 files changed

+171
-15
lines changed

packages/core-mobile/app/new/features/portfolio/screens/PortfolioScreen.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import { selectSelectedCurrency } from 'store/settings/currency'
6464
import { selectIsPrivacyModeEnabled } from 'store/settings/securityPrivacy'
6565
import { useFocusedSelector } from 'utils/performance/useFocusedSelector'
6666
import { useIsRefetchingBalancesForAccount } from '../hooks/useIsRefetchingBalancesForAccount'
67+
import { useIsLoadingBalancesForAccount } from '../hooks/useIsLoadingBalancesForAccount'
6768

6869
const SEGMENT_ITEMS = [
6970
{ title: 'Assets' },
@@ -114,6 +115,7 @@ const PortfolioHomeScreen = (): JSX.Element => {
114115
const totalPriceChange = useBalanceTotalPriceChangeForAccount(activeAccount)
115116
const tabViewRef = useRef<CollapsibleTabsRef>(null)
116117
const isBalanceLoaded = useIsBalanceLoadedForAccount(activeAccount)
118+
const isLoadingBalances = useIsLoadingBalancesForAccount(activeAccount)
117119
const isLoading = isRefetchingBalance || !isBalanceLoaded
118120
const balanceAccurate = useIsAccountBalanceAccurate(activeAccount)
119121
const selectedCurrency = useSelector(selectSelectedCurrency)
@@ -285,7 +287,8 @@ const PortfolioHomeScreen = (): JSX.Element => {
285287
errorMessage={
286288
balanceAccurate ? undefined : 'Unable to load all balances'
287289
}
288-
isLoading={isLoading}
290+
isLoading={isLoading && balanceTotalInCurrency === 0}
291+
isLoadingBalances={isLoadingBalances || isLoading}
289292
isPrivacyModeEnabled={isPrivacyModeEnabled}
290293
isDeveloperModeEnabled={isDeveloperMode}
291294
renderMaskView={renderMaskView}
@@ -318,6 +321,8 @@ const PortfolioHomeScreen = (): JSX.Element => {
318321
percentChange24h,
319322
balanceAccurate,
320323
isLoading,
324+
balanceTotalInCurrency,
325+
isLoadingBalances,
321326
isPrivacyModeEnabled,
322327
isDeveloperMode,
323328
renderMaskView,

packages/k2-alpine/src/components/AnimatedBalance/AnimatedBalance.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export const AnimatedBalance = ({
6666
<Text variant={variant} sx={balanceSx} numberOfLines={1}>
6767
{balance}
6868
</Text>
69+
6970
{currency && (
7071
<Text variant={variant} sx={currencySx}>
7172
{currency}

packages/k2-alpine/src/components/Header/BalanceHeader.tsx

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import React, { useCallback } from 'react'
22
import { LayoutChangeEvent } from 'react-native'
33
import { Icons } from '../../theme/tokens/Icons'
44
import { colors } from '../../theme/tokens/colors'
5+
import { AnimatedBalance } from '../AnimatedBalance/AnimatedBalance'
6+
import { LoadingContent } from '../LoadingContent/LoadingContent'
57
import { PriceChangeIndicator } from '../PriceChangeIndicator/PriceChangeIndicator'
6-
import { Text, View } from '../Primitives'
78
import { PriceChange } from '../PriceChangeIndicator/types'
8-
import { AnimatedBalance } from '../AnimatedBalance/AnimatedBalance'
9+
import { Text, View } from '../Primitives'
910
import { BalanceLoader } from './BalanceHeaderLoader'
1011
import { PrivacyModeAlert } from './PrivacyModeAlert'
1112

@@ -17,6 +18,7 @@ export const BalanceHeader = ({
1718
priceChange,
1819
onLayout,
1920
isLoading,
21+
isLoadingBalances,
2022
isPrivacyModeEnabled = false,
2123
isDeveloperModeEnabled = false,
2224
renderMaskView,
@@ -29,6 +31,7 @@ export const BalanceHeader = ({
2931
priceChange?: PriceChange
3032
onLayout?: (event: LayoutChangeEvent) => void
3133
isLoading?: boolean
34+
isLoadingBalances?: boolean
3235
isPrivacyModeEnabled?: boolean
3336
isDeveloperModeEnabled?: boolean
3437
testID?: string
@@ -94,18 +97,20 @@ export const BalanceHeader = ({
9497
flexDirection: 'column',
9598
gap: 5
9699
}}>
97-
<AnimatedBalance
98-
balance={formattedBalance}
99-
currency={` ${currency}`}
100-
shouldMask={isPrivacyModeEnabled}
101-
renderMaskView={renderMaskView}
102-
balanceSx={{ lineHeight: 38 }}
103-
currencySx={{
104-
fontFamily: 'Aeonik-Medium',
105-
fontSize: 18,
106-
lineHeight: 28
107-
}}
108-
/>
100+
<LoadingContent isLoading={isLoadingBalances}>
101+
<AnimatedBalance
102+
balance={formattedBalance}
103+
currency={` ${currency}`}
104+
shouldMask={isPrivacyModeEnabled}
105+
renderMaskView={renderMaskView}
106+
balanceSx={{ lineHeight: 38 }}
107+
currencySx={{
108+
fontFamily: 'Aeonik-Medium',
109+
fontSize: 18,
110+
lineHeight: 28
111+
}}
112+
/>
113+
</LoadingContent>
109114

110115
<View
111116
style={{
@@ -119,6 +124,7 @@ export const BalanceHeader = ({
119124
currency,
120125
formattedBalance,
121126
isLoading,
127+
isLoadingBalances,
122128
isPrivacyModeEnabled,
123129
renderMaskView,
124130
renderPriceChangeIndicator
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import React, { useState } from 'react'
2+
import { Button } from '../Button/Button'
3+
import { ScrollView, Text, View } from '../Primitives'
4+
import { LoadingContent } from './LoadingContent'
5+
6+
export default {
7+
title: 'LoadingContent'
8+
}
9+
10+
export const All = (): JSX.Element => {
11+
const [isLoading, setIsLoading] = useState(true)
12+
13+
return (
14+
<ScrollView
15+
sx={{
16+
width: '100%'
17+
}}
18+
contentContainerStyle={{
19+
padding: 16,
20+
paddingHorizontal: 16,
21+
gap: 32
22+
}}>
23+
<View sx={{ gap: 16 }}>
24+
<Text variant="heading3">Loading State</Text>
25+
<LoadingContent isLoading={true}>
26+
<Text
27+
style={{
28+
fontSize: 24,
29+
lineHeight: 24,
30+
fontFamily: 'Aeonik-Medium'
31+
}}>
32+
Loading...
33+
</Text>
34+
</LoadingContent>
35+
</View>
36+
37+
<View sx={{ gap: 16 }}>
38+
<Text variant="heading3">Not Loading State</Text>
39+
<LoadingContent isLoading={false}>
40+
<Text
41+
style={{
42+
fontSize: 24,
43+
lineHeight: 24,
44+
fontFamily: 'Aeonik-Medium'
45+
}}>
46+
Loading...
47+
</Text>
48+
</LoadingContent>
49+
</View>
50+
51+
<View sx={{ gap: 16, alignItems: 'flex-start' }}>
52+
<View
53+
sx={{
54+
width: '100%',
55+
flexDirection: 'row',
56+
alignItems: 'center',
57+
justifyContent: 'space-between'
58+
}}>
59+
<Text variant="heading3">Interactive Example</Text>
60+
<Button
61+
onPress={() => setIsLoading(!isLoading)}
62+
type="primary"
63+
size="small">
64+
{isLoading ? 'Stop' : 'Start'}
65+
</Button>
66+
</View>
67+
68+
<LoadingContent isLoading={isLoading}>
69+
<Text
70+
style={{
71+
fontSize: 24,
72+
lineHeight: 24,
73+
fontFamily: 'Aeonik-Medium'
74+
}}>
75+
Loading...
76+
</Text>
77+
</LoadingContent>
78+
</View>
79+
</ScrollView>
80+
)
81+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { ActivityIndicator, View } from 'dripsy'
2+
import React, { useEffect } from 'react'
3+
import Animated, {
4+
useAnimatedStyle,
5+
useSharedValue,
6+
withRepeat,
7+
withTiming
8+
} from 'react-native-reanimated'
9+
import { useTheme } from '../../hooks'
10+
11+
interface LoadingContentProps {
12+
isLoading?: boolean
13+
children: React.ReactNode
14+
}
15+
16+
export const LoadingContent = ({
17+
isLoading,
18+
children
19+
}: LoadingContentProps): JSX.Element => {
20+
const { theme } = useTheme()
21+
const opacity = useSharedValue(0.3)
22+
23+
useEffect(() => {
24+
if (isLoading) {
25+
opacity.value = withRepeat(withTiming(0.5, { duration: 800 }), -1, true)
26+
} else {
27+
opacity.value = withTiming(1, { duration: 500 })
28+
}
29+
}, [isLoading, opacity])
30+
31+
const animatedStyle = useAnimatedStyle(() => {
32+
return {
33+
opacity: opacity.value,
34+
paddingLeft: withTiming(isLoading ? 28 : 0, { duration: 300 })
35+
}
36+
})
37+
38+
const indicatorStyle = useAnimatedStyle(() => {
39+
return {
40+
width: withTiming(isLoading ? 20 : 0, { duration: 300 }),
41+
opacity: withTiming(isLoading ? 1 : 0, { duration: 300 })
42+
}
43+
})
44+
45+
return (
46+
<View
47+
style={{
48+
gap: 8,
49+
flexDirection: 'row',
50+
alignItems: 'center'
51+
}}>
52+
<Animated.View
53+
style={[
54+
{ position: 'absolute', left: 0, justifyContent: 'center' },
55+
indicatorStyle
56+
]}>
57+
<ActivityIndicator size="small" color={theme.colors.$textPrimary} />
58+
</Animated.View>
59+
<Animated.View style={animatedStyle}>{children}</Animated.View>
60+
</View>
61+
)
62+
}

packages/k2-alpine/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,4 @@ export type {
6060
TokenAmount,
6161
TokenAmountInputRef
6262
} from './TokenAmountInput/TokenAmountInput'
63+
export * from './LoadingContent/LoadingContent'

0 commit comments

Comments
 (0)