Skip to content

Commit

Permalink
feat(wallet-tab): Add Hide Balance ability to wallet tab (#5074)
Browse files Browse the repository at this point in the history
### Description

Generalizes the `hideHomeBalances` state into `hideBalances` and updates
the `hideHomeBalancesSelector` and `hideWalletBalancesSelector` to use
the statsig `USE_TAB_NAVIGATOR` feature flag.


### Video

In the video you can see me switch between the current experience and
the new tab experience showing how the changes affect both home and
wallet/assets.
 


https://github.com/valora-inc/wallet/assets/8432644/287fca7b-f2e9-4fac-ade0-12f07067ade1



### Test plan

updated unit tests

### Related issues

https://linear.app/valora/issue/ACT-1109/hide-token-balance

---------

Co-authored-by: Satish Ravi <satish.ravi@valoraapp.com>
  • Loading branch information
jh2oman and satish-ravi authored Mar 18, 2024
1 parent 08b5527 commit b2b5fbb
Show file tree
Hide file tree
Showing 22 changed files with 185 additions and 134 deletions.
12 changes: 6 additions & 6 deletions src/app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export enum Actions {
PUSH_NOTIFICATIONS_PERMISSION_CHANGED = 'APP/PUSH_NOTIFICATIONS_PERMISSION_CHANGED',
IN_APP_REVIEW_REQUESTED = 'APP/IN_APP_REVIEW_REQUESTED',
NOTIFICATION_SPOTLIGHT_SEEN = 'APP/NOTIFICATION_SPOTLIGHT_SEEN',
TOGGLE_HIDE_HOME_BALANCES = 'APP/TOGGLE_HIDE_HOME_BALANCES',
TOGGLE_HIDE_BALANCES = 'APP/TOGGLE_HIDE_BALANCES',
OPT_MULTICHAIN_BETA = 'APP/OPT_MULTICHAIN_BETA',
}

Expand Down Expand Up @@ -184,8 +184,8 @@ export interface NotificationSpotlightSeen {
type: Actions.NOTIFICATION_SPOTLIGHT_SEEN
}

interface ToggleHideHomeBalances {
type: Actions.TOGGLE_HIDE_HOME_BALANCES
interface ToggleHideBalances {
type: Actions.TOGGLE_HIDE_BALANCES
}

interface OptMultichainBeta {
Expand Down Expand Up @@ -220,7 +220,7 @@ export type ActionTypes =
| PushNotificationsPermissionChanged
| inAppReviewRequested
| NotificationSpotlightSeen
| ToggleHideHomeBalances
| ToggleHideBalances
| OptMultichainBeta
| DeepLinkDeferred

Expand Down Expand Up @@ -398,9 +398,9 @@ export const notificationSpotlightSeen = (): NotificationSpotlightSeen => {
}
}

export const toggleHideHomeBalances = (): ToggleHideHomeBalances => {
export const toggleHideBalances = (): ToggleHideBalances => {
return {
type: Actions.TOGGLE_HIDE_HOME_BALANCES,
type: Actions.TOGGLE_HIDE_BALANCES,
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/app/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ interface State {
pushNotificationsEnabled: boolean
inAppReviewLastInteractionTimestamp: number | null
showNotificationSpotlight: boolean
hideHomeBalances: boolean
hideBalances: boolean
multichainBetaStatus: MultichainBetaStatus
pendingDeepLinks: PendingDeepLink[]
}
Expand Down Expand Up @@ -97,7 +97,7 @@ const initialState = {
pushNotificationsEnabled: false,
inAppReviewLastInteractionTimestamp: null,
showNotificationSpotlight: false,
hideHomeBalances: false,
hideBalances: false,
multichainBetaStatus: MultichainBetaStatus.NotSeen,
pendingDeepLinks: [],
}
Expand Down Expand Up @@ -268,10 +268,10 @@ export const appReducer = (
...state,
inAppReviewLastInteractionTimestamp: action.inAppReviewLastInteractionTimestamp,
}
case Actions.TOGGLE_HIDE_HOME_BALANCES:
case Actions.TOGGLE_HIDE_BALANCES:
return {
...state,
hideHomeBalances: !state.hideHomeBalances,
hideBalances: !state.hideBalances,
}
case Actions.OPT_MULTICHAIN_BETA:
return {
Expand Down
8 changes: 7 additions & 1 deletion src/app/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { createSelector } from 'reselect'
import { RootState } from 'src/redux/reducers'
import { getFeatureGate } from 'src/statsig'
import { StatsigFeatureGates } from 'src/statsig/types'
import { walletAddressSelector } from 'src/web3/selectors'

export const getRequirePinOnAppOpen = (state: RootState) => {
Expand Down Expand Up @@ -110,7 +112,11 @@ export const inAppReviewLastInteractionTimestampSelector = (state: RootState) =>
export const showNotificationSpotlightSelector = (state: RootState) =>
state.app.showNotificationSpotlight

export const hideHomeBalancesSelector = (state: RootState) => state.app.hideHomeBalances
export const hideHomeBalancesSelector = (state: RootState) =>
!getFeatureGate(StatsigFeatureGates.USE_TAB_NAVIGATOR) && state.app.hideBalances

export const hideWalletBalancesSelector = (state: RootState) =>
getFeatureGate(StatsigFeatureGates.USE_TAB_NAVIGATOR) && state.app.hideBalances

export const multichainBetaStatusSelector = (state: RootState) => state.app.multichainBetaStatus

Expand Down
44 changes: 4 additions & 40 deletions src/components/TokenBalance.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('AssetsTokenBalance', () => {
})

it('should show info on tap', () => {
const { getByText, getByTestId, queryByText } = render(
const { getByText, getByTestId, queryByText, queryByTestId } = render(
<Provider store={createMockStore()}>
<AssetsTokenBalance showInfo isWalletTab={false} />
</Provider>
Expand All @@ -71,6 +71,7 @@ describe('AssetsTokenBalance', () => {
expect(getByText('totalAssets')).toBeTruthy()
expect(getByTestId('TotalTokenBalance')).toHaveTextContent('₱55.74')
expect(queryByText('totalAssetsInfo')).toBeFalsy()
expect(queryByTestId('EyeIcon')).toBeFalsy()

fireEvent.press(getByTestId('AssetsTokenBalance/Info'))
expect(getByText('totalAssetsInfo')).toBeTruthy()
Expand All @@ -83,6 +84,7 @@ describe('AssetsTokenBalance', () => {
</Provider>
)

expect(getByTestId('EyeIcon')).toBeTruthy()
expect(getByText('bottomTabsNavigator.wallet.title')).toBeTruthy()
expect(getByTestId('TotalTokenBalance')).toHaveTextContent('₱55.74')
expect(queryByText('AssetsTokenBalance/Info')).toBeFalsy()
Expand Down Expand Up @@ -419,7 +421,7 @@ describe('HomeTokenBalance', () => {
})

it('renders correctly when hideBalance is true', async () => {
const store = createMockStore({ ...defaultStore, app: { hideHomeBalances: true } })
const store = createMockStore({ ...defaultStore, app: { hideBalances: true } })

const tree = render(
<Provider store={store}>
Expand All @@ -444,44 +446,6 @@ describe('HomeTokenBalance', () => {
expect(tree.getByTestId('EyeIcon')).toBeTruthy()
})

it('renders correctly when feature flag is off, hideBalance is false', async () => {
jest
.mocked(getFeatureGate)
.mockImplementation(
(featureGate) => featureGate !== StatsigFeatureGates.SHOW_HIDE_HOME_BALANCES_TOGGLE
)
const store = createMockStore(defaultStore)

const tree = render(
<Provider store={store}>
<HomeTokenBalance />
</Provider>
)

expect(getElementText(tree.getByTestId('TotalTokenBalance'))).toEqual('$8.41')
expect(tree.queryByTestId('EyeIcon')).toBeFalsy()
expect(tree.queryByTestId('HiddenEyeIcon')).toBeFalsy()
})

it('renders correctly when feature flag is off, hideBalance is true', async () => {
jest
.mocked(getFeatureGate)
.mockImplementation(
(featureGate) => featureGate !== StatsigFeatureGates.SHOW_HIDE_HOME_BALANCES_TOGGLE
)
const store = createMockStore({ ...defaultStore, app: { hideHomeBalances: true } })

const tree = render(
<Provider store={store}>
<HomeTokenBalance />
</Provider>
)

expect(getElementText(tree.getByTestId('TotalTokenBalance'))).toEqual('$8.41')
expect(tree.queryByTestId('EyeIcon')).toBeFalsy()
expect(tree.queryByTestId('HiddenEyeIcon')).toBeFalsy()
})

it('tracks analytics event when eye icon is pressed', async () => {
const store = createMockStore(defaultStore)

Expand Down
28 changes: 14 additions & 14 deletions src/components/TokenBalance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanima
import { hideAlert, showToast } from 'src/alert/actions'
import { AssetsEvents, FiatExchangeEvents, HomeEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import { toggleHideHomeBalances } from 'src/app/actions'
import { hideHomeBalancesSelector } from 'src/app/selectors'
import { toggleHideBalances } from 'src/app/actions'
import { hideHomeBalancesSelector, hideWalletBalancesSelector } from 'src/app/selectors'
import Dialog from 'src/components/Dialog'
import { formatValueToDisplay } from 'src/components/TokenDisplay'
import Touchable from 'src/components/Touchable'
Expand Down Expand Up @@ -55,11 +55,11 @@ import { getSupportedNetworkIdsForTokenBalances } from 'src/tokens/utils'
function TokenBalance({
style = styles.balance,
singleTokenViewEnabled = true,
showHideHomeBalancesToggle = false,
showBalanceToggle = false,
}: {
style?: StyleProp<TextStyle>
singleTokenViewEnabled?: boolean
showHideHomeBalancesToggle?: boolean
showBalanceToggle?: boolean
}) {
const supportedNetworkIds = getSupportedNetworkIdsForTokenBalances()
const tokensWithUsdValue = useTokensWithUsdValue(supportedNetworkIds)
Expand All @@ -78,7 +78,8 @@ function TokenBalance({
const { decimalSeparator } = getNumberFormatSettings()

const hideHomeBalanceState = useSelector(hideHomeBalancesSelector)
const hideBalance = showHideHomeBalancesToggle && hideHomeBalanceState
const hideWalletBalance = useSelector(hideWalletBalancesSelector)
const hideBalance = showBalanceToggle && (hideHomeBalanceState || hideWalletBalance)
const balanceDisplay = hideBalance ? `XX${decimalSeparator}XX` : totalBalanceLocal?.toFormat(2)

const TotalTokenBalance = ({ balanceDisplay }: { balanceDisplay: string }) => {
Expand All @@ -88,7 +89,7 @@ function TokenBalance({
{!hideBalance && localCurrencySymbol}
{balanceDisplay}
</Text>
{showHideHomeBalancesToggle && <HideBalanceButton hideBalance={hideBalance} />}
{showBalanceToggle && <HideBalanceButton hideBalance={hideBalance} />}
</View>
)
}
Expand Down Expand Up @@ -124,7 +125,7 @@ function HideBalanceButton({ hideBalance }: { hideBalance: boolean }) {
const dispatch = useDispatch()
const eyeIconOnPress = () => {
ValoraAnalytics.track(hideBalance ? HomeEvents.show_balances : HomeEvents.hide_balances)
dispatch(toggleHideHomeBalances())
dispatch(toggleHideBalances())
}
return (
<Touchable onPress={eyeIconOnPress} hitSlop={variables.iconHitslop}>
Expand Down Expand Up @@ -223,7 +224,11 @@ export function AssetsTokenBalance({
</TouchableOpacity>
)}
</View>
<TokenBalance style={styles.totalBalance} singleTokenViewEnabled={false} />
<TokenBalance
style={styles.totalBalance}
singleTokenViewEnabled={false}
showBalanceToggle={isWalletTab}
/>

{shouldRenderInfoComponent && (
<Animated.View style={[styles.totalAssetsInfoContainer, animatedStyles]}>
Expand Down Expand Up @@ -282,12 +287,7 @@ export function HomeTokenBalance() {
<ProgressArrow style={styles.arrow} color={Colors.primary} />
</TouchableOpacity>
</View>
<TokenBalance
style={styles.totalBalance}
showHideHomeBalancesToggle={getFeatureGate(
StatsigFeatureGates.SHOW_HIDE_HOME_BALANCES_TOGGLE
)}
/>
<TokenBalance style={styles.totalBalance} showBalanceToggle={true} />
</View>
)
}
Expand Down
15 changes: 15 additions & 0 deletions src/redux/migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
v18Schema,
v197Schema,
v1Schema,
v200Schema,
v21Schema,
v28Schema,
v2Schema,
Expand Down Expand Up @@ -1550,4 +1551,18 @@ describe('Redux persist migrations', () => {
expectedSchema.home.nftCelebration = null
expect(migratedSchema).toStrictEqual(expectedSchema)
})
it('works from 200 to 201', () => {
const oldSchema = {
...v200Schema,
app: {
...v200Schema.app,
hideHomeBalances: true,
},
}
const migratedSchema = migrations[201](oldSchema)
const expectedSchema: any = _.cloneDeep(oldSchema)
expectedSchema.app.hideBalances = true
delete expectedSchema.app.hideHomeBalances
expect(migratedSchema).toStrictEqual(expectedSchema)
})
})
7 changes: 7 additions & 0 deletions src/redux/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1623,4 +1623,11 @@ export const migrations = {
depositStatus: 'idle',
},
}),
201: (state: any) => ({
...state,
app: {
..._.omit(state.app, 'hideHomeBalances'),
hideBalances: state.app.hideHomeBalances,
},
}),
}
4 changes: 2 additions & 2 deletions src/redux/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('store state', () => {
{
"_persist": {
"rehydrated": true,
"version": 200,
"version": 201,
},
"account": {
"acceptedTerms": false,
Expand Down Expand Up @@ -142,7 +142,7 @@ describe('store state', () => {
"fiatConnectCashOutEnabled": false,
"googleMobileServicesAvailable": undefined,
"hapticFeedbackEnabled": true,
"hideHomeBalances": false,
"hideBalances": false,
"huaweiMobileServicesAvailable": undefined,
"inAppReviewLastInteractionTimestamp": null,
"inviterAddress": null,
Expand Down
2 changes: 1 addition & 1 deletion src/redux/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const persistConfig: PersistConfig<ReducersRootState> = {
key: 'root',
// default is -1, increment as we make migrations
// See https://github.com/valora-inc/wallet/tree/main/WALLET.md#redux-state-migration
version: 200,
version: 201,
keyPrefix: `reduxStore-`, // the redux-persist default is `persist:` which doesn't work with some file systems.
storage: FSStorage(),
blacklist: ['networkInfo', 'alert', 'imports', 'keylessBackup', 'jumpstart'],
Expand Down
1 change: 0 additions & 1 deletion src/statsig/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export const FeatureGates = {
[StatsigFeatureGates.SHOW_CLOUD_ACCOUNT_BACKUP_RESTORE]: false,
[StatsigFeatureGates.RESTRICT_SUPERCHARGE_FOR_CLAIM_ONLY]: false,
[StatsigFeatureGates.SHOW_IMPORT_TOKENS_FLOW]: false,
[StatsigFeatureGates.SHOW_HIDE_HOME_BALANCES_TOGGLE]: false,
[StatsigFeatureGates.SHOW_MULTICHAIN_BETA_SCREEN]: false,
[StatsigFeatureGates.SHOW_BETA_TAG]: false,
[StatsigFeatureGates.SAVE_CONTACTS]: false,
Expand Down
1 change: 0 additions & 1 deletion src/statsig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export enum StatsigFeatureGates {
SHOW_CLOUD_ACCOUNT_BACKUP_RESTORE = 'show_cloud_account_backup_restore',
RESTRICT_SUPERCHARGE_FOR_CLAIM_ONLY = 'restrict_supercharge_for_claim_only',
SHOW_IMPORT_TOKENS_FLOW = 'show_import_tokens_flow',
SHOW_HIDE_HOME_BALANCES_TOGGLE = 'show_hide_home_balances_toggle',
SHOW_MULTICHAIN_BETA_SCREEN = 'show_multichain_beta_screen',
SHOW_BETA_TAG = 'show_beta_tag',
SAVE_CONTACTS = 'save_contacts',
Expand Down
6 changes: 5 additions & 1 deletion src/tokens/AssetList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Animated from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { AssetsEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import { hideWalletBalancesSelector } from 'src/app/selectors'
import Touchable from 'src/components/Touchable'
import CircledIcon from 'src/icons/CircledIcon'
import ImageErrorIcon from 'src/icons/ImageErrorIcon'
Expand Down Expand Up @@ -100,6 +101,8 @@ export default function AssetList({
sortedTokensWithBalanceOrShowZeroBalanceSelector(state, supportedNetworkIds)
)

const hideWalletBalances = useSelector(hideWalletBalancesSelector) && isWalletTab

const positions = useSelector(positionsSelector)
const positionSections = useMemo(() => {
const positionsByDapp = new Map<string, Position[]>()
Expand Down Expand Up @@ -224,7 +227,7 @@ export default function AssetList({
index: number
}) => {
if (assetIsPosition(item)) {
return <PositionItem position={item} />
return <PositionItem position={item} hideBalances={hideWalletBalances} />
} else if ('balance' in item) {
return (
<TokenBalanceItem
Expand All @@ -238,6 +241,7 @@ export default function AssetList({
assetType: 'token',
})
}}
hideBalances={hideWalletBalances}
/>
)
} else {
Expand Down
13 changes: 13 additions & 0 deletions src/tokens/PositionItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ describe('PositionItem', () => {
expect(getByText('11.90')).toBeTruthy()
})

it('shows the correct info for a position with balance hidden', () => {
const { getByText, queryByText } = render(
<Provider store={createMockStore({})}>
<PositionItem position={mockPositions[0]} hideBalances={true} />
</Provider>
)

expect(getByText('MOO / CELO')).toBeTruthy()
expect(getByText('Pool')).toBeTruthy()
expect(queryByText('₱3.34')).toBeFalsy()
expect(queryByText('11.90')).toBeFalsy()
})

it('shows the correct info for a position with a negative balance', () => {
const mockPosition = mockPositions[0] as AppTokenPosition
const { getByText } = render(
Expand Down
Loading

0 comments on commit b2b5fbb

Please sign in to comment.