Standardize Redux Actions / Reducers (#95)

* Convert Swap to consistent style

* Generate wallet reducer cleanup.

* Confirm empty action in swap reducer

* Union types. Fix gen wallet collision

* Fix not using all actions in reducer. Added reducer state for is fetching from bity. Added todo to make that a loader.

* Readme instructions.

* Remove common action constants.

* Bring all actions and reducers inline with readme instructions.

* Readme fixes

* address comments
This commit is contained in:
William O'Beirne 2017-07-27 13:05:09 -04:00 committed by Daniel Ternyak
parent e8ad2ce958
commit 1aad9d1c21
32 changed files with 542 additions and 448 deletions

View File

@ -51,6 +51,91 @@ docker-compose up
The following are guides for developers to follow for writing compliant code.
### Redux and Actions
Each reducer has one file in `reducers/[namespace].js` that contains the reducer
and initial state, one file in `actions/[namespace].js` that contains the action
creators and their return types, and optionally one file in
`sagas/[namespace].js` that handles action side effects using
[`redux-saga`](https://github.com/redux-saga/redux-saga).
The files should be laid out as follows:
#### Reducer
* State should be explicitly defined and exported
* Initial state should match state flow typing, define every key
* Reducer function should handle all cases for actions. If state does not change
as a result of an action (Because it merely kicks off side-effects in saga) then
define the case above default, and have it fall through.
```js
// @flow
import type { NamespaceAction } from "actions/namespace";
export type State = { /* Flowtype definition for state object */ };
export const INITIAL_STATE: State = { /* Initial state shape */ };
export function namespace(
state: State = INITIAL_STATE,
action: NamespaceAction
): State {
switch (action.type) {
case 'NAMESPACE_NAME_OF_ACTION':
return {
...state,
// Alterations to state
};
case 'NAMESPACE_NAME_OF_SAGA_ACTION':
default:
// Ensures every action was handled in reducer
// Unhandled actions should just fall into default
(action: empty);
return state;
}
}
```
#### Actions
* Define each action object type beside the action creator
* Export a union of all of the action types for use by the reducer
```js
/*** Name of action ***/
export type NameOfActionAction = {
type: 'NAMESPACE_NAME_OF_ACTION',
/* Rest of the action object shape */
};
export function nameOfAction(): NameOfActionAction {
return {
type: 'NAMESPACE_NAME_OF_ACTION',
/* Rest of the action object */
};
};
/*** Action Union ***/
export type NamespaceAction =
| ActionOneAction
| ActionTwoAction
| ActionThreeAction;
```
#### Action Constants
Action constants are not used thanks to flow type checking. To avoid typos, we
use `(action: empty)` in the default case which assures every case is accounted
for. If you need to use another reducer's action, import that action type into
your reducer, and create a new action union of your actions, and the other
action types used.
### Styling
Legacy styles are housed under `common/assets/styles` and written with LESS.

View File

@ -1,4 +0,0 @@
export const LOCATION_CHANGE = '@@router/LOCATION_CHANGE';
// I'm not sure, but I guess that if you use redux-devtools extension your APP_INIT should be look like:
// export const APP_INIT = '@@INIT'
export const APP_INIT = '@@redux/INIT';

View File

@ -1,30 +1,25 @@
// @flow
export type ChangeNodeAction = {
type: 'CONFIG_NODE_CHANGE',
// FIXME $keyof?
value: string
};
/*** Change Language ***/
export type ChangeLanguageAction = {
type: 'CONFIG_LANGUAGE_CHANGE',
value: string
};
export type ChangeGasPriceAction = {
type: 'CONFIG_GAS_PRICE',
value: number
}
export type ConfigAction = ChangeNodeAction | ChangeLanguageAction;
export function changeLanguage(sign: string) {
export function changeLanguage(sign: string): ChangeLanguageAction {
return {
type: 'CONFIG_LANGUAGE_CHANGE',
value: sign
};
}
/*** Change Node ***/
export type ChangeNodeAction = {
type: 'CONFIG_NODE_CHANGE',
// FIXME $keyof?
value: string
};
export function changeNode(value: string): ChangeNodeAction {
return {
type: 'CONFIG_NODE_CHANGE',
@ -32,10 +27,21 @@ export function changeNode(value: string): ChangeNodeAction {
};
}
/*** Change gas price ***/
export type ChangeGasPriceAction = {
type: 'CONFIG_GAS_PRICE',
value: number
};
export function changeGasPrice(value: number): ChangeGasPriceAction {
return {
type: 'CONFIG_GAS_PRICE',
value
}
};
}
/*** Union Type ***/
export type ConfigAction =
| ChangeNodeAction
| ChangeLanguageAction
| ChangeGasPriceAction;

View File

@ -1,18 +1,12 @@
// @flow
import type { Token } from 'config/data';
/*** Add custom token ***/
export type AddCustomTokenAction = {
type: 'CUSTOM_TOKEN_ADD',
payload: Token
};
export type RemoveCustomTokenAction = {
type: 'CUSTOM_TOKEN_REMOVE',
payload: string
};
export type CustomTokenAction = AddCustomTokenAction | RemoveCustomTokenAction;
export function addCustomToken(payload: Token): AddCustomTokenAction {
return {
type: 'CUSTOM_TOKEN_ADD',
@ -20,9 +14,18 @@ export function addCustomToken(payload: Token): AddCustomTokenAction {
};
}
/*** Remove Custom Token ***/
export type RemoveCustomTokenAction = {
type: 'CUSTOM_TOKEN_REMOVE',
payload: string
};
export function removeCustomToken(payload: string): RemoveCustomTokenAction {
return {
type: 'CUSTOM_TOKEN_REMOVE',
payload
};
}
/*** Union Type ***/
export type CustomTokenAction = AddCustomTokenAction | RemoveCustomTokenAction;

View File

@ -1,20 +1,11 @@
// @flow
/*** Resolve ENS name ***/
export type ResolveEnsNameAction = {
type: 'ENS_RESOLVE',
payload: string
};
export type CacheEnsAddressAction = {
type: 'ENS_CACHE',
payload: {
ensName: string,
address: string
}
};
export type EnsAction = ResolveEnsNameAction | CacheEnsAddressAction;
export function resolveEnsName(name: string): ResolveEnsNameAction {
return {
type: 'ENS_RESOLVE',
@ -22,6 +13,15 @@ export function resolveEnsName(name: string): ResolveEnsNameAction {
};
}
/*** Cache ENS address ***/
export type CacheEnsAddressAction = {
type: 'ENS_CACHE',
payload: {
ensName: string,
address: string
}
};
export function cacheEnsAddress(
ensName: string,
address: string
@ -34,3 +34,6 @@ export function cacheEnsAddress(
}
};
}
/*** Union Type ***/
export type EnsAction = ResolveEnsNameAction | CacheEnsAddressAction;

View File

@ -1,33 +1,38 @@
// @flow
import {
GENERATE_WALLET_CONFIRM_CONTINUE_TO_PAPER,
GENERATE_WALLET_FILE,
GENERATE_WALLET_DOWNLOAD_FILE,
GENERATE_WALLET_SHOW_PASSWORD,
RESET_GENERATE_WALLET
} from 'actions/generateWalletConstants';
import { PrivKeyWallet } from 'libs/wallet';
export const showPasswordGenerateWallet = () => {
return { type: GENERATE_WALLET_SHOW_PASSWORD };
/*** Generate Wallet File ***/
export type GenerateNewWalletAction = {
type: 'GENERATE_WALLET_GENERATE_WALLET',
wallet: PrivKeyWallet,
password: string
};
export const generateUTCGenerateWallet = (password: string) => {
export function generateNewWallet(password: string): GenerateNewWalletAction {
return {
type: GENERATE_WALLET_FILE,
type: 'GENERATE_WALLET_GENERATE_WALLET',
wallet: PrivKeyWallet.generate(),
password
};
}
/*** Confirm Continue To Paper ***/
export type ContinueToPaperAction = {
type: 'GENERATE_WALLET_CONTINUE_TO_PAPER'
};
export const downloadUTCGenerateWallet = () => {
return { type: GENERATE_WALLET_DOWNLOAD_FILE };
export function continueToPaper(): ContinueToPaperAction {
return { type: 'GENERATE_WALLET_CONTINUE_TO_PAPER' };
}
/*** Reset Generate Wallet ***/
export type ResetGenerateWalletAction = {
type: 'GENERATE_WALLET_RESET'
};
export const confirmContinueToPaperGenerateWallet = () => {
return { type: GENERATE_WALLET_CONFIRM_CONTINUE_TO_PAPER };
};
export function resetGenerateWallet(): ResetGenerateWalletAction {
return { type: 'GENERATE_WALLET_RESET' };
}
export const resetGenerateWallet = () => {
return { type: RESET_GENERATE_WALLET };
};
/*** Action Union ***/
export type GenerateWalletAction = GenerateWalletAction;

View File

@ -1,6 +0,0 @@
export const GENERATE_WALLET_SHOW_PASSWORD = 'GENERATE_WALLET_SHOW_PASSWORD';
export const GENERATE_WALLET_FILE = 'GENERATE_WALLET_FILE';
export const GENERATE_WALLET_DOWNLOAD_FILE = 'GENERATE_WALLET_DOWNLOAD_FILE';
export const GENERATE_WALLET_CONFIRM_CONTINUE_TO_PAPER =
'GENERATE_WALLET_CONFIRM_CONTINUE_TO_PAPER';
export const RESET_GENERATE_WALLET = 'RESET_GENERATE_WALLET';

View File

@ -1,5 +1,6 @@
// @flow
/*** Shared types ***/
export type NOTIFICATION_LEVEL = 'danger' | 'warning' | 'success' | 'info';
export type Notification = {
@ -8,20 +9,12 @@ export type Notification = {
duration?: number
};
/*** Show Notification ***/
export type ShowNotificationAction = {
type: 'SHOW_NOTIFICATION',
payload: Notification
};
export type CloseNotificationAction = {
type: 'CLOSE_NOTIFICATION',
payload: Notification
};
export type NotificationsAction =
| ShowNotificationAction
| CloseNotificationAction;
export function showNotification(
level: NOTIFICATION_LEVEL = 'info',
msg: string,
@ -37,6 +30,12 @@ export function showNotification(
};
}
/*** Close notification ***/
export type CloseNotificationAction = {
type: 'CLOSE_NOTIFICATION',
payload: Notification
};
export function closeNotification(
notification: Notification
): CloseNotificationAction {
@ -45,3 +44,8 @@ export function closeNotification(
payload: notification
};
}
/*** Union Type ***/
export type NotificationsAction =
| ShowNotificationAction
| CloseNotificationAction;

View File

@ -1,15 +1,17 @@
// @flow
/*** Set rates ***/
export type SetRatesAction = {
type: 'RATES_SET',
payload: { [string]: number }
};
export type RatesAction = SetRatesAction;
export function setRates(payload: { [string]: number }): SetRatesAction {
return {
type: 'RATES_SET',
payload
};
}
/*** Union Type ***/
export type RatesAction = SetRatesAction;

View File

@ -1,104 +1,163 @@
// @flow
import {
SWAP_DESTINATION_AMOUNT,
SWAP_DESTINATION_KIND,
SWAP_ORIGIN_AMOUNT,
SWAP_ORIGIN_KIND,
SWAP_UPDATE_BITY_RATES,
SWAP_DESTINATION_ADDRESS,
SWAP_RESTART,
SWAP_LOAD_BITY_RATES,
SWAP_STOP_LOAD_BITY_RATES,
SWAP_STEP,
SWAP_REFERENCE_NUMBER
} from './swapConstants';
import * as swapTypes from './swapTypes';
/*** Change Step ***/
export type ChangeStepSwapAction = {
type: 'SWAP_STEP',
value: number
};
export function changeStepSwap(value: number): swapTypes.ChangeStepSwapAction {
export function changeStepSwap(value: number): ChangeStepSwapAction {
return {
type: SWAP_STEP,
type: 'SWAP_STEP',
value
};
}
export function referenceNumberSwap(
/*** Change Reference Number ***/
export type ReferenceNumberSwapAction = {
type: 'SWAP_REFERENCE_NUMBER',
value: string
): swapTypes.ReferenceNumberSwapAction {
};
export function referenceNumberSwap(value: string): ReferenceNumberSwapAction {
return {
type: SWAP_REFERENCE_NUMBER,
type: 'SWAP_REFERENCE_NUMBER',
value
};
}
export const originKindSwap = (
/*** Change Origin Kind ***/
export type OriginKindSwapAction = {
type: 'SWAP_ORIGIN_KIND',
value: string
): swapTypes.OriginKindSwapAction => {
return {
type: SWAP_ORIGIN_KIND,
value
};
};
export const destinationKindSwap = (
export function originKindSwap(value: string): OriginKindSwapAction {
return {
type: 'SWAP_ORIGIN_KIND',
value
};
}
/*** Change Destination Kind ***/
export type DestinationKindSwapAction = {
type: 'SWAP_DESTINATION_KIND',
value: string
): swapTypes.DestinationKindSwapAction => {
return {
type: SWAP_DESTINATION_KIND,
value
};
};
export const originAmountSwap = (
export function destinationKindSwap(value: string): DestinationKindSwapAction {
return {
type: 'SWAP_DESTINATION_KIND',
value
};
}
/*** Change Origin Amount ***/
export type OriginAmountSwapAction = {
type: 'SWAP_ORIGIN_AMOUNT',
value: ?number
): swapTypes.OriginAmountSwapAction => {
return {
type: SWAP_ORIGIN_AMOUNT,
value
};
};
export const destinationAmountSwap = (
export function originAmountSwap(value: ?number): OriginAmountSwapAction {
return {
type: 'SWAP_ORIGIN_AMOUNT',
value
};
}
/*** Change Destination Amount ***/
export type DestinationAmountSwapAction = {
type: 'SWAP_DESTINATION_AMOUNT',
value: ?number
): swapTypes.DestinationAmountSwapAction => {
return {
type: SWAP_DESTINATION_AMOUNT,
value
};
};
export const updateBityRatesSwap = (
value: swapTypes.Pairs
): swapTypes.BityRatesSwapAction => {
export function destinationAmountSwap(
value: ?number
): DestinationAmountSwapAction {
return {
type: SWAP_UPDATE_BITY_RATES,
type: 'SWAP_DESTINATION_AMOUNT',
value
};
}
/*** Update Bity Rates ***/
export type Pairs = {
ETHBTC: number,
ETHREP: number,
BTCETH: number,
BTCREP: number
};
export const destinationAddressSwap = (
export type BityRatesSwapAction = {
type: 'SWAP_UPDATE_BITY_RATES',
value: Pairs
};
export function updateBityRatesSwap(value: Pairs): BityRatesSwapAction {
return {
type: 'SWAP_UPDATE_BITY_RATES',
value
};
}
/*** Change Destination Address ***/
export type DestinationAddressSwapAction = {
type: 'SWAP_DESTINATION_ADDRESS',
value: ?string
): swapTypes.DestinationAddressSwapAction => {
};
export function destinationAddressSwap(
value: ?string
): DestinationAddressSwapAction {
return {
type: SWAP_DESTINATION_ADDRESS,
type: 'SWAP_DESTINATION_ADDRESS',
value
};
}
/*** Restart ***/
export type RestartSwapAction = {
type: 'SWAP_RESTART'
};
export const restartSwap = (): swapTypes.RestartSwapAction => {
export function restartSwap(): RestartSwapAction {
return {
type: SWAP_RESTART
type: 'SWAP_RESTART'
};
}
/*** Load Bity Rates ***/
export type LoadBityRatesSwapAction = {
type: 'SWAP_LOAD_BITY_RATES'
};
export const loadBityRatesSwap = (): swapTypes.LoadBityRatesSwapAction => {
export function loadBityRatesSwap(): LoadBityRatesSwapAction {
return {
type: SWAP_LOAD_BITY_RATES
type: 'SWAP_LOAD_BITY_RATES'
};
}
/*** Stop Loading Bity Rates ***/
export type StopLoadBityRatesSwapAction = {
type: 'SWAP_STOP_LOAD_BITY_RATES'
};
export const stopLoadBityRatesSwap = (): swapTypes.StopLoadBityRatesSwapAction => {
export function stopLoadBityRatesSwap(): StopLoadBityRatesSwapAction {
return {
type: SWAP_STOP_LOAD_BITY_RATES
type: 'SWAP_STOP_LOAD_BITY_RATES'
};
};
}
/*** Action Type Union ***/
export type SwapAction =
| ChangeStepSwapAction
| ReferenceNumberSwapAction
| OriginKindSwapAction
| DestinationKindSwapAction
| OriginAmountSwapAction
| DestinationAmountSwapAction
| BityRatesSwapAction
| DestinationAddressSwapAction
| RestartSwapAction
| LoadBityRatesSwapAction
| StopLoadBityRatesSwapAction;

View File

@ -1,11 +0,0 @@
export const SWAP_ORIGIN_KIND = 'SWAP_ORIGIN_KIND';
export const SWAP_DESTINATION_KIND = 'SWAP_DESTINATION_KIND';
export const SWAP_ORIGIN_AMOUNT = 'SWAP_ORIGIN_AMOUNT';
export const SWAP_DESTINATION_AMOUNT = 'SWAP_DESTINATION_AMOUNT';
export const SWAP_UPDATE_BITY_RATES = 'SWAP_UPDATE_BITY_RATES';
export const SWAP_DESTINATION_ADDRESS = 'SWAP_DESTINATION_ADDRESS';
export const SWAP_RESTART = 'SWAP_RESTART';
export const SWAP_LOAD_BITY_RATES = 'SWAP_LOAD_BITY_RATES';
export const SWAP_STOP_LOAD_BITY_RATES = 'SWAP_STOP_LOAD_BITY_RATES';
export const SWAP_STEP = 'SWAP_STEP';
export const SWAP_REFERENCE_NUMBER = 'SWAP_REFERENCE_NUMBER';

View File

@ -1,66 +0,0 @@
import {
SWAP_DESTINATION_AMOUNT,
SWAP_DESTINATION_KIND,
SWAP_ORIGIN_AMOUNT,
SWAP_ORIGIN_KIND,
SWAP_UPDATE_BITY_RATES,
SWAP_DESTINATION_ADDRESS,
SWAP_RESTART,
SWAP_LOAD_BITY_RATES,
SWAP_STOP_LOAD_BITY_RATES,
SWAP_STEP,
SWAP_REFERENCE_NUMBER
} from './swapConstants';
export type Pairs = {
ETHBTC: number,
ETHREP: number,
BTCETH: number,
BTCREP: number
};
export type ReferenceNumberSwapAction = {
type: SWAP_REFERENCE_NUMBER,
value: string
};
export type OriginKindSwapAction = {
type: SWAP_ORIGIN_KIND,
value: string
};
export type DestinationKindSwapAction = {
type: SWAP_DESTINATION_KIND,
value: string
};
export type OriginAmountSwapAction = {
type: SWAP_ORIGIN_AMOUNT,
value: ?number
};
export type DestinationAmountSwapAction = {
type: SWAP_DESTINATION_AMOUNT,
value: ?number
};
export type BityRatesSwapAction = {
type: SWAP_UPDATE_BITY_RATES,
value: Pairs
};
export type DestinationAddressSwapAction = {
type: SWAP_DESTINATION_ADDRESS,
value: ?number
};
export type RestartSwapAction = {
type: SWAP_RESTART
};
export type LoadBityRatesSwapAction = {
type: SWAP_LOAD_BITY_RATES
};
export type ChangeStepSwapAction = {
type: SWAP_STEP,
value: number
};
export type StopLoadBityRatesSwapAction = {
type: SWAP_STOP_LOAD_BITY_RATES
};

View File

@ -2,6 +2,7 @@
import BaseWallet from 'libs/wallet/base';
import Big from 'big.js';
/*** Unlock Private Key ***/
export type PrivateKeyUnlockParams = {
key: string,
password: string
@ -12,29 +13,6 @@ export type UnlockPrivateKeyAction = {
payload: PrivateKeyUnlockParams
};
export type SetWalletAction = {
type: 'WALLET_SET',
payload: BaseWallet
};
export type SetBalanceAction = {
type: 'WALLET_SET_BALANCE',
payload: Big
};
export type SetTokenBalancesAction = {
type: 'WALLET_SET_TOKEN_BALANCES',
payload: {
[string]: Big
}
};
export type WalletAction =
| UnlockPrivateKeyAction
| SetWalletAction
| SetBalanceAction
| SetTokenBalancesAction;
export function unlockPrivateKey(
value: PrivateKeyUnlockParams
): UnlockPrivateKeyAction {
@ -44,6 +22,12 @@ export function unlockPrivateKey(
};
}
/*** Set Wallet ***/
export type SetWalletAction = {
type: 'WALLET_SET',
payload: BaseWallet
};
export function setWallet(value: BaseWallet): SetWalletAction {
return {
type: 'WALLET_SET',
@ -51,6 +35,12 @@ export function setWallet(value: BaseWallet): SetWalletAction {
};
}
/*** Set Balance ***/
export type SetBalanceAction = {
type: 'WALLET_SET_BALANCE',
payload: Big
};
export function setBalance(value: Big): SetBalanceAction {
return {
type: 'WALLET_SET_BALANCE',
@ -58,6 +48,14 @@ export function setBalance(value: Big): SetBalanceAction {
};
}
/*** Set Token Balance ***/
export type SetTokenBalancesAction = {
type: 'WALLET_SET_TOKEN_BALANCES',
payload: {
[string]: Big
}
};
export function setTokenBalances(payload: {
[string]: Big
}): SetTokenBalancesAction {
@ -66,3 +64,10 @@ export function setTokenBalances(payload: {
payload
};
}
/*** Union Type ***/
export type WalletAction =
| UnlockPrivateKeyAction
| SetWalletAction
| SetBalanceAction
| SetTokenBalancesAction;

View File

@ -1,6 +1,5 @@
// @flow
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import translate from 'translations';
import type PrivKeyWallet from 'libs/wallet/privkey';
import { makeBlob } from 'utils/blob';
@ -9,23 +8,16 @@ import { getV3Filename } from 'libs/keystore';
type Props = {
wallet: PrivKeyWallet,
password: string,
hasDownloadedWalletFile: boolean,
downloadUTCGenerateWallet: () => any,
confirmContinueToPaperGenerateWallet: () => any
continueToPaper: Function
};
export default class DownloadWallet extends Component {
props: Props;
keystore: Object;
static propTypes = {
// Store state
wallet: PropTypes.object.isRequired,
password: PropTypes.string.isRequired,
hasDownloadedWalletFile: PropTypes.bool,
// Actions
downloadUTCGenerateWallet: PropTypes.func,
confirmContinueToPaperGenerateWallet: PropTypes.func
state = {
hasDownloadedWallet: false
};
componentWillMount() {
this.keystore = this.props.wallet.toKeystore(this.props.password);
}
@ -35,12 +27,18 @@ export default class DownloadWallet extends Component {
}
}
_markDownloaded = () => {
this.setState({ hasDownloadedWallet: true });
};
_handleContinue = () => {
if (this.state.hasDownloadedWallet) {
this.props.continueToPaper();
}
};
render() {
const {
hasDownloadedWalletFile,
downloadUTCGenerateWallet,
confirmContinueToPaperGenerateWallet
} = this.props;
const { hasDownloadedWallet } = this.state;
return (
<div>
@ -68,7 +66,7 @@ export default class DownloadWallet extends Component {
aria-describedby="x_KeystoreDesc"
download={this.getFilename()}
href={this.getBlob()}
onClick={downloadUTCGenerateWallet}
onClick={this._markDownloaded}
>
{translate('x_Download')}
</a>
@ -95,10 +93,10 @@ export default class DownloadWallet extends Component {
<br />
<a
role="button"
className={`btn btn-info ${hasDownloadedWalletFile
className={`btn btn-info ${hasDownloadedWallet
? ''
: 'disabled'}`}
onClick={confirmContinueToPaperGenerateWallet}
onClick={this._handleContinue}
>
I understand. Continue.
</a>

View File

@ -1,6 +1,5 @@
// @flow
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import translate from 'translations';
import PasswordInput from './PasswordInput';
@ -14,32 +13,33 @@ const minLength = min => value => {
const minLength9 = minLength(9);
const required = value => (value ? undefined : 'Required');
type Props = {
walletPasswordForm: Object,
showWalletPassword: Function,
generateNewWallet: Function
};
class EnterPassword extends Component {
static propTypes = {
// Store state
generateWalletPassword: PropTypes.object,
showPassword: PropTypes.bool,
// Actions
showPasswordGenerateWallet: PropTypes.func,
generateUTCGenerateWallet: PropTypes.func
};
props: Props;
state = {
fileName: null,
blobURI: null
blobURI: null,
isPasswordVisible: false
};
onClickGenerateFile = () => {
const form = this.props.generateWalletPassword;
this.props.generateUTCGenerateWallet(form.values.password);
_onClickGenerateFile = () => {
const form = this.props.walletPasswordForm;
this.props.generateNewWallet(form.values.password);
};
_togglePassword = () => {
this.setState({ isPasswordVisible: !this.state.isPasswordVisible });
};
render() {
const {
generateWalletPassword,
showPassword,
showPasswordGenerateWallet
} = this.props;
const { walletPasswordForm } = this.props;
const { isPasswordVisible } = this.state;
return (
<div>
@ -54,17 +54,15 @@ class EnterPassword extends Component {
<Field
validate={[required, minLength9]}
component={PasswordInput}
showPassword={showPassword}
showPasswordGenerateWallet={showPasswordGenerateWallet}
isPasswordVisible={isPasswordVisible}
togglePassword={this._togglePassword}
name="password"
type="text"
/>
<br />
<button
onClick={this.onClickGenerateFile}
disabled={
generateWalletPassword ? generateWalletPassword.syncErrors : true
}
onClick={this._onClickGenerateFile}
disabled={walletPasswordForm ? walletPasswordForm.syncErrors : true}
className="btn btn-primary btn-block"
>
{translate('NAV_GenerateWallet')}
@ -76,5 +74,5 @@ class EnterPassword extends Component {
}
export default reduxForm({
form: 'generateWalletPassword' // a unique name for this form
form: 'walletPasswordForm' // a unique name for this form
})(EnterPassword);

View File

@ -1,37 +1,33 @@
// @flow
import React, { Component } from 'react';
import PropTypes from 'prop-types';
type Props = {
togglePassword: Function,
isPasswordVisible: ?boolean,
input: Object,
meta: Object
};
export default class PasswordInput extends Component {
constructor(props) {
super(props);
}
static propTypes = {
showPasswordGenerateWallet: PropTypes.func,
showPassword: PropTypes.bool,
input: PropTypes.object,
meta: PropTypes.object
};
props: Props;
render() {
const { input, meta, isPasswordVisible, togglePassword } = this.props;
return (
<div>
<div>
<div className="input-group" style={{ width: '100%' }}>
<input
{...this.props.input}
{...input}
name="password"
className={
this.props.meta.error
? 'form-control is-invalid'
: 'form-control'
}
type={this.props.showPassword ? 'text' : 'password'}
className={`form-control ${meta.error ? 'is-invalid' : ''}`}
type={isPasswordVisible ? 'text' : 'password'}
placeholder="Do NOT forget to save this!"
aria-label="Enter a strong password (at least 9 characters)"
/>
<span
onClick={this.props.showPasswordGenerateWallet}
onClick={togglePassword}
aria-label="make password visible"
role="button"
className="input-group-addon eye"

View File

@ -2,7 +2,12 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as generateWalletActions from 'actions/generateWallet';
import PropTypes from 'prop-types';
import type {
GenerateNewWalletAction,
ContinueToPaperAction,
ResetGenerateWalletAction
} from 'actions/generateWallet';
import EnterPassword from './components/EnterPassword';
import DownloadWallet from './components/DownloadWallet';
import PaperWallet from './components/PaperWallet';
@ -10,28 +15,19 @@ import type PrivKeyWallet from 'libs/wallet/privkey';
import type { State } from 'reducers';
type Props = {
// FIXME union actual steps
activeStep: string,
// Redux state
activeStep: string, // FIXME union actual steps
password: string,
hasDownloadedWalletFile: boolean,
wallet: ?PrivKeyWallet
} & typeof generateWalletActions;
wallet: ?PrivKeyWallet,
walletPasswordForm: Object,
// Actions
generateNewWallet: (pw: string) => GenerateNewWalletAction,
continueToPaper: () => ContinueToPaperAction,
resetGenerateWallet: () => ResetGenerateWalletAction
};
class GenerateWallet extends Component {
props: Props;
static propTypes = {
// Store state
activeStep: PropTypes.string,
wallet: PropTypes.object,
password: PropTypes.string,
hasDownloadedWalletFile: PropTypes.bool,
// Actions
showPasswordGenerateWallet: PropTypes.func,
generateUTCGenerateWallet: PropTypes.func,
downloadUTCGenerateWallet: PropTypes.func,
confirmContinueToPaperGenerateWallet: PropTypes.func,
resetGenerateWallet: PropTypes.func
};
componentWillUnmount() {
this.props.resetGenerateWallet();
@ -43,20 +39,21 @@ class GenerateWallet extends Component {
switch (activeStep) {
case 'password':
content = <EnterPassword {...this.props} />;
content = (
<EnterPassword
walletPasswordForm={this.props.walletPasswordForm}
generateNewWallet={this.props.generateNewWallet}
/>
);
break;
case 'download':
if (wallet) {
content = (
<DownloadWallet
hasDownloadedWalletFile={this.props.hasDownloadedWalletFile}
wallet={wallet}
password={password}
downloadUTCGenerateWallet={this.props.downloadUTCGenerateWallet}
confirmContinueToPaperGenerateWallet={
this.props.confirmContinueToPaperGenerateWallet
}
continueToPaper={this.props.continueToPaper}
/>
);
}
@ -91,12 +88,10 @@ class GenerateWallet extends Component {
function mapStateToProps(state: State) {
return {
generateWalletPassword: state.form.generateWalletPassword,
walletPasswordForm: state.form.walletPasswordForm,
activeStep: state.generateWallet.activeStep,
password: state.generateWallet.password,
hasDownloadedWalletFile: state.generateWallet.hasDownloadedWalletFile,
wallet: state.generateWallet.wallet,
walletFile: state.generateWallet.walletFile
wallet: state.generateWallet.wallet
};
}

View File

@ -1,8 +1,14 @@
import React, { Component } from 'react';
import translate from 'translations';
import { combineAndUpper } from 'utils/formatters';
import * as swapTypes from 'actions/swapTypes';
import SimpleDropDown from 'components/ui/SimpleDropdown';
import type {
OriginKindSwapAction,
DestinationKindSwapAction,
OriginAmountSwapAction,
DestinationAmountSwapAction,
ChangeStepSwapAction
} from 'actions/swap';
export type StateProps = {
bityRates: {},
@ -15,13 +21,11 @@ export type StateProps = {
};
export type ActionProps = {
originKindSwap: (value: string) => swapTypes.OriginKindSwapAction,
destinationKindSwap: (value: string) => swapTypes.DestinationKindSwapAction,
originAmountSwap: (value: ?number) => swapTypes.OriginAmountSwapAction,
destinationAmountSwap: (
value: ?number
) => swapTypes.DestinationAmountSwapAction,
changeStepSwap: () => swapTypes.ChangeStepSwapAction
originKindSwap: (value: string) => OriginKindSwapAction,
destinationKindSwap: (value: string) => DestinationKindSwapAction,
originAmountSwap: (value: ?number) => OriginAmountSwapAction,
destinationAmountSwap: (value: ?number) => DestinationAmountSwapAction,
changeStepSwap: () => ChangeStepSwapAction
};
export default class CurrencySwap extends Component {

View File

@ -2,7 +2,7 @@
import React, { Component } from 'react';
import translate from 'translations';
import { toFixedIfLarger } from 'utils/formatters';
import { Pairs } from 'actions/swapTypes';
import type { Pairs } from 'actions/swap';
import { bityReferralURL } from 'config/data';
import bityLogoWhite from 'assets/images/logo-bity-white.svg';

View File

@ -1,6 +1,11 @@
// @flow
import React, { Component } from 'react';
import * as swapTypes from 'actions/swapTypes';
import type {
DestinationAddressSwapAction,
ChangeStepSwapAction,
StopLoadBityRatesSwapAction,
ReferenceNumberSwapAction
} from 'actions/swap';
import { donationAddressMap } from 'config/data';
import { isValidBTCAddress, isValidETHAddress } from 'libs/validators';
import translate from 'translations';
@ -11,11 +16,10 @@ export type StateProps = {
};
export type ActionProps = {
destinationAddressSwap: (
value: ?string
) => swapTypes.DestinationAddressSwapAction,
changeStepSwap: (value: number) => swapTypes.ChangeStepSwapAction,
stopLoadBityRatesSwap: () => swapTypes.StopLoadBityRatesSwapAction
destinationAddressSwap: (value: ?string) => DestinationAddressSwapAction,
changeStepSwap: (value: number) => ChangeStepSwapAction,
stopLoadBityRatesSwap: () => StopLoadBityRatesSwapAction,
referenceNumberSwap: (value: string) => ReferenceNumberSwapAction
};
export default class ReceivingAddress extends Component {

View File

@ -2,7 +2,7 @@
import React, { Component } from 'react';
import { toFixedIfLarger } from 'utils/formatters';
import translate from 'translations';
import * as swapTypes from 'actions/swapTypes';
import type { RestartSwapAction } from 'actions/swap';
import bityLogo from 'assets/images/logo-bity.svg';
import { bityReferralURL } from 'config/data';
@ -16,7 +16,7 @@ export type StateProps = {
};
export type ActionProps = {
restartSwap: () => swapTypes.RestartSwapAction
restartSwap: () => RestartSwapAction
};
export default class SwapInfoHeader extends Component {
@ -126,10 +126,7 @@ export default class SwapInfoHeader extends Component {
{/*Your rate*/}
<div className={this.computedClass()}>
<h4>
{` ${toFixedIfLarger(
this.computedOriginDestinationRatio(),
6
)} ${originKind}/${destinationKind} `}
{` ${this.computedOriginDestinationRatio()} ${originKind}/${destinationKind} `}
</h4>
<p>
{translate('SWAP_your_rate')}

View File

@ -1,7 +1,17 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as swapActions from 'actions/swap';
import * as swapTypes from 'actions/swapTypes';
import type {
ChangeStepSwapAction,
OriginKindSwapAction,
DestinationKindSwapAction,
OriginAmountSwapAction,
DestinationAmountSwapAction,
LoadBityRatesSwapAction,
DestinationAddressSwapAction,
RestartSwapAction,
StopLoadBityRatesSwapAction
} from 'actions/swap';
import CurrencySwap from './components/CurrencySwap';
import CurrentRates from './components/CurrentRates';
import ReceivingAddress from './components/ReceivingAddress';
@ -18,6 +28,7 @@ type ReduxStateProps = {
bityRates: boolean,
originAmount: ?number,
destinationAmount: ?number,
isFetchingRates: boolean,
// PART 3
referenceNumber: string,
timeRemaining: string,
@ -27,19 +38,15 @@ type ReduxStateProps = {
};
type ReduxActionProps = {
changeStepSwap: (value: number) => swapTypes.ChangeStepSwapAction,
originKindSwap: (value: string) => swapTypes.OriginKindSwapAction,
destinationKindSwap: (value: string) => swapTypes.DestinationKindSwapAction,
originAmountSwap: (value: ?number) => swapTypes.OriginAmountSwapAction,
destinationAmountSwap: (
value: ?number
) => swapTypes.DestinationAmountSwapAction,
loadBityRatesSwap: () => swapTypes.LoadBityRatesSwapAction,
destinationAddressSwap: (
value: ?string
) => swapTypes.DestinationAddressSwapAction,
restartSwap: () => swapTypes.RestartSwapAction,
stopLoadBityRatesSwap: () => swapTypes.StopLoadBityRatesSwapAction,
changeStepSwap: (value: number) => ChangeStepSwapAction,
originKindSwap: (value: string) => OriginKindSwapAction,
destinationKindSwap: (value: string) => DestinationKindSwapAction,
originAmountSwap: (value: ?number) => OriginAmountSwapAction,
destinationAmountSwap: (value: ?number) => DestinationAmountSwapAction,
loadBityRatesSwap: () => LoadBityRatesSwapAction,
destinationAddressSwap: (value: ?string) => DestinationAddressSwapAction,
restartSwap: () => RestartSwapAction,
stopLoadBityRatesSwap: () => StopLoadBityRatesSwapAction,
// PART 3 (IGNORE FOR NOW)
referenceNumberSwap: typeof swapActions.referenceNumberSwap
};
@ -48,6 +55,7 @@ class Swap extends Component {
props: ReduxActionProps & ReduxStateProps;
componentDidMount() {
// TODO: Use `isFetchingRates` to show a loader
this.props.loadBityRatesSwap();
}
@ -158,7 +166,8 @@ function mapStateToProps(state) {
timeRemaining: state.swap.timeRemaining,
numberOfConfirmations: state.swap.numberOfConfirmations,
orderStep: state.swap.orderStep,
orderStarted: state.swap.orderStarted
orderStarted: state.swap.orderStarted,
isFetchingRates: state.swap.isFetchingRates
};
}

View File

@ -14,7 +14,7 @@ export type State = {
gasPriceGwei: number
};
export const initialState: State = {
export const INITIAL_STATE: State = {
languageSelection: languages[0].sign,
nodeSelection: Object.keys(NODES)[0],
gasPriceGwei: 21
@ -42,7 +42,7 @@ function changeGasPrice(state: State, action: ChangeGasPriceAction): State {
}
export function config(
state: State = initialState,
state: State = INITIAL_STATE,
action: ConfigAction
): State {
switch (action.type) {

View File

@ -8,7 +8,7 @@ import type { Token } from 'config/data';
export type State = Token[];
const initialState: State = [];
export const INITIAL_STATE: State = [];
function addCustomToken(state: State, action: AddCustomTokenAction): State {
if (state.find(token => token.symbol === action.payload.symbol)) {
@ -25,7 +25,7 @@ function removeCustomToken(
}
export function customTokens(
state: State = initialState,
state: State = INITIAL_STATE,
action: CustomTokenAction
): State {
switch (action.type) {

View File

@ -3,14 +3,14 @@ import type { EnsAction, CacheEnsAddressAction } from 'actions/ens';
export type State = { [string]: string };
const initialState: State = {};
export const INITIAL_STATE: State = {};
function cacheEnsAddress(state: State, action: CacheEnsAddressAction): State {
const { ensName, address } = action.payload;
return { ...state, [ensName]: address };
}
export function ens(state: State = initialState, action: EnsAction): State {
export function ens(state: State = INITIAL_STATE, action: EnsAction): State {
switch (action.type) {
case 'ENS_CACHE':
return cacheEnsAddress(state, action);

View File

@ -1,37 +1,25 @@
// @flow
import {
GENERATE_WALLET_SHOW_PASSWORD,
GENERATE_WALLET_FILE,
GENERATE_WALLET_DOWNLOAD_FILE,
GENERATE_WALLET_CONFIRM_CONTINUE_TO_PAPER,
RESET_GENERATE_WALLET
} from 'actions/generateWalletConstants';
import type PrivateKeyWallet from 'libs/wallet/privkey';
import type { GenerateWalletAction } from 'actions/generateWallet';
export type State = {
activeStep: string,
hasDownloadedWalletFile: boolean,
wallet: ?PrivateKeyWallet,
password: ?string
};
const initialState: State = {
export const INITIAL_STATE: State = {
activeStep: 'password',
hasDownloadedWalletFile: false,
wallet: null,
password: null
};
export function generateWallet(state: State = initialState, action): State {
export function generateWallet(
state: State = INITIAL_STATE,
action: GenerateWalletAction
): State {
switch (action.type) {
case GENERATE_WALLET_SHOW_PASSWORD: {
return {
...state,
activeStep: 'password'
};
}
case GENERATE_WALLET_FILE: {
case 'GENERATE_WALLET_GENERATE_WALLET': {
return {
...state,
wallet: action.wallet,
@ -40,28 +28,19 @@ export function generateWallet(state: State = initialState, action): State {
};
}
case GENERATE_WALLET_DOWNLOAD_FILE: {
return {
...state,
hasDownloadedWalletFile: true
};
}
case GENERATE_WALLET_CONFIRM_CONTINUE_TO_PAPER: {
case 'GENERATE_WALLET_CONTINUE_TO_PAPER': {
return {
...state,
activeStep: 'paper'
};
}
case RESET_GENERATE_WALLET: {
return {
...state,
...initialState
};
case 'GENERATE_WALLET_RESET': {
return INITIAL_STATE;
}
default:
(action: empty);
return state;
}
}

View File

@ -27,13 +27,17 @@ import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
export type State = {
// Custom reducers
generateWallet: GenerateWalletState,
config: ConfigState,
notifications: NotificationsState,
ens: EnsState,
wallet: WalletState,
customTokens: CustomTokensState,
rates: RatesState
rates: RatesState,
// Third party reducers (TODO: Fill these out)
form: Object,
routing: Object
};
export default combineReducers({

View File

@ -8,7 +8,7 @@ import type {
export type State = Notification[];
const initialState: State = [];
export const INITIAL_STATE: State = [];
function showNotification(state: State, action: ShowNotificationAction): State {
return state.concat(action.payload);
@ -21,7 +21,7 @@ function closeNotification(state, action: CloseNotificationAction): State {
}
export function notifications(
state: State = initialState,
state: State = INITIAL_STATE,
action: NotificationsAction
): State {
switch (action.type) {

View File

@ -6,13 +6,16 @@ export type State = {
[key: string]: number
};
const initialState: State = {};
export const INITIAL_STATE: State = {};
function setRates(state: State, action: SetRatesAction): State {
return action.payload;
}
export function rates(state: State = initialState, action: RatesAction): State {
export function rates(
state: State = INITIAL_STATE,
action: RatesAction
): State {
switch (action.type) {
case 'RATES_SET':
return setRates(state, action);

View File

@ -1,36 +1,45 @@
import {
SWAP_DESTINATION_AMOUNT,
SWAP_DESTINATION_KIND,
SWAP_ORIGIN_AMOUNT,
SWAP_ORIGIN_KIND,
SWAP_UPDATE_BITY_RATES,
SWAP_DESTINATION_ADDRESS,
SWAP_RESTART,
SWAP_STEP,
SWAP_REFERENCE_NUMBER
} from 'actions/swapConstants';
// @flow
import { combineAndUpper } from 'utils/formatters';
import type { SwapAction } from 'actions/swap';
export const ALL_CRYPTO_KIND_OPTIONS = ['BTC', 'ETH', 'REP'];
const initialState = {
originAmount: '',
destinationAmount: '',
type State = {
originAmount: number,
destinationAmount: number,
originKind: string,
destinationKind: string,
destinationKindOptions: Array<string>,
originKindOptions: Array<string>,
step: number,
bityRates: Object,
destinationAddress: string,
referenceNumber: string,
timeRemaining: string,
numberOfConfirmations: ?number,
orderStep: ?number,
isFetchingRates: boolean
};
export const INITIAL_STATE: State = {
originAmount: 0,
destinationAmount: 0,
originKind: 'BTC',
destinationKind: 'ETH',
destinationKindOptions: ALL_CRYPTO_KIND_OPTIONS.filter(
element => element !== 'BTC'
),
originKindOptions: ALL_CRYPTO_KIND_OPTIONS.filter(
element => element !== 'REP'
),
destinationKindOptions: ALL_CRYPTO_KIND_OPTIONS.filter(element => {
return element !== 'BTC';
}),
originKindOptions: ALL_CRYPTO_KIND_OPTIONS.filter(element => {
return element !== 'REP';
}),
step: 1,
bityRates: {},
destinationAddress: '',
referenceNumber: '',
timeRemaining: '',
numberOfConfirmations: null,
orderStep: null
orderStep: null,
isFetchingRates: false
};
const buildDestinationAmount = (
@ -44,7 +53,10 @@ const buildDestinationAmount = (
return originAmount * bityRate;
};
const buildDestinationKind = (originKind, destinationKind) => {
const buildDestinationKind = (
originKind: string,
destinationKind: string
): string => {
if (originKind === destinationKind) {
return ALL_CRYPTO_KIND_OPTIONS.filter(element => element !== originKind)[0];
} else {
@ -52,9 +64,9 @@ const buildDestinationKind = (originKind, destinationKind) => {
}
};
export function swap(state = initialState, action) {
export function swap(state: State = INITIAL_STATE, action: SwapAction) {
switch (action.type) {
case SWAP_ORIGIN_KIND: {
case 'SWAP_ORIGIN_KIND': {
const newDestinationKind = buildDestinationKind(
action.value,
state.destinationKind
@ -63,9 +75,10 @@ export function swap(state = initialState, action) {
...state,
originKind: action.value,
destinationKind: newDestinationKind,
destinationKindOptions: ALL_CRYPTO_KIND_OPTIONS.filter(
element => element !== action.value
),
destinationKindOptions: ALL_CRYPTO_KIND_OPTIONS.filter(element => {
// $FlowFixMe
return element !== action.value;
}),
destinationAmount: buildDestinationAmount(
state.originAmount,
action.value,
@ -74,7 +87,7 @@ export function swap(state = initialState, action) {
)
};
}
case SWAP_DESTINATION_KIND: {
case 'SWAP_DESTINATION_KIND': {
return {
...state,
destinationKind: action.value,
@ -86,42 +99,42 @@ export function swap(state = initialState, action) {
)
};
}
case SWAP_ORIGIN_AMOUNT:
case 'SWAP_ORIGIN_AMOUNT':
return {
...state,
originAmount: action.value
};
case SWAP_DESTINATION_AMOUNT:
case 'SWAP_DESTINATION_AMOUNT':
return {
...state,
destinationAmount: action.value
};
case SWAP_UPDATE_BITY_RATES:
case 'SWAP_UPDATE_BITY_RATES':
return {
...state,
bityRates: {
...state.bityRates,
...action.value
}
},
isFetchingRates: false
};
case SWAP_STEP: {
case 'SWAP_STEP': {
return {
...state,
step: action.value
};
}
case SWAP_DESTINATION_ADDRESS:
case 'SWAP_DESTINATION_ADDRESS':
return {
...state,
destinationAddress: action.value
};
case SWAP_RESTART:
case 'SWAP_RESTART':
return {
...state,
...initialState,
...INITIAL_STATE,
bityRates: state.bityRates
};
case SWAP_REFERENCE_NUMBER:
case 'SWAP_REFERENCE_NUMBER':
return {
...state,
referenceNumber: '2341asdfads',
@ -129,7 +142,21 @@ export function swap(state = initialState, action) {
numberOfConfirmations: 3,
orderStep: 2
};
case 'SWAP_LOAD_BITY_RATES':
return {
...state,
isFetchingRates: true
};
case 'SWAP_STOP_LOAD_BITY_RATES':
return {
...state,
isFetchingRates: false
};
default:
(action: empty);
return state;
}
}

View File

@ -18,7 +18,7 @@ export type State = {
}
};
const initialState: State = {
export const INITIAL_STATE: State = {
inst: null,
balance: new Big(0),
tokens: {}
@ -38,7 +38,7 @@ function setTokenBalances(state: State, action: SetTokenBalancesAction): State {
}
export function wallet(
state: State = initialState,
state: State = INITIAL_STATE,
action: WalletAction
): State {
switch (action.type) {

View File

@ -1,14 +1,9 @@
// @flow
import { call, put, fork, take, cancel, cancelled } from 'redux-saga/effects';
import type { Effect } from 'redux-saga/effects';
import { delay } from 'redux-saga';
import { updateBityRatesSwap } from 'actions/swap';
import {
SWAP_LOAD_BITY_RATES,
SWAP_STOP_LOAD_BITY_RATES
} from 'actions/swapConstants';
import { getAllRates } from 'api/bity';
export function* loadBityRates(_action?: any): Generator<Effect, void, any> {
@ -31,12 +26,12 @@ export function* loadBityRates(_action?: any): Generator<Effect, void, any> {
}
export default function* bitySaga(): Generator<Effect, void, any> {
while (yield take(SWAP_LOAD_BITY_RATES)) {
while (yield take('SWAP_LOAD_BITY_RATES')) {
// starts the task in the background
const loadBityRatesTask = yield fork(loadBityRates);
// wait for the user to get to point where refresh is no longer needed
yield take(SWAP_STOP_LOAD_BITY_RATES);
yield take('SWAP_STOP_LOAD_BITY_RATES');
// cancel the background task
// this will cause the forked loadBityRates task to jump into its finally block
yield cancel(loadBityRatesTask);