Skip to content

Commit

Permalink
chore(lib): @ledgerhq/crypto-icons updates (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
mwiercinska authored Aug 8, 2024
1 parent 9ff334f commit e8fa289
Show file tree
Hide file tree
Showing 14 changed files with 210 additions and 13 deletions.
97 changes: 96 additions & 1 deletion lib/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,96 @@
TODO
# @ledgerhq/crypto-icons

A package which provides a `<CryptoIcon />` component that can be consumed by Ledger applications in a React environment and used with a `ledgerId` to render icons.

<!-- TODO: add Storybook link -->

## Installation

```bash
pnpm i @ledgerhq/crypto-icons
# or
yarn add @ledgerhq/crypto-icons
```

### Usage example

```JSX
import { CryptoIcon } from '@ledgerhq/crypto-icons';

const Page = () => {
return (
<>
<CryptoIcon ledgerId="bitcoin" ticker="BTC" />
<CryptoIcon ledgerId="ethereum" ticker="ETH" size="32px" />
<CryptoIcon ledgerId="solana" ticker="SOL" size="48px" theme="light" />
</>
)
}
```

## Icon sources

The component's primary source of icons is Ledger's CDN which contains the [assets](../assets/index.json) from this repository. It attempts to fetch a [mapping from Ledger's CDN](https://crypto-icons.ledger.com/index.json) and if the ledgerId that is passed in as a prop to the component is found, the URL for that key is used as the image source. Otherwise, a request to the [Ledger mapping service](https://ledgerhq.atlassian.net/wiki/spaces/BE/pages/3973022073/Mapping+Service) is made to retrieve a [CoinGecko mapping](https://mapping-service.api.ledger.com/v1/coingecko/mapped-assets) as a fallback. If a match for an icon is found using the ledgerId then it is used as the image source. If neither mapping has a match, a `<FallbackIcon />` component is returned with the first letter of the currency ticker as its content e.g. B for BTC.

```mermaid
flowchart TD
A[getIconUrl with ledgerId] --> B{ledgerMapping is null?}
B --> |Yes| C[setLedgerIconMapping] --> D[fetchIconMapping from Ledger CDN] --> E{success?}
E --> |Yes| F[cache data in ledgerMapping] --> G
E --> |No| I{coinGeckoMapping is null?}
B --> |No| G{ledgerId in ledgerMapping?}
G --> |Yes| H[return URL]
G --> |No| I{coinGeckoMapping is null?}
I --> |Yes| J[setCoinGeckoIconMapping] --> K[fetchIconMapping from CoinGecko] --> L{success?}
I --> |No| O
L --> |Yes| M[cache data in coinGeckoMapping] --> O{ledgerId in coinGeckoMapping?}
L --> |No| N[return null]
O --> |Yes| P[return URL]
O --> |No| R[return null]
```

## Contributing

Make sure you're in the correct directory:

```bash
cd lib
```

### Install dependencies

```bash
pnpm i
```

### Run tests

```bash
pnpm test
# or
pnpm test:watch # to run in watch mode
```

### Run storybook

```bash
pnpm storybook
```

### Lint

```bash
pnpm lint # to find issues
# or
pnpm lint:fix # to find and fix issues
```

### Build package with Rollup

```bash
pnpm build
```

### Test locally

Package can be tested locally with `pnpm-link` or `file:` protocol. Details can be found here: [https://pnpm.io/cli/link#whats-the-difference-between-pnpm-link-and-using-the-file-protocol](https://pnpm.io/cli/link#whats-the-difference-between-pnpm-link-and-using-the-file-protocol).
1 change: 1 addition & 0 deletions lib/__mocks__/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'test-file-stub';
6 changes: 6 additions & 0 deletions lib/__mocks__/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ export const coinGeckoMock: CoinGeckoMapping = [
img: 'https://proxycgassets.api.live.ledger.com/coins/images/329/large/decred.png',
},
},
{
ledgerId: 'particl',
data: {
img: 'https://proxycgassets.api.live.ledger.com/coins/images/839/large/Particl.png',
},
},
];

export const handlers = [
Expand Down
44 changes: 43 additions & 1 deletion lib/__tests__/iconMapping.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,45 @@
import axios from 'axios';
import { coinGeckoMock, ledgerCDNMock } from '../__mocks__/handlers';
import { CRYPTO_ICONS_CDN_BASE } from '../src/constants';
import { getIconUrl, resetIconCacheForTesting } from '../src/iconMapping';

const axiosSpy = jest.spyOn(axios, 'get');

describe('iconMapping', () => {
it.todo('test caching logic');
beforeEach(() => {
resetIconCacheForTesting();
});

afterEach(() => {
jest.clearAllMocks();
});

it('should request icon data from Ledger CDN mapping, store the response and not re-fetch', async () => {
const ledgerIconUrl = await getIconUrl('bitcoin');
expect(ledgerIconUrl).toBe(`${CRYPTO_ICONS_CDN_BASE}/${ledgerCDNMock?.['bitcoin'].icon}`);

const ledgerIconUrl2 = await getIconUrl('arbitrum');
expect(ledgerIconUrl2).toBe(`${CRYPTO_ICONS_CDN_BASE}/${ledgerCDNMock?.['arbitrum'].icon}`);

expect(axiosSpy).toHaveBeenCalledTimes(1);
});

it('should request fallback icon data from CoinGecko mapping if icon is not available in existing data, store the response and not re-fetch', async () => {
const ledgerIconUrl = await getIconUrl('bitcoin');
expect(ledgerIconUrl).toBe(`${CRYPTO_ICONS_CDN_BASE}/${ledgerCDNMock?.['bitcoin'].icon}`);

const coinGeckoIconUrl = await getIconUrl('decred');
expect(coinGeckoIconUrl).toBe(coinGeckoMock?.find((i) => i.ledgerId === 'decred')?.data.img);

const coinGeckoIconUrl2 = await getIconUrl('particl');
expect(coinGeckoIconUrl2).toBe(coinGeckoMock?.find((i) => i.ledgerId === 'particl')?.data.img);

expect(axiosSpy).toHaveBeenCalledTimes(2);
});

it('should return null if icon is not found in either data', async () => {
const iconUrl = await getIconUrl('nonexisting');
expect(axiosSpy).toHaveBeenCalledTimes(2);
expect(iconUrl).toBe(null);
});
});
4 changes: 3 additions & 1 deletion lib/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// eslint-disable-next-line no-undef
module.exports = {
roots: ['<rootDir>/__tests__'],
moduleFileExtensions: ['ts', 'tsx', 'js'],
Expand All @@ -13,4 +12,7 @@ module.exports = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testEnvironment: 'jsdom',
testMatch: ['**/*.test.(ts|tsx)'],
moduleNameMapper: {
'\\.woff2$': '<rootDir>/__mocks__/files.ts',
},
};
1 change: 1 addition & 0 deletions lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-url": "^8.0.2",
"@storybook/addon-webpack5-compiler-swc": "^1.0.4",
"@storybook/react": "^8.2.6",
"@storybook/react-webpack5": "^8.2.6",
Expand Down
27 changes: 27 additions & 0 deletions lib/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions lib/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
import url from '@rollup/plugin-url';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import typescript from 'rollup-plugin-typescript2';

Expand All @@ -22,12 +23,18 @@ export default {
],
external: [Object.keys(pkg.peerDependencies || {})],
plugins: [
commonjs(),
peerDepsExternal(),
resolve(),
commonjs(),
terser(),
typescript({
useTsconfigDeclarationDir: true,
}),
terser(),
url({
include: ['**/*.woff2'],
fileName: '[dirname][hash][extname]',
emitFiles: 'true',
limit: 0,
}),
],
};
22 changes: 17 additions & 5 deletions lib/src/components/FallbackIcon/FallbackIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import React, { FC } from 'react';
import styled from 'styled-components';
import styled, { createGlobalStyle } from 'styled-components';
import InterBold from '../../fonts/Inter-Bold.woff2';
import { CryptoIconProps } from '../CryptoIcon/CryptoIcon.types';

type FallbackIconProps = Pick<CryptoIconProps, 'ticker' | 'size'>;

const InterBoldFont = createGlobalStyle`
@font-face {
font-family: 'Inter';
src: url(${InterBold}) format('woff2');
}
`;

const iconSizeToFontSize: {
[key in NonNullable<FallbackIconProps['size']>]: string;
} = {
Expand All @@ -21,16 +29,20 @@ const Icon = styled.div<Partial<FallbackIconProps>>`
display: flex;
align-items: center;
justify-content: center;
background-color: #717070;
background-color: #757575;
color: #ffffff;
font-weight: 700;
font-size: ${({ size }) => iconSizeToFontSize[size!]};
font-family: 'Inter', sans-serif;
`;

const FallbackIcon: FC<FallbackIconProps> = ({ ticker, size }) => (
<Icon size={size} role="img">
{ticker[0]}
</Icon>
<>
<InterBoldFont />
<Icon size={size} role="img">
{ticker[0]}
</Icon>
</>
);

export default FallbackIcon;
2 changes: 1 addition & 1 deletion lib/src/components/IconWrapper/IconWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const Wrapper = styled.div<IconWrapperProps>`
border-color: ${({ theme }: { theme: 'dark' | 'light' }) => palettes[theme].opacityDefault.c05};
`;

const IconWrapper: FC<IconWrapperProps> = ({ children, size, theme = 'light' }) => (
const IconWrapper: FC<IconWrapperProps> = ({ children, size, theme = 'dark' }) => (
<Wrapper size={size} theme={theme}>
{children}
</Wrapper>
Expand Down
2 changes: 1 addition & 1 deletion lib/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const CRYPTO_ICONS_CDN_BASE = 'https://crypto-icons.ledger.com';
export const COINGECKO_MAPPED_ASSETS_URL =
'https://mapping-service.api.aws.stg.ldg-tech.com/v1/coingecko/mapped-assets';
'https://mapping-service.api.ledger.com/v1/coingecko/mapped-assets';
Binary file added lib/src/fonts/Inter-Bold.woff2
Binary file not shown.
4 changes: 4 additions & 0 deletions lib/src/fonts/fonts.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module '*.woff2' {
const src: string;
export default src;
}
2 changes: 1 addition & 1 deletion lib/tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"extends": "./tsconfig.json",
"include": ["__tests__"]
"include": ["__tests__", "jest.setup.ts", "src"]
}

0 comments on commit e8fa289

Please sign in to comment.