Skip to content

Commit

Permalink
chore(points): points activities list redesign (#5616)
Browse files Browse the repository at this point in the history
### Description

Implement new points activities list design.

Figma:
https://www.figma.com/design/rXBDplfMHHqYmuu6EkgMEo/Gamification-experiments?node-id=1747-6464&t=aOfd2uHuL1gAJfTG-0

Important changes:
1. Activities are rendered as a list
2. No more grouping by points amount; the amount is indicated on the
card
3. Activities can have previous amount crossed out
4. "More coming soon" card dividing incomplete and completed activities
5. "More coming soon" card has a new icon (rushing clock instead of
rocket)

Implementation notes:
* `ActivityCard` is refactored as a "dumb" component only relying on its
props
* Added a specific `CheckCircle` icon instead of a mix of another icon
and css styling used previously

| New design | Previous design |
|--------|--------|
| ![new
design](https://github.com/valora-inc/wallet/assets/2737872/30687de9-2b81-43b7-a67c-dc650e67af21)
| ![prev
design](https://github.com/valora-inc/wallet/assets/2737872/8ae2badb-8793-4f36-bb43-d174f6323c61)
|

Crossed out previous amount:

| Card | Bottom sheet |
|--------|--------|
| <img width="279" alt="previous amount"
src="https://github.com/valora-inc/wallet/assets/2737872/3fc30522-ab1d-4ba1-a443-d926f20b3964">
|
![image](https://github.com/valora-inc/wallet/assets/2737872/65be43d9-d63e-42ed-82de-43fcbd327d53)
|

### Test plan

* Tested manually
* Updated unit tests

### Related issues

- Fixes RET-1103

### 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
bakoushin committed Jul 8, 2024
1 parent e5e2b3f commit d20acd3
Show file tree
Hide file tree
Showing 17 changed files with 393 additions and 357 deletions.
19 changes: 19 additions & 0 deletions src/icons/CheckCircle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react'
import Colors from 'src/styles/colors'
import Svg, { Path } from 'svgs'

interface Props {
size?: number
color?: string
testID?: string
}

const CheckCircle = ({ size = 24, color = Colors.black, testID }: Props) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" testID={testID}>
<Path
fill={color}
d="m10.6 13.8-2.15-2.15a.948.948 0 0 0-.7-.275.948.948 0 0 0-.7.275.948.948 0 0 0-.275.7c0 .283.092.517.275.7L9.9 15.9c.2.2.433.3.7.3.267 0 .5-.1.7-.3l5.65-5.65a.948.948 0 0 0 .275-.7.948.948 0 0 0-.275-.7.948.948 0 0 0-.7-.275.948.948 0 0 0-.7.275L10.6 13.8ZM12 22a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12c0-1.383.263-2.683.788-3.9a10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2c1.383 0 2.683.263 3.9.788a10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Zm0-2c2.233 0 4.125-.775 5.675-2.325C19.225 16.125 20 14.233 20 12c0-2.233-.775-4.125-2.325-5.675C16.125 4.775 14.233 4 12 4c-2.233 0-4.125.775-5.675 2.325C4.775 7.875 4 9.767 4 12c0 2.233.775 4.125 2.325 5.675C7.875 19.225 9.767 20 12 20Z"
/>
</Svg>
)
export default CheckCircle
18 changes: 0 additions & 18 deletions src/icons/Rocket.tsx

This file was deleted.

18 changes: 18 additions & 0 deletions src/icons/RushingClock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react'
import Colors from 'src/styles/colors'
import Svg, { Path } from 'svgs'

interface Props {
size?: number
color?: string
}

const RushingClock = ({ size = 24, color = Colors.black }: Props) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path
fill={color}
d="M14.667 18.333c-1.861 0-3.438-.645-4.73-1.937C8.646 15.104 8 13.528 8 11.666c0-1.847.646-3.42 1.938-4.718C11.229 5.649 12.806 5 14.666 5c1.847 0 3.42.65 4.718 1.948 1.299 1.299 1.948 2.871 1.948 4.719 0 1.86-.649 3.437-1.948 4.729-1.298 1.291-2.871 1.937-4.718 1.937Zm0-1.666c1.389 0 2.57-.486 3.541-1.459.973-.972 1.459-2.152 1.459-3.541 0-1.39-.486-2.57-1.459-3.542-.972-.972-2.152-1.458-3.541-1.458-1.39 0-2.57.486-3.542 1.458s-1.458 2.153-1.458 3.542c0 1.389.486 2.57 1.458 3.541.972.973 2.153 1.459 3.542 1.459Zm1.896-1.896 1.187-1.188-2.25-2.25v-3h-1.667v3.688l2.73 2.75ZM3.832 9.167V7.5h3.334v1.667H3.833ZM3 12.5v-1.667h4.167V12.5H3Zm.833 3.333v-1.666h3.334v1.666H3.833Z"
/>
</Svg>
)
export default RushingClock
5 changes: 3 additions & 2 deletions src/icons/SwapArrows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import Svg, { Path } from 'svgs'
interface Props {
size?: number
color?: string
testID?: string
}

const SwapArrows = ({ size = 24, color = Colors.black }: Props) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
const SwapArrows = ({ size = 24, color = Colors.black, testID }: Props) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" testID={testID}>
<Path
fill={color}
d="M15.722 17.567v.5h2.624l-3.235 3.227-3.235-3.227H14.5v-7.79h1.222v7.29ZM7.833 6.433v-.5H5.21l3.235-3.227 3.236 3.227H9.056v7.79H7.833v-7.29Z"
Expand Down
63 changes: 63 additions & 0 deletions src/points/ActivityCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { fireEvent, render } from '@testing-library/react-native'
import React from 'react'
import { PointsEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import SwapArrows from 'src/icons/SwapArrows'
import ActivityCard, { Props } from './ActivityCard'

jest.mock('src/analytics/ValoraAnalytics')

const mockProps: Props = {
activityId: 'swap',
pointsAmount: 20,
previousPointsAmount: 10,
completed: false,
title: 'Swap',
icon: <SwapArrows testID="SwapArrowsIcon" />,
onPress: jest.fn(),
}

describe('ActivityCard', () => {
beforeEach(() => {
jest.clearAllMocks()
})

it('renders correctly', () => {
const { getByText, getByTestId } = render(<ActivityCard {...mockProps} />)

expect(getByText('Swap')).toBeTruthy()
expect(getByText('20')).toBeTruthy()
expect(getByText('10')).toBeTruthy()
expect(getByTestId('SwapArrowsIcon')).toBeTruthy()
})

it('renders correcly when completed', () => {
const completedProps = { ...mockProps, completed: true }
const { getByTestId } = render(<ActivityCard {...completedProps} />)

const card = getByTestId('ActivityCard/swap')
expect(card).toBeDisabled()
expect(card.props.style).toContainEqual({ opacity: 0.5 })
expect(getByTestId('CheckCircleIcon')).toBeTruthy()
})

it('handles press correctly', () => {
const { getByText } = render(<ActivityCard {...mockProps} />)

fireEvent.press(getByText('Swap'))
expect(ValoraAnalytics.track).toHaveBeenCalledWith(PointsEvents.points_screen_card_press, {
activityId: 'swap',
})
expect(mockProps.onPress).toHaveBeenCalled()
})

it('tracks analytics event even when no press handler provided', () => {
const noPressProps = { ...mockProps, onPress: undefined }
const { getByText } = render(<ActivityCard {...noPressProps} />)

fireEvent.press(getByText('Swap'))
expect(ValoraAnalytics.track).toHaveBeenCalledWith(PointsEvents.points_screen_card_press, {
activityId: 'swap',
})
})
})
154 changes: 78 additions & 76 deletions src/points/ActivityCard.tsx
Original file line number Diff line number Diff line change
@@ -1,98 +1,100 @@
import React from 'react'
import React, { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native'
import { PointsEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import Touchable from 'src/components/Touchable'
import CheckCircle from 'src/icons/CheckCircle'
import LogoHeart from 'src/icons/LogoHeart'
import RushingClock from 'src/icons/RushingClock'
import { PointsActivity } from 'src/points/types'
import { Colors } from 'src/styles/colors'
import { typeScale } from 'src/styles/fonts'
import { Spacing } from 'src/styles/styles'
import { StyleSheet, Text, View } from 'react-native'
import { Colors } from 'src/styles/colors'
import Checkmark from 'src/icons/Checkmark'
import { PointsActivityId, BottomSheetMetadata, BottomSheetParams } from 'src/points/types'
import useCardDefinitions from 'src/points/cardDefinitions'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import { PointsEvents } from 'src/analytics/Events'

interface Props {
activityId: PointsActivityId
pointsAmount: number
onPress: (bottomSheetParams: BottomSheetParams) => void
completed?: boolean
export interface Props extends PointsActivity {
title: string
icon: ReactNode
onPress?: () => void
}

export default function ActivityCard({ activityId, pointsAmount, onPress, completed }: Props) {
const cardDefinition = useCardDefinitions(pointsAmount)[activityId]
export default function ActivityCard({
activityId,
pointsAmount,
previousPointsAmount,
completed = false,
title,
icon,
onPress,
}: Props) {
const handleOnPress = () => {
ValoraAnalytics.track(PointsEvents.points_screen_card_press, { activityId })
onPress?.()
}

const isCompleted = completed !== undefined ? completed : cardDefinition.defaultCompletionStatus
return (
<Touchable
testID={`ActivityCard/${activityId}`}
style={[styles.card, completed && styles.faded]}
borderRadius={Spacing.Smallest8}
disabled={completed}
onPress={handleOnPress}
>
<>
{completed ? <CheckCircle testID="CheckCircleIcon" /> : icon}
<Text style={styles.cardTitle}>{title}</Text>
<View style={styles.pointsAmountContainer}>
<Text style={styles.previousPointsAmount}>{previousPointsAmount}</Text>
<Text style={styles.pointsAmount}>{pointsAmount}</Text>
<LogoHeart size={Spacing.Regular16} />
</View>
</>
</Touchable>
)
}

const onPressWrapper = (bottomSheetMetadata: BottomSheetMetadata) => {
return () => {
ValoraAnalytics.track(PointsEvents.points_screen_card_press, {
activityId,
})
onPress({
...bottomSheetMetadata,
pointsAmount,
activityId,
})
}
}
export function MoreComingCard() {
const { t } = useTranslation()

const cardStyle = {
...styles.card,
opacity: isCompleted ? 0.5 : 1,
}
return (
<View style={styles.container}>
<View style={styles.cardContainer}>
<Touchable
testID={`PointsActivityCard-${activityId}-${pointsAmount}`}
style={cardStyle}
onPress={
cardDefinition.bottomSheet ? onPressWrapper(cardDefinition.bottomSheet) : undefined
}
disabled={!cardDefinition.bottomSheet}
borderRadius={Spacing.Regular16}
>
<>
{isCompleted && (
<View style={styles.checkmarkIcon}>
<Checkmark height={12} width={12} color={Colors.black} stroke={true} />
</View>
)}
{cardDefinition.icon}
<Text style={styles.cardTitle}>{cardDefinition.title}</Text>
</>
</Touchable>
</View>
<View style={styles.card}>
<RushingClock />
<Text style={styles.cardTitle}>{t('points.activityCards.moreComing.title')}</Text>
</View>
)
}

const styles = StyleSheet.create({
container: {
flexBasis: '50%',
padding: Spacing.Smallest8,
},
checkmarkIcon: {
position: 'absolute',
top: Spacing.Smallest8,
right: Spacing.Smallest8,
borderRadius: 16,
borderWidth: 1.5,
borderColor: Colors.black,
},
cardTitle: {
...typeScale.labelXSmall,
paddingTop: Spacing.Smallest8,
textAlign: 'center',
},
cardContainer: {
borderRadius: Spacing.Regular16,
backgroundColor: Colors.gray1,
...typeScale.labelSmall,
},
card: {
flex: 1,
justifyContent: 'center',
backgroundColor: Colors.gray1,
flexDirection: 'row',
alignItems: 'center',
padding: Spacing.Regular16,
minHeight: 96,
gap: Spacing.Smallest8,
},
faded: {
opacity: 0.5,
},
pointsAmountContainer: {
marginLeft: 'auto',
flexDirection: 'row',
borderRadius: 100,
borderWidth: 1,
borderColor: Colors.gray2,
paddingHorizontal: Spacing.Smallest8,
paddingVertical: Spacing.Tiny4,
gap: Spacing.Tiny4,
},
pointsAmount: {
...typeScale.labelXSmall,
color: Colors.black,
},
previousPointsAmount: {
...typeScale.labelXSmall,
color: Colors.gray3,
textDecorationLine: 'line-through',
},
})
Loading

0 comments on commit d20acd3

Please sign in to comment.