The goal of this documentation is to comprehensively define best practice for the Trinity codebase and user interface styling.
- Filenames: Use PascalCase for filenames.
- Component Naming: Use the filename as the component name. Do not use
displayName
for naming components. - Reference Naming: Use PascalCase for React components and camelCase for their instances.
- Props Naming: Always use camelCase for prop names.
// Component names match filenames in PascalCase
import HistoryList from 'components/HistoryList';
// Props names in camelCase
<HistoryList historyItems={items} />
// Reference names in camelCase
const historyList = <HistoryList />;
- Container Naming: Provide the passed-in component's name and the containers name as the displayName on the generated component.
export default function withChartData(ChartComponent) {
class ChartData extends React.Component {
render() {
const chartProps = {};
return <ChartComponent {...chartProps}/>;
}
}
// Set displayName containing wrapped component's name
ChartData.displayName = `withChartData(${ChartComponent.name})`;
return ChartData;
}
Follow the code structure, formatting and overall best practice described in detail in Airbnb React Style Guide and defined in Eslint configuration of the wallet. Run eslint src/desktop/src/
or eslint src/mobile/src/
to check for formatting errors.
All React components and containers should have a general description (purpose, functionality) and the description of it's propTypes in JSDoc format:
import React from 'react'
import PropTypes from 'prop-types'
/**
* General component description.
*/
export default class Button extends React.Component {
static propTypes = {
/** Description of prop "foo". */
foo: PropTypes.number,
/** Description of prop "baz". */
baz: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
}
}
The application follows higher-order component pattern by splitting platform (mobile and desktop) dependent presentational components and shared platform independent container components.
- Presentational components are concerned only with the UI output. They are not bound to the redux state and have no external dependencies (except UI libraries and helpers).
- Presentational components are split between desktop and mobile.
- By default presentational components should be implemented as stateless functional components without internal state. The exception are components dealing with non-persistent visual state (e.g. a password input toggle for hidden/visible characters).
import React, { PureComponent } from 'react';
import withPriceData from 'containers/components/Price';
/**
* Description of the Price component
*/
class Price extends PureComponent {
static propTypes = {
/* Current market price */
price: PropTypes.number.isRequired,
};
render() {
return (
<h1>{this.props.price}</h1>
);
}
}
export default withPriceData(Price);
For a full example, see the components located at /src/desktop/src/ui/
or /src/mobile/src/ui/
.
- Container components deal with the business logic of the application. They are bound to the redux state, deal with component lifecycle (mounting) hooks and data processing.
- Container components should be resuable for desktop and mobile.
- Container component render function holds only the presentational component passed as a prop to the container.
import React from 'react';
import { connect } from 'react-redux';
/**
* Description of the Price container
* @ignore
*/
export default function withPriceData(PriceComponent) {
class PriceData extends React.Component {
static propTypes = {
marketData: PropTypes.object.isRequired,
};
preparePrice(marketData){
//[...]
}
render() {
const priceProps = {
price: this.preparePrice(this.props.marketData)
};
return <PriceComponent {...priceProps} />;
}
}
PriceData.displayName = `withPriceData(${PriceComponent.name})`;
const mapStateToProps = (state) => ({
marketData: state.marketData
});
return connect(mapStateToProps)(PriceData));
}
For a full example, see the containers located at /src/shared/containers/
.
[WIP]
- Expose reducers as the default export.
- Expose handlers as named exports for unit testing.
- Use constants instead of inline strings for action types.
- Always define an
initialState
variable.
import { ActionTypes } from '../actions/account';
const initialState = {};
export default (state = initialState, action) => { };
- Aggregate all booleans associated with components in a single
ui
reducer and based on their purposes further break them down viacombineReducers
. - Aggregate similar state into a single reducer. Since redux allows you to update multiple reducers with a single action, makes it easier to update different parts of the state.
import { ActionTypes } from '../actions/account';
const initialState = {
isUpdatingAccount: false
};
export default (state = initialState, action) => {
case ActionTypes.ACCOUNT_INFO_FETCH_SUCCESS:
return {
...state,
isUpdatingAccount: true
};
default:
return state;
};
- Blacklist reducer in case the state does not need to be persisted to local storage.
[WIP]
- Filenames: Use
*.spec.js
for test filenames. - Structure: Nest test suites to logically structure tests in subsets.
describe('selectors: account', () => {
describe('#getAccountFromState', () => {
describe('when "account" prop is not defined in argument', () => {
it('should return an empty object', () => { });
});
describe('when "account" prop is defined in argument', () => {
it('should return value for "account" prop', () => { });
});
});
});
- Expectations: Do not write unnecessary expectations in a single test. A test should just be a design specification of a certain behavior for how it should work.
- Multiple Concerns: Always test a single concern. If a function has different end results, each of them should be tested separately.
- Test named exports for container components.
- Define a global
getProps
function for injecting props to a component.
const getProps = (overrides) => {
return assign({}, {
pollFor: 'accountInfo',
allPollingServices: ['accountInfo', 'marketData'],
setPollFor: noop
}, overrides);
};
describe('component: Poll', () => {
describe('when renders', () => {
it('should return null', () => {
const props = getProps();
const wrapper = shallow(<Poll {...props} />);
expect(wrapper.type()).toEqual(null);
});
});
});
- Test all instance methods that lead to state updates.
- Test all instance methods that dispatch redux actions.
- Always test initial state for a reducer.
it('should have an initial state', () => {
const initialState = {
firstUse: true,
onboardingComplete: false,
accountInfo: {}
};
expect(reducer(undefined, {})).to.eql(initialState);
});
- Use action creators in tests for returing type and payloads.
it('should assign payload to unconfirmedBundleTails prop in state', () => {
const action = actions.updateUnconfirmedBundleTails({ foo: {} });
const newState = reducer({}, action);
const expectedState = {
unconfirmedBundleTails: { foo: {} },
};
expect(newState).to.eql(expectedState);
});
- Test handlers as separate functions.
describe('#setNetPollIfSuccessful', () => {
let state;
beforeEach(() => {
state = {
allPollingServices: ['marketData', 'price', 'chartData', 'accountInfo', 'promotion'],
pollFor: 'marketData',
};
});
describe('when pollFor value exists in allPollingServices array', () => {
it('should return an object with prop pollFor equals value of next element in allPollingServices array', () => {
expect(setNextPollIfSuccessful(state)).to.eql({ pollFor: 'price' });
});
});
});
[WIP]
All following static assets should be shared between desktop and mobile, stored at /src/shared/assets/
.
To ensure cross-platform compatibility and identical display, all icons should follow these rules:
- SVG icon file should be 64x64px in size without any position transform:
<svg width="64" height="64" viewBox="0 0 64 64"></svg>
- The icon itself should be 64px wide with the height adjusted to the icons ratio.
- Do not use
stroke
- all strokes should be expanded as outlines. - Do not use
fill
- the icon color is defined in the theme declaration.
All animations used in the wallet should be made using Lottie for cross platfrom compatibillity.
A theme consists of a color scheme used by the UI of the mobile and desktop wallets. All themes are located in separate js files at /src/shared/themes/
.
The color scheme consists of following mandatory sets and colors:
body
,bar
- base color sets used for the application and secondary navigation. Both sets have three colors required:color
- required, the body color. Used for text, icons.bg
- required, the background color.alt
- required, accent color used for info block background, dividers and borders.
primary
,secondary
,positive
,negative
,highlight
,extra
- accent color sets used by their name meaning. Possible colors to be defined:color
- required; the accent color itself.hover
- required; the accent colors hover state.body
- optional; used for content when the accent color is it's background. Defaults tobody.color
.
input
- colors for input elements:color
- required; text color of input elementsbg
- required; background color of input elementsalt
- optional; used for icons and links inside input elements. Defaults toinput.color
.
label
- input element label color:color
- required; label colorhover
- required; active input element label color
chart.color
- required; chart line color.
When updating or creating a new UI component or theme, it should be checked for contrast and readability against the Styleguide containing all main UI elements.
To launch the styleguide, inside wallets root directory run:
yarn run start:styleguide