From e95866a07f7f0813a727ee92ac0788fc95339af4 Mon Sep 17 00:00:00 2001 From: Brian August Nguyen Date: Fri, 28 Feb 2025 10:32:20 -0800 Subject: [PATCH] [DSRN] Added MVP Blockies (#455) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds the MVP version of `Blockies` by using `metamask-mobile`'s version of `Blockies` under the hood. In the future, `Blockies` will be recreated to align across platforms ## **Related issues** Fixes: #424 ## **Manual testing steps** 1. Run `yarn storybook:ios` from root 2. Go to Primitives > Jazzicon 3. ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/389bcb7d-71c7-4a1d-95fc-a8eae73fd5f2 ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../.storybook/storybook.requires.js | 1 + .../design-system-react-native/src/index.ts | 8 + .../primitives/Blockies/Blockies.stories.tsx | 50 +++ .../src/primitives/Blockies/Blockies.test.tsx | 60 +++ .../src/primitives/Blockies/Blockies.tsx | 21 + .../src/primitives/Blockies/Blockies.types.ts | 15 + .../primitives/Blockies/Blockies.utilities.js | 422 ++++++++++++++++++ .../src/primitives/Blockies/README.md | 114 +++++ .../src/primitives/Blockies/index.ts | 2 + 9 files changed, 693 insertions(+) create mode 100644 packages/design-system-react-native/src/primitives/Blockies/Blockies.stories.tsx create mode 100644 packages/design-system-react-native/src/primitives/Blockies/Blockies.test.tsx create mode 100644 packages/design-system-react-native/src/primitives/Blockies/Blockies.tsx create mode 100644 packages/design-system-react-native/src/primitives/Blockies/Blockies.types.ts create mode 100644 packages/design-system-react-native/src/primitives/Blockies/Blockies.utilities.js create mode 100644 packages/design-system-react-native/src/primitives/Blockies/README.md create mode 100644 packages/design-system-react-native/src/primitives/Blockies/index.ts diff --git a/apps/storybook-react-native/.storybook/storybook.requires.js b/apps/storybook-react-native/.storybook/storybook.requires.js index 6d5cb089..4ffe6ed5 100644 --- a/apps/storybook-react-native/.storybook/storybook.requires.js +++ b/apps/storybook-react-native/.storybook/storybook.requires.js @@ -57,6 +57,7 @@ const getStories = () => { "./../../packages/design-system-react-native/src/components/Text/Text.stories.tsx": require("../../../packages/design-system-react-native/src/components/Text/Text.stories.tsx"), "./../../packages/design-system-react-native/src/components/TextButton/TextButton.stories.tsx": require("../../../packages/design-system-react-native/src/components/TextButton/TextButton.stories.tsx"), "./../../packages/design-system-react-native/src/primitives/AvatarBase/AvatarBase.stories.tsx": require("../../../packages/design-system-react-native/src/primitives/AvatarBase/AvatarBase.stories.tsx"), + "./../../packages/design-system-react-native/src/primitives/Blockies/Blockies.stories.tsx": require("../../../packages/design-system-react-native/src/primitives/Blockies/Blockies.stories.tsx"), "./../../packages/design-system-react-native/src/primitives/ButtonAnimated/ButtonAnimated.stories.tsx": require("../../../packages/design-system-react-native/src/primitives/ButtonAnimated/ButtonAnimated.stories.tsx"), "./../../packages/design-system-react-native/src/primitives/ButtonBase/ButtonBase.stories.tsx": require("../../../packages/design-system-react-native/src/primitives/ButtonBase/ButtonBase.stories.tsx"), "./../../packages/design-system-react-native/src/primitives/ImageOrSvg/ImageOrSvg.stories.tsx": require("../../../packages/design-system-react-native/src/primitives/ImageOrSvg/ImageOrSvg.stories.tsx"), diff --git a/packages/design-system-react-native/src/index.ts b/packages/design-system-react-native/src/index.ts index 7f494ab2..79009ea1 100644 --- a/packages/design-system-react-native/src/index.ts +++ b/packages/design-system-react-native/src/index.ts @@ -12,6 +12,10 @@ export { AvatarIconSize, } from './components/AvatarIcon'; +import BlockiesComponent from './primitives/Blockies'; +export const Blockies = BlockiesComponent; +export { BlockiesProps } from './primitives/Blockies'; + import ButtonAnimatedComponent from './primitives/ButtonAnimated'; export const ButtonAnimated = withThemeProvider(ButtonAnimatedComponent); export { ButtonAnimatedProps } from './primitives/ButtonAnimated'; @@ -32,6 +36,10 @@ import IconComponent from './components/Icon'; export const Icon = withThemeProvider(IconComponent); export { IconColor, IconName, IconProps, IconSize } from './components/Icon'; +import JazziconComponent from './primitives/Jazzicon'; +export const Jazzicon = JazziconComponent; +export { JazziconProps } from './primitives/Jazzicon'; + import TextButtonComponent from './components/TextButton'; export const TextButton = withThemeProvider(TextButtonComponent); export { TextButtonProps } from './components/TextButton'; diff --git a/packages/design-system-react-native/src/primitives/Blockies/Blockies.stories.tsx b/packages/design-system-react-native/src/primitives/Blockies/Blockies.stories.tsx new file mode 100644 index 00000000..7a12d3f6 --- /dev/null +++ b/packages/design-system-react-native/src/primitives/Blockies/Blockies.stories.tsx @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from '@storybook/react-native'; +import { View } from 'react-native'; + +import Blockies from './Blockies'; +import type { BlockiesProps } from './Blockies.types'; + +const meta: Meta = { + title: 'Primitives/Blockies', + component: Blockies, + argTypes: { + size: { + control: 'number', + }, + }, +}; + +export default meta; +type Story = StoryObj; +const sampleAccountAddresses = [ + '0x9Cbf7c41B7787F6c621115010D3B044029FE2Ce8', + '0xb9b81f6bd23B953c5257C3b5E2F0c03B07E944eB', + '0x360507dfEC4Bf0c03495f91154A78C672599F308', + '0x50cA820Ff810F7687E7d0aDb23A830e3ac6032C3', + '0x840C9Eb73729E626673714D6E4dA8afc8Ccc90d3', + '0xCA0361BE89B7d47a6233d1875F0727ddeAB23377', + '0xD78CBcA88eCd65c6128511e46a518CDc6c66fC74', + '0xCFc8b1d1031ef2ecce3a98d5D79ce4D75Edb06bA', + '0xDe53fa2E659b6134991bB56F64B691990e5C44E7', + '0x8AceA3A9748294d1B5C25a08EFE37b756AafDFdd', + '0xEC5CE72f2e18B0017C88F7B12d3308119C5Cf129', + '0xeC56Da21c90Af6b50E4Ba5ec252bD97e735290fc', +]; +export const Default: Story = { + args: { + size: 32, + }, + render: (args) => { + return ; + }, +}; + +export const SampleAddresses: Story = { + render: () => ( + + {sampleAccountAddresses.map((addressKey) => ( + + ))} + + ), +}; diff --git a/packages/design-system-react-native/src/primitives/Blockies/Blockies.test.tsx b/packages/design-system-react-native/src/primitives/Blockies/Blockies.test.tsx new file mode 100644 index 00000000..30c6f353 --- /dev/null +++ b/packages/design-system-react-native/src/primitives/Blockies/Blockies.test.tsx @@ -0,0 +1,60 @@ +// Blockies.test.tsx +import React from 'react'; +import { render } from '@testing-library/react-native'; +import renderer from 'react-test-renderer'; + +// @ts-ignore +import { toDataUrl } from './Blockies.utilities'; +import Blockies from './Blockies'; + +// Mock the toDataUrl utility +jest.mock('./Blockies.utilities', () => ({ + toDataUrl: jest.fn(() => 'data:image/png;base64,mockedBlockyImage'), +})); + +describe('Blockies Component', () => { + beforeEach(() => { + (toDataUrl as jest.Mock).mockClear(); + }); + + it('renders with default size (32) if size is not provided', () => { + const { getByTestId } = render( + , + ); + // toDataUrl should have been called with the address + expect(toDataUrl).toHaveBeenCalledWith('0x123'); + // Verify the returned Image has width & height of 32 + const image = getByTestId('blockies'); + expect(image.props.width).toBe(32); + expect(image.props.height).toBe(32); + }); + + it('renders with a custom size', () => { + const { getByTestId } = render( + , + ); + const image = getByTestId('blockies'); + expect(image.props.width).toBe(64); + expect(image.props.height).toBe(64); + }); + + it('passes additional image props correctly', () => { + const { getByTestId } = render( + , + ); + const image = getByTestId('blockies'); + expect(image.props.resizeMode).toBe('contain'); + expect(image.props.style).toMatchObject({ margin: 10 }); + }); + + it('calls toDataUrl with the correct address', () => { + render(); + expect(toDataUrl).toHaveBeenCalledWith('0xabc'); + }); +}); diff --git a/packages/design-system-react-native/src/primitives/Blockies/Blockies.tsx b/packages/design-system-react-native/src/primitives/Blockies/Blockies.tsx new file mode 100644 index 00000000..ad95975c --- /dev/null +++ b/packages/design-system-react-native/src/primitives/Blockies/Blockies.tsx @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ +import React from 'react'; +import { Image } from 'react-native'; + +// @ts-ignore +import { toDataUrl } from './Blockies.utilities'; + +import type { BlockiesProps } from './Blockies.types'; + +const Blockies = ({ address, size = 32, ...imageProps }: BlockiesProps) => { + return ( + + ); +}; + +export default Blockies; diff --git a/packages/design-system-react-native/src/primitives/Blockies/Blockies.types.ts b/packages/design-system-react-native/src/primitives/Blockies/Blockies.types.ts new file mode 100644 index 00000000..68da3565 --- /dev/null +++ b/packages/design-system-react-native/src/primitives/Blockies/Blockies.types.ts @@ -0,0 +1,15 @@ +import { ImageProps } from 'react-native'; + +/** + * Blockies component props. + */ +export type BlockiesProps = { + /** + * Required address used as a unique identifier to generate the Blockies. + */ + address: string; + /** + * Optional prop to control the size of the Blockies. + */ + size?: number; +} & Omit; diff --git a/packages/design-system-react-native/src/primitives/Blockies/Blockies.utilities.js b/packages/design-system-react-native/src/primitives/Blockies/Blockies.utilities.js new file mode 100644 index 00000000..90a6da54 --- /dev/null +++ b/packages/design-system-react-native/src/primitives/Blockies/Blockies.utilities.js @@ -0,0 +1,422 @@ +(function (global, factory) { + exports && typeof exports === 'object' && typeof module !== 'undefined' + ? factory(exports) + : typeof define === 'function' && define.amd + ? define(['exports'], factory) + : factory((global.blockies = {})); +})(this, (exports) => { + 'use strict'; + + /** + * A handy class to calculate color values. + * + * @version 1.0 + * @author Robert Eisele + * @copyright Copyright (c) 2010, Robert Eisele + * @link http://www.xarg.org/2010/03/generate-client-side-png-files-using-javascript/ + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * + */ + + // helper functions for that ctx + function write(buffer, offs) { + for (let i = 2; i < arguments.length; i++) { + for (let j = 0; j < arguments[i].length; j++) { + buffer[offs++] = arguments[i].charAt(j); + } + } + } + + function byte2(w) { + return String.fromCharCode((w >> 8) & 255, w & 255); + } + + function byte4(w) { + return String.fromCharCode( + (w >> 24) & 255, + (w >> 16) & 255, + (w >> 8) & 255, + w & 255, + ); + } + + function byte2lsb(w) { + return String.fromCharCode(w & 255, (w >> 8) & 255); + } + + const PNG = function (width, height, depth) { + this.width = width; + this.height = height; + this.depth = depth; + + // pixel data and row filter identifier size + this.pix_size = height * (width + 1); + + // deflate header, pix_size, block headers, adler32 checksum + this.data_size = + 2 + this.pix_size + 5 * Math.floor((0xfffe + this.pix_size) / 0xffff) + 4; + + // offsets and sizes of Png chunks + this.ihdr_offs = 0; // IHDR offset and size + this.ihdr_size = 4 + 4 + 13 + 4; + this.plte_offs = this.ihdr_offs + this.ihdr_size; // PLTE offset and size + this.plte_size = 4 + 4 + 3 * depth + 4; + this.trns_offs = this.plte_offs + this.plte_size; // tRNS offset and size + this.trns_size = 4 + 4 + depth + 4; + this.idat_offs = this.trns_offs + this.trns_size; // IDAT offset and size + this.idat_size = 4 + 4 + this.data_size + 4; + this.iend_offs = this.idat_offs + this.idat_size; // IEND offset and size + this.iend_size = 4 + 4 + 4; + this.buffer_size = this.iend_offs + this.iend_size; // total PNG size + + this.buffer = new Array(); + this.palette = new Object(); + this.pindex = 0; + + const _crc32 = new Array(); + + // initialize buffer with zero bytes + for (var i = 0; i < this.buffer_size; i++) { + this.buffer[i] = '\x00'; + } + + // initialize non-zero elements + write( + this.buffer, + this.ihdr_offs, + byte4(this.ihdr_size - 12), + 'IHDR', + byte4(width), + byte4(height), + '\x08\x03', + ); + write(this.buffer, this.plte_offs, byte4(this.plte_size - 12), 'PLTE'); + write(this.buffer, this.trns_offs, byte4(this.trns_size - 12), 'tRNS'); + write(this.buffer, this.idat_offs, byte4(this.idat_size - 12), 'IDAT'); + write(this.buffer, this.iend_offs, byte4(this.iend_size - 12), 'IEND'); + + // initialize deflate header + let header = ((8 + (7 << 4)) << 8) | (3 << 6); + header += 31 - (header % 31); + + write(this.buffer, this.idat_offs + 8, byte2(header)); + + // initialize deflate block headers + for (var i = 0; (i << 16) - 1 < this.pix_size; i++) { + var size, bits; + if (i + 0xffff < this.pix_size) { + size = 0xffff; + bits = '\x00'; + } else { + size = this.pix_size - (i << 16) - i; + bits = '\x01'; + } + write( + this.buffer, + this.idat_offs + 8 + 2 + (i << 16) + (i << 2), + bits, + byte2lsb(size), + byte2lsb(~size), + ); + } + + /* Create crc32 lookup table */ + for (var i = 0; i < 256; i++) { + let c = i; + for (let j = 0; j < 8; j++) { + if (c & 1) { + c = -306674912 ^ ((c >> 1) & 0x7fffffff); + } else { + c = (c >> 1) & 0x7fffffff; + } + } + _crc32[i] = c; + } + + // compute the index into a png for a given pixel + this.index = function (x, y) { + const i = y * (this.width + 1) + x + 1; + const j = this.idat_offs + 8 + 2 + 5 * Math.floor(i / 0xffff + 1) + i; + return j; + }; + + // convert a color and build up the palette + this.color = function (red, green, blue, alpha) { + alpha = alpha >= 0 ? alpha : 255; + const color = (((((alpha << 8) | red) << 8) | green) << 8) | blue; + + if (typeof this.palette[color] === 'undefined') { + if (this.pindex == this.depth) return '\x00'; + + const ndx = this.plte_offs + 8 + 3 * this.pindex; + + this.buffer[ndx + 0] = String.fromCharCode(red); + this.buffer[ndx + 1] = String.fromCharCode(green); + this.buffer[ndx + 2] = String.fromCharCode(blue); + this.buffer[this.trns_offs + 8 + this.pindex] = + String.fromCharCode(alpha); + + this.palette[color] = String.fromCharCode(this.pindex++); + } + return this.palette[color]; + }; + + // output a PNG string, Base64 encoded + this.getBase64 = function () { + const s = this.getDump(); + + const ch = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + let c1, c2, c3, e1, e2, e3, e4; + const l = s.length; + let i = 0; + let r = ''; + + do { + c1 = s.charCodeAt(i); + e1 = c1 >> 2; + c2 = s.charCodeAt(i + 1); + e2 = ((c1 & 3) << 4) | (c2 >> 4); + c3 = s.charCodeAt(i + 2); + if (l < i + 2) { + e3 = 64; + } else { + e3 = ((c2 & 0xf) << 2) | (c3 >> 6); + } + if (l < i + 3) { + e4 = 64; + } else { + e4 = c3 & 0x3f; + } + r += ch.charAt(e1) + ch.charAt(e2) + ch.charAt(e3) + ch.charAt(e4); + } while ((i += 3) < l); + return r; + }; + + // output a PNG string + this.getDump = function () { + // compute adler32 of output pixels + row filter bytes + const BASE = 65521; /* largest prime smaller than 65536 */ + const NMAX = 5552; /* NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 */ + let s1 = 1; + let s2 = 0; + let n = NMAX; + + for (let y = 0; y < this.height; y++) { + for (let x = -1; x < this.width; x++) { + s1 += this.buffer[this.index(x, y)].charCodeAt(0); + s2 += s1; + if ((n -= 1) == 0) { + s1 %= BASE; + s2 %= BASE; + n = NMAX; + } + } + } + s1 %= BASE; + s2 %= BASE; + write( + this.buffer, + this.idat_offs + this.idat_size - 8, + byte4((s2 << 16) | s1), + ); + + // compute crc32 of the PNG chunks + function crc32(png, offs, size) { + let crc = -1; + for (let i = 4; i < size - 4; i += 1) { + crc = + _crc32[(crc ^ png[offs + i].charCodeAt(0)) & 0xff] ^ + ((crc >> 8) & 0x00ffffff); + } + write(png, offs + size - 4, byte4(crc ^ -1)); + } + + crc32(this.buffer, this.ihdr_offs, this.ihdr_size); + crc32(this.buffer, this.plte_offs, this.plte_size); + crc32(this.buffer, this.trns_offs, this.trns_size); + crc32(this.buffer, this.idat_offs, this.idat_size); + crc32(this.buffer, this.iend_offs, this.iend_size); + + // convert PNG to string + return '\x89PNG\r\n\x1A\n' + this.buffer.join(''); + }; + + this.fillRect = function (x, y, w, h, color) { + for (let i = 0; i < w; i++) { + for (let j = 0; j < h; j++) { + this.buffer[this.index(x + i, y + j)] = color; + } + } + }; + }; + + // https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion + /** + * Converts an HSL color value to RGB. Conversion formula + * adapted from http://en.wikipedia.org/wiki/HSL_color_space. + * Assumes h, s, and l are contained in the set [0, 1] and + * returns r, g, and b in the set [0, 255]. + * + * @param {number} h The hue + * @param {number} s The saturation + * @param {number} l The lightness + * @return {Array} The RGB representation + */ + + function hue2rgb(p, q, t) { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + } + + function hsl2rgb(h, s, l) { + let r, g, b; + + if (s == 0) { + r = g = b = l; // achromatic + } else { + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), 255]; + } + + // The random number is a js implementation of the Xorshift PRNG + const randseed = new Array(4); // Xorshift: [x, y, z, w] 32 bit values + + function seedrand(seed) { + for (var i = 0; i < randseed.length; i++) { + randseed[i] = 0; + } + for (var i = 0; i < seed.length; i++) { + randseed[i % 4] = + (randseed[i % 4] << 5) - randseed[i % 4] + seed.charCodeAt(i); + } + } + + function rand() { + // based on Java's String.hashCode(), expanded to 4 32bit values + const t = randseed[0] ^ (randseed[0] << 11); + + randseed[0] = randseed[1]; + randseed[1] = randseed[2]; + randseed[2] = randseed[3]; + randseed[3] = randseed[3] ^ (randseed[3] >> 19) ^ t ^ (t >> 8); + + return (randseed[3] >>> 0) / ((1 << 31) >>> 0); + } + + function createColor() { + //saturation is the whole color spectrum + const h = Math.floor(rand() * 360); + //saturation goes from 40 to 100, it avoids greyish colors + const s = rand() * 60 + 40; + //lightness can be anything from 0 to 100, but probabilities are a bell curve around 50% + const l = (rand() + rand() + rand() + rand()) * 25; + + return [h / 360, s / 100, l / 100]; + } + + function createImageData(size) { + const width = size; // Only support square icons for now + const height = size; + + const dataWidth = Math.ceil(width / 2); + const mirrorWidth = width - dataWidth; + + const data = []; + for (let y = 0; y < height; y++) { + let row = []; + for (let x = 0; x < dataWidth; x++) { + // this makes foreground and background color to have a 43% (1/2.3) probability + // spot color has 13% chance + row[x] = Math.floor(rand() * 2.3); + } + const r = row.slice(0, mirrorWidth); + r.reverse(); + row = row.concat(r); + + for (let i = 0; i < row.length; i++) { + data.push(row[i]); + } + } + + return data; + } + + function buildOpts(opts) { + if (!opts.seed) { + throw new Error('No seed provided'); + } + + seedrand(opts.seed); + + return Object.assign( + { + size: 8, + scale: 16, + color: createColor(), + bgcolor: createColor(), + spotcolor: createColor(), + }, + opts, + ); + } + + function toDataUrl(address) { + const cache = Blockies.cache[address]; + if (address && cache) { + return cache; + } + + const opts = buildOpts({ seed: address.toLowerCase() }); + + const imageData = createImageData(opts.size); + const width = Math.sqrt(imageData.length); + + const p = new PNG(opts.size * opts.scale, opts.size * opts.scale, 3); + const bgcolor = p.color(...hsl2rgb(...opts.bgcolor)); + const color = p.color(...hsl2rgb(...opts.color)); + const spotcolor = p.color(...hsl2rgb(...opts.spotcolor)); + + for (let i = 0; i < imageData.length; i++) { + const row = Math.floor(i / width); + const col = i % width; + // if data is 0, leave the background + if (imageData[i]) { + // if data is 2, choose spot color, if 1 choose foreground + const pngColor = imageData[i] == 1 ? color : spotcolor; + p.fillRect( + col * opts.scale, + row * opts.scale, + opts.scale, + opts.scale, + pngColor, + ); + } + } + const ret = `data:image/png;base64,${p.getBase64()}`; + Blockies.cache[address] = ret; + return ret; + } + + exports.toDataUrl = toDataUrl; + + Object.defineProperty(exports, '__esModule', { value: true }); +}); + +/** + * Utility class with the single responsibility + * of caching Blockies Data URIs + */ +class Blockies { + static cache = {}; +} diff --git a/packages/design-system-react-native/src/primitives/Blockies/README.md b/packages/design-system-react-native/src/primitives/Blockies/README.md new file mode 100644 index 00000000..cfaac517 --- /dev/null +++ b/packages/design-system-react-native/src/primitives/Blockies/README.md @@ -0,0 +1,114 @@ +# Blockies + +The `Blockies` component generates a unique, consistent, and visually distinct icon based on an Ethereum address. It acts as a wrapper around the `Image` component, using [`toDataUrl`](#) to generate a base64-encoded blocky avatar. + +--- + +## Props + +The `Blockies` component accepts the following props: + +### `address` (Required) + +A string address used as a unique identifier to generate the Blockies avatar. This ensures that the same input always produces the same avatar. + +| TYPE | REQUIRED | DEFAULT | +| :------- | :------- | :------ | +| `string` | Yes | `N/A` | + +--- + +### `size` (Optional) + +Defines the size of the Blockies avatar in pixels. Defaults to `32`. + +| TYPE | REQUIRED | DEFAULT | +| :------- | :------- | :------ | +| `number` | No | `32` | + +--- + +### Other Props + +`Blockies` supports all other props from [`Image`](https://reactnative.dev/docs/image) except `source`, `width`, and `height`. This includes: + +- **`resizeMode`** – Controls how the image is resized within the container. +- **`style`** – Custom styles for the image. +- **`testID`** – Identifier used for testing purposes. +- Any other valid `ImageProps`. + +--- + +## Usage + +### Basic Usage + +```tsx +import React from 'react'; +import Blockies from '@your-library/blockies'; + +const App = () => ; + +export default App; +``` + +--- + +### Custom Size + +```tsx +import React from 'react'; +import Blockies from '@your-library/blockies'; + +const App = () => ; + +export default App; +``` + +--- + +### Applying Custom Styles + +```tsx +import React from 'react'; +import { StyleSheet } from 'react-native'; +import Blockies from '@your-library/blockies'; + +const styles = StyleSheet.create({ + customBlockies: { + borderRadius: 10, + borderWidth: 2, + borderColor: 'blue', + }, +}); + +const App = () => ( + +); + +export default App; +``` + +--- + +## Notes + +- **Unique Avatar Generation:** + `Blockies` ensures each address generates a unique, consistent avatar using [`toDataUrl`](#). +- **Size Customization:** + The `size` prop allows resizing of the avatar while maintaining its resolution. + +- **Extensibility:** + Any additional `Image` props are forwarded to the underlying component for flexibility. + +--- + +## Contributing + +1. Add tests for any new features or modifications. +2. Update this README to reflect any changes in the API. +3. Follow the project's coding guidelines and best practices. + +--- + +For further details, refer to the [React Native documentation](https://reactnative.dev/docs/image). diff --git a/packages/design-system-react-native/src/primitives/Blockies/index.ts b/packages/design-system-react-native/src/primitives/Blockies/index.ts new file mode 100644 index 00000000..1290476a --- /dev/null +++ b/packages/design-system-react-native/src/primitives/Blockies/index.ts @@ -0,0 +1,2 @@ +export { default } from './Blockies'; +export type { BlockiesProps } from './Blockies.types';