Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UAT-31 #73

Merged
merged 5 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 30 additions & 19 deletions docs/components/badge-tile.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Badge Tile

Displays achievement badges with support for latest earned, specific badges, and multiple achievement states.
Displays achievement badges with support for latest earned and specific badges.

> ⚠️ **Important**: This component only supports full height tiles (`tileHeight: 'FULL'`). Half-height tiles are not supported.
Expand All @@ -13,14 +13,13 @@ const tile = {
type: 'BADGE',
tileHeight: 'FULL',
configuration: {
type: 'SPECIFIC',
type: 'SPECIFIC', // or 'LATEST_EARNED'
name: 'Top Spender',
description: 'Spent £100 on 5 Separate transactions',
artworkUrl: 'https://example.com/badge.png',
count: 1,
awardedDatePrefix: 'Awarded',
badgeNotEarnedMessage: 'Badge not earned yet',
emptyBadgeMessage: "You haven't earned any badges yet"
badgeNotEarnedMessage: 'Badge not earned yet' // Optional, controls earned/not earned chip visibility
}
}

Expand All @@ -37,28 +36,40 @@ function MyComponent() {

## Configuration Object

| Property | Type | Description |
|----------|------|-------------|
| type | 'SPECIFIC' \| 'LATEST_EARNED' | Badge display type |
| name | string | Badge name |
| description | string | Badge description |
| artworkUrl | string | Badge image URL |
| count | number | Times achieved (0 = locked) |
| awardedDatePrefix | string | Text before award date |
| badgeNotEarnedMessage | string | Message for unearned badges |
| emptyBadgeMessage | string | Message when no badges earned |
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| type | 'SPECIFIC' \| 'LATEST_EARNED' | Yes | Badge display type. Use 'SPECIFIC' for a particular badge, 'LATEST_EARNED' for most recent achievement |
| name | string | Yes | Badge name |
| description | string | No | Badge description |
| artworkUrl | string | Yes | Badge image URL |
| count | number | Yes | Times achieved (0 = not earned) |
| awardedDatePrefix | string | No | Text before award date |
| badgeNotEarnedMessage | string | No | Message shown in status chip when badge is not earned. If not provided, no chip will be shown for unearned badges |

## Display Types

### SPECIFIC
- Shows a particular badge regardless of earned state
- Always shows description if provided
- Shows earned/not earned chip if badgeNotEarnedMessage is provided

### LATEST_EARNED
- Used to display the most recent achievement
- Hides description when not earned (count = 0)
- Shows earned/not earned chip if badgeNotEarnedMessage is provided

## States

- **Not Earned**: Shows lock icon (count: 0)
- **Not Earned**: Shows badge with count: 0
- If `badgeNotEarnedMessage` is provided, shows status chip with that message
- For LATEST_EARNED type, hides description
- **Earned Once**: Shows badge with award date
- **Multiple Earned**: Shows badge with count (e.g., "3x")
- **Latest Badge**: Special display for most recent achievement

## Composition

- `BadgeTile.Media` - Badge artwork container
- `BadgeTile.Status` - Lock icon or achievement count
- `BadgeTile.Title` - Badge name or empty state message
- `BadgeTile.Description` - Badge description
- `BadgeTile.DateEarned` - Award date or status message
- `BadgeTile.Title` - Badge name
- `BadgeTile.Description` - Badge description (hidden for unearned LATEST_EARNED badges)
- `BadgeTile.DateEarned` - Award date or not earned message (if provided)
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ exports[`<Grid /> matches snapshot 1`] = `
style="background-color: rgba(57, 46, 215, 0.2);"
>
<div
class="css-text-146c3p1"
class="css-text-146c3p1 r-maxWidth-dnmrzs r-overflow-1udh08x r-textOverflow-1udbk01 r-whiteSpace-3s2u2q r-wordWrap-1iln25a"
dir="auto"
style="font-size: 12px; color: rgb(255, 255, 255);"
>
Expand Down
45 changes: 39 additions & 6 deletions lib/components/organisms/BadgeTile/BadgeTile.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,39 @@ describe('<BadgeTile />', () => {
},
};
render(<BadgeTile tile={specificBadge} />);
expect(
screen.getByText(specificBadge.configuration.name)
).toBeInTheDocument();
expect(
screen.getByText(specificBadge.configuration.description)
).toBeInTheDocument();
expect(
screen.getByText(specificBadge.configuration.badgeNotEarnedMessage)
).toBeInTheDocument();
});

it('does not show earned/not earned chip when badgeNotEarnedMessage is not provided', () => {
const specificBadge = {
...BadgeTileMock,
configuration: {
...BadgeTileMock.configuration,
type: BadgeTileType.Specific,
count: 0,
badgeNotEarnedMessage: undefined,
},
};
render(<BadgeTile tile={specificBadge} />);
expect(
screen.getByText(specificBadge.configuration.name)
).toBeInTheDocument();
expect(
screen.getByText(specificBadge.configuration.description)
).toBeInTheDocument();
expect(
screen.queryByTestId('badge-tile-date-earned')
).not.toBeInTheDocument();
});

it('renders specific badge type correctly when earned once', () => {
const specificBadge = {
...BadgeTileMock,
Expand Down Expand Up @@ -157,25 +185,30 @@ describe('<BadgeTile />', () => {
describe('Badge Media', () => {
it('renders badge artwork when provided', () => {
render(<BadgeTile tile={BadgeTileMock} />);
expect(screen.getByTestId('badge-tile-media')).toBeInTheDocument();
const media = screen.getByTestId('badge-tile-media');
expect(media).toBeInTheDocument();
const progressiveImage = media.querySelector('[src]');
expect(progressiveImage).toHaveAttribute(
'src',
BadgeTileMock.configuration.artworkUrl
);
});

it('renders empty badge artwork when no badges earned', () => {
const emptyBadge = {
it('always uses main artworkUrl regardless of earned state', () => {
const unearned = {
...BadgeTileMock,
configuration: {
...BadgeTileMock.configuration,
count: 0,
},
};
render(<BadgeTile tile={emptyBadge} />);
render(<BadgeTile tile={unearned} />);
const media = screen.getByTestId('badge-tile-media');
expect(media).toBeInTheDocument();

const progressiveImage = media.querySelector('[src]');
expect(progressiveImage).toHaveAttribute(
'src',
emptyBadge.configuration.emptyBadgeArtworkUrl
unearned.configuration.artworkUrl
);
});
});
Expand Down
28 changes: 28 additions & 0 deletions lib/components/organisms/BadgeTile/BadgeTile.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,31 @@ SpecificBadgeAwardedOnce.args = {
},
},
};

export const SpecificBadgeNotEarnedNoEmpty = Template.bind({});
SpecificBadgeNotEarnedNoEmpty.args = {
tile: {
id: 'specific-badge-not-earned-no-empty',
type: TileType.Badge,
active: true,
createdAt: '2024-08-06T08:53:24.307Z',
updatedAt: '2024-08-06T08:53:24.307Z',
tileHeight: TileHeight.Full,
priority: 0,
configuration: {
type: BadgeTileType.Specific,
badgeId: '900a2477-95c4-4c42-ae2d-3795e7f0f5f2',
internalName: 'Top Spender',
name: 'Top Spender',
description: 'Spent £100 on 5 Separate transactions',
artworkUrl: 'https://ucarecdn.com/3d3731b2-faec-4779-9cd8-3691631d280c/',
priority: 0,
status: 'ACTIVE',
createdAt: '2024-08-06T08:53:24.307Z',
updatedAt: '2024-08-06T08:53:24.307Z',
awardedDatePrefix: 'Awarded',
badgeNotEarnedMessage: 'Complete 5 top-ups to earn this',
count: 0,
},
},
};
26 changes: 18 additions & 8 deletions lib/components/organisms/BadgeTile/badge-tile-date-earned.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,24 @@ export const BadgeTileDateEarned = (): JSX.Element | null => {
const styles = useBadgeTileStyles();
const tile = useTileContext();
const { configuration } = tile as { configuration: BadgeTileConfig };
const { type, count, awardedDatePrefix, createdAt, badgeNotEarnedMessage } =
const { count, awardedDatePrefix, createdAt, badgeNotEarnedMessage, type } =
configuration;
const { theme } = useWllSdk();

// Don't show for Latest type with count=0
if (type === BadgeTileType.Latest && count === 0) {
return null;
}

// For Specific type, only show if count > 0 or badgeNotEarnedMessage exists
if (
type === BadgeTileType.Specific &&
count === 0 &&
!badgeNotEarnedMessage
) {
return null;
}

const backgroundColor = getStateColor(
theme.alphaDerivedPrimary[20],
type,
Expand All @@ -26,10 +40,6 @@ export const BadgeTileDateEarned = (): JSX.Element | null => {
const containerStyle = [styles.dateEarnedContainer, { backgroundColor }];
const textColor = getReadableTextColor(backgroundColor);

if (type === BadgeTileType.Latest && count === 0) {
return null;
}

const displayText =
count === 0
? badgeNotEarnedMessage
Expand All @@ -40,8 +50,6 @@ export const BadgeTileDateEarned = (): JSX.Element | null => {
? 'Badge not yet earned'
: `Badge earned on ${new Date(createdAt).toLocaleDateString()}`;

if (!displayText) return null;

return (
<View
style={containerStyle}
Expand All @@ -51,7 +59,9 @@ export const BadgeTileDateEarned = (): JSX.Element | null => {
>
<Text
variant="label"
style={{ color: textColor }}
style={[styles.dateEarnedText, { color: textColor }]}
numberOfLines={1}
ellipsizeMode="tail"
accessibilityElementsHidden={true}
importantForAccessibility="no-hide-descendants"
>
Expand Down
10 changes: 7 additions & 3 deletions lib/components/organisms/BadgeTile/badge-tile-description.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import React from 'react';
import { View } from 'react-native';
import { BadgeTileConfig } from '../../../types/tile';
import { BadgeTileConfig, BadgeTileType } from '../../../types/tile';
import { Text } from '../../atoms';
import { useTileContext } from '../../atoms/BaseTile';

export const BadgeTileDescription = (): JSX.Element | null => {
const tile = useTileContext();
const { configuration } = tile as { configuration: BadgeTileConfig };
const { count, description } = configuration;
const { count, description, type } = configuration;

if (count === 0 || !description) return null;
if (!description) return null;

// For Latest type, hide description when count = 0
// For Specific type, always show description
if (count === 0 && type !== BadgeTileType.Specific) return null;
iamgraeme marked this conversation as resolved.
Show resolved Hide resolved

return (
<View
Expand Down
12 changes: 9 additions & 3 deletions lib/components/organisms/BadgeTile/badge-tile-media.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { View } from 'react-native';
import { BadgeTileConfig } from '../../../types/tile';
import { BadgeTileConfig, BadgeTileType } from '../../../types/tile';
import { shouldDesaturate } from '../../../utils/themeHelpers';
import { ProgressiveImage } from '../../atoms';
import { useTileContext } from '../../atoms/BaseTile';
Expand All @@ -17,9 +17,15 @@ export const BadgeTileMedia = ({
const styles = useBadgeTileStyles();
const tile = useTileContext();
const { configuration } = tile as { configuration: BadgeTileConfig };
const { type, count, artworkUrl, emptyBadgeArtworkUrl } = configuration;
const { count, artworkUrl, emptyBadgeArtworkUrl, type } = configuration;

const displayUrl =
type === BadgeTileType.Specific
? artworkUrl
: count === 0
? emptyBadgeArtworkUrl
: artworkUrl;

const displayUrl = count === 0 ? emptyBadgeArtworkUrl : artworkUrl;
if (!displayUrl) return null;

return (
Expand Down
12 changes: 9 additions & 3 deletions lib/components/organisms/BadgeTile/badge-tile-title.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { View } from 'react-native';
import { BadgeTileConfig } from '../../../types/tile';
import { BadgeTileConfig, BadgeTileType } from '../../../types/tile';
import { Text } from '../../atoms';
import { useTileContext } from '../../atoms/BaseTile';
import { useBadgeTileStyles } from './styles';
Expand All @@ -9,9 +9,15 @@ export const BadgeTileTitle = (): JSX.Element | null => {
const styles = useBadgeTileStyles();
const tile = useTileContext();
const { configuration } = tile as { configuration: BadgeTileConfig };
const { count, name, emptyBadgeMessage } = configuration;
const { count, name, emptyBadgeMessage, type } = configuration;

const displayText =
type === BadgeTileType.Specific
? name
: count === 0
? emptyBadgeMessage
: name;

const displayText = count === 0 ? emptyBadgeMessage : name;
if (!displayText) return null;

return (
Expand Down