Skip to content

Commit

Permalink
add: a/b tested promotion of hiding spam tokens (#1504)
Browse files Browse the repository at this point in the history
* add: a/b tested promotion of hiding spam tokens

* hide tooltip on mobile

* fix: small h and correct singular / plural

* fix: remove unnecessary css
  • Loading branch information
schmanu authored Jan 10, 2023
1 parent c1454b2 commit bd19628
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 27 deletions.
56 changes: 39 additions & 17 deletions src/components/balances/HiddenTokenButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import { VisibilityOutlined } from '@mui/icons-material'
import Track from '@/components/common/Track'

import css from './styles.module.css'
import { AbTest } from '@/services/tracking/abTesting'
import { OnboardingTooltip } from '@/components/common/OnboardingTooltip'
import useABTesting from '@/services/tracking/useAbTesting'

const LS_ONBOARDING = 'ONBOARDING_HIDDEN_TOKEN_BUTTON'

const HiddenTokenButton = ({
toggleShowHiddenAssets,
Expand All @@ -17,28 +22,45 @@ const HiddenTokenButton = ({
}): ReactElement | null => {
const { balances } = useBalances()
const currentHiddenAssets = useHiddenTokens()
const isTooltipShown = useABTesting(AbTest.HIDE_TOKEN_PROMO)

const hiddenAssetCount =
balances.items?.filter((item) => currentHiddenAssets.includes(item.tokenInfo.address)).length || 0

return (
<Track {...ASSETS_EVENTS.SHOW_HIDDEN_ASSETS}>
<Button
className={css.hiddenTokenButton}
sx={{ gap: 1, padding: 1, borderWidth: '1px !important', borderColor: ({ palette }) => palette.border.main }}
disabled={showHiddenAssets}
onClick={toggleShowHiddenAssets}
data-testid="toggle-hidden-assets"
variant="outlined"
>
<>
<VisibilityOutlined fontSize="small" />
<Typography fontSize="small">
{hiddenAssetCount === 0 ? 'Hide tokens' : `${hiddenAssetCount} Hidden token(s)`}{' '}
</Typography>
</>
</Button>
</Track>
<OnboardingTooltip
className={css.hiddenTokenButton}
initiallyShown={isTooltipShown}
widgetLocalStorageId={LS_ONBOARDING}
text="Spam or unwanted tokens in your asset list? Hide them now!"
>
<div>
<Track {...ASSETS_EVENTS.SHOW_HIDDEN_ASSETS}>
<Button
className={css.hiddenTokenButton}
sx={{
gap: 1,
padding: 1,
borderWidth: '1px !important',
borderColor: ({ palette }) => palette.border.main,
}}
disabled={showHiddenAssets}
onClick={toggleShowHiddenAssets}
data-testid="toggle-hidden-assets"
variant="outlined"
>
<>
<VisibilityOutlined fontSize="small" />
<Typography fontSize="small">
{hiddenAssetCount === 0
? 'Hide tokens'
: `${hiddenAssetCount} hidden token${hiddenAssetCount > 1 ? 's' : ''}`}{' '}
</Typography>
</>
</Button>
</Track>
</div>
</OnboardingTooltip>
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import local from '@/services/local-storage/local'
import { act, render } from '@/tests/test-utils'
import { Button } from '@mui/material'
import { OnboardingTooltip } from '..'

describe('<OnboardingWidget>', () => {
test('renders widget on initial render and hides it after click on button', () => {
const text = 'New feature available!'

const result = render(
<OnboardingTooltip text={text} widgetLocalStorageId="someTestId">
<Button>Testbutton</Button>
</OnboardingTooltip>,
)

expect(result.getByText(new RegExp(text))).toBeInTheDocument()
act(() => result.getByText(/Got it/).click())

expect(result.queryByText(new RegExp(text))).not.toBeInTheDocument()
expect(result.getByText(/Testbutton/)).toBeInTheDocument()
})

test('renders multiple widgets with different local storage ids', () => {
const text1 = 'New feature available!'

const text2 = 'Some other feature is available too!'

const result = render(
<div>
<OnboardingTooltip text={text1} widgetLocalStorageId="someTestId1">
<Button>First Button</Button>
</OnboardingTooltip>
<OnboardingTooltip text={text2} widgetLocalStorageId="someTestId2">
<Button>Second Button</Button>
</OnboardingTooltip>
</div>,
)

expect(result.getByText(new RegExp(text1))).toBeInTheDocument()
expect(result.getByText(new RegExp(text2))).toBeInTheDocument()

act(() => result.getAllByText(/Got it/)[0].click())

expect(result.queryByText(new RegExp(text1))).not.toBeInTheDocument()
expect(result.getByText(new RegExp(text2))).toBeInTheDocument()

act(() => result.getByText(/Got it/).click())
expect(result.queryByText(new RegExp(text1))).not.toBeInTheDocument()
expect(result.queryByText(new RegExp(text2))).not.toBeInTheDocument()

expect(result.getByText(/First Button/)).toBeInTheDocument()
expect(result.getByText(/Second Button/)).toBeInTheDocument()
})

test('renders only children if the widget has been hidden', () => {
const widgetStorageId = 'alreadyHiddenId'
local.setItem(widgetStorageId, true)
const text = 'New feature available!'

const result = render(
<OnboardingTooltip text={text} widgetLocalStorageId={widgetStorageId}>
<Button>Testbutton</Button>
</OnboardingTooltip>,
)
expect(result.queryByText(new RegExp(text))).not.toBeInTheDocument()
expect(result.getByText(/Testbutton/)).toBeInTheDocument()
})
})
55 changes: 55 additions & 0 deletions src/components/common/OnboardingTooltip/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import useLocalStorage from '@/services/local-storage/useLocalStorage'
import { Box, Button, SvgIcon, Tooltip } from '@mui/material'
import InfoIcon from '@/public/images/notifications/info.svg'
import React from 'react'
import { useDarkMode } from '@/hooks/useDarkMode'

/**
* The OnboardingTooltip renders a sticky Tooltip with an arrow pointing towards the wrapped component.
* This Tooltip contains a button to hide it. This decision will be stored in the local storage such that the OnboardingTooltip will only popup until clicked away once.
*/
export const OnboardingTooltip = ({
children,
widgetLocalStorageId,
text,
initiallyShown = true,
className,
}: {
children: React.ReactElement
widgetLocalStorageId: string
text: string
initiallyShown?: boolean
className?: string
}): React.ReactElement => {
const [widgetHidden = !initiallyShown, setWidgetHidden] = useLocalStorage<boolean>(widgetLocalStorageId)
const isDarkMode = useDarkMode()

return widgetHidden ? (
children
) : (
<Tooltip
PopperProps={{
className,
}}
open
arrow
title={
<Box display="flex" alignItems="center" gap={1} padding={1}>
<SvgIcon component={InfoIcon} inheritViewBox fontSize="small" />
<span>{text}</span>
<Button
size="small"
color={isDarkMode ? undefined : 'secondary'}
variant="text"
sx={{ whiteSpace: 'nowrap' }}
onClick={() => setWidgetHidden(true)}
>
Got it!
</Button>
</Box>
}
>
{children}
</Tooltip>
)
}
8 changes: 2 additions & 6 deletions src/pages/balances/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@ import { useState } from 'react'

import PagePlaceholder from '@/components/common/PagePlaceholder'
import NoAssetsIcon from '@/public/images/balances/no-assets.svg'
import useHiddenTokens from '@/hooks/useHiddenTokens'
import HiddenTokenButton from '@/components/balances/HiddenTokenButton'
import CurrencySelect from '@/components/balances/CurrencySelect'

const Balances: NextPage = () => {
const { balances, loading, error } = useBalances()
const hiddenAssets = useHiddenTokens()
const { loading, error } = useBalances()
const [showHiddenAssets, setShowHiddenAssets] = useState(false)
const toggleShowHiddenAssets = () => setShowHiddenAssets((prev) => !prev)

Expand All @@ -27,9 +25,7 @@ const Balances: NextPage = () => {

<AssetsHeader>
<Box display="flex" flexDirection="row" alignItems="center" gap={1}>
{hiddenAssets && (
<HiddenTokenButton showHiddenAssets={showHiddenAssets} toggleShowHiddenAssets={toggleShowHiddenAssets} />
)}
<HiddenTokenButton showHiddenAssets={showHiddenAssets} toggleShowHiddenAssets={toggleShowHiddenAssets} />
<CurrencySelect />
</Box>
</AssetsHeader>
Expand Down
9 changes: 5 additions & 4 deletions src/services/tracking/abTesting.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/*
Holds current abTest identifiers e.g.
SAFE_CREATION = 'safe-creation'
/**
* Holds current A/B test identifiers.
*/
export const enum AbTest {}
export const enum AbTest {
HIDE_TOKEN_PROMO = 'hide_token_promo',
}

let _abTest: AbTest | null = null

Expand Down

0 comments on commit bd19628

Please sign in to comment.