Merge remote-tracking branch 'remotes/dartandrevinsky/wip-customer' into wip-customer

This commit is contained in:
dartpopikyardo
2016-03-29 20:30:22 +03:00
79 changed files with 3796 additions and 880 deletions

View File

@@ -14,7 +14,7 @@ const PORT = process.env.PORT || 3000;
const $ = gulpLoadPlugins({camelize: true});
// Main tasks
// MyAccounts tasks
gulp.task('serve', () => runSequence('serve:clean', 'serve:index', 'serve:start'));
gulp.task('dist', () => runSequence('dist:clean', 'dist:build', 'dist:index'));
gulp.task('clean', ['dist:clean', 'serve:clean']);
@@ -89,13 +89,21 @@ gulp.task('serve:start', ['serve:static'], () => {
//}
proxy: {
'/user*' : {
target: 'http://localhost:8080'
},
'/login' : {
target: 'http://localhost:8080'
},
'/customers' : {
'/customers*' : {
target: 'http://localhost:8080'
},
'/accounts*' : {
target: 'http://localhost:8080'
},
'/transfers*' : {
target: 'http://localhost:8080'
}
}
})
.listen(PORT, '0.0.0.0', (err) => {

View File

@@ -15,11 +15,14 @@
"autoprefixer-loader": "^2.0.0",
"babel-core": "6.1.4",
"babel-loader": "6.1.0",
"babel-plugin-add-module-exports": "^0.1.2",
"babel-plugin-transform-runtime": "6.1.4",
"babel-polyfill": "^6.1.4",
"babel-preset-es2015": "6.1.4",
"babel-preset-react": "6.1.4",
"babel-preset-stage-0": "6.1.2",
"babel-register": "6.1.4",
"babel-runtime": "^6.0.14",
"css-loader": "^0.14.4",
"del": "^1.2.0",
"extract-text-webpack-plugin": "^0.8.1",
@@ -45,12 +48,14 @@
"webpack-dev-server": "^1.9.0"
},
"dependencies": {
"babel-polyfill": "6.1.4",
"babel-runtime": "6.0.14",
"classnames": "^2.2.3",
"history": "1.17.0",
"immutable": "^3.7.6",
"invariant": "^2.1.1",
"isomorphic-fetch": "^2.2.1",
"js-cookie": "^2.1.0",
"object-pick": "^0.1.1",
"querystring": "^0.2.0",
"react": "^0.14.7",
"react-bootstrap": "^0.28.3",
"react-dom": "^0.14.0",
@@ -61,10 +66,11 @@
"react-router-bootstrap": "^0.20.1",
"react-router-redux": "^3.0.0",
"react-select": "^0.9.1",
"react-timeago": "^2.2.1",
"redux": "^3.0.2",
"redux-auth": "0.0.2",
"redux-batched-subscribe": "^0.1.4",
"redux-multi": "^0.1.9",
"redux-logger": "^2.6.0",
"redux-multi": "^0.1.91",
"redux-router": "^1.0.0-beta7",
"redux-thunk": "^1.0.3",
"uniloc": "^0.2.0"

View File

@@ -3,44 +3,46 @@
*/
import React from "react";
import { Provider} from "react-redux";
import { createStore, compose, applyMiddleware, combineReducers} from "redux";
import { Provider, connect} from "react-redux";
import thunk from "redux-thunk";
import createLogger from 'redux-logger';
import { Route, IndexRoute, Link, IndexLink } from "react-router";
import { ReduxRouter} from "redux-router";
//import { Router, IndexRoute, Route, browserHistory } from 'react-router';
//import { syncHistory, routeReducer } from 'react-router-redux';
import { Route, IndexRoute, Link, IndexLink} from "react-router";
import { configure as reduxAuthConfigure, authStateReducer} from "redux-auth";
import { AuthGlobals } from "redux-auth/bootstrap-theme";
//import { configure as reduxAuthConfigure, authStateReducer } from "redux-auth";
//import { authStateReducer } from "redux-auth";
//import authStateReducer from './reducers/auth';
//import appStateReducer from './reducers/data';
import mainReducer from './reducers';
import { configure as reduxAuthConfigure } from './actions/configure';
//import { AuthGlobals } from "redux-auth/bootstrap-theme";
import { createStore, compose, applyMiddleware} from "redux";
import { createHistory, createHashHistory, createMemoryHistory } from "history";
import { routerStateReducer, reduxReactRouter as clientRouter} from "redux-router";
import { pushState, routerStateReducer, reduxReactRouter as clientRouter} from "redux-router";
import { reduxReactRouter as serverRouter } from "redux-router/server";
import { combineReducers} from "redux";
import thunk from "redux-thunk";
import { connect } from 'react-redux';
import { pushState } from 'redux-router';
import { requireAuthentication } from './components/AuthComponent';
//import demoButtons from "./reducers/request-test-buttons";
//import demoUi from "./reducers/demo-ui";
import Container from "./components/partials/Container";
import Main from "./views/Main";
import MyAccounts from "./views/MyAccounts";
import Account from "./views/Account";
import SignIn from "./views/SignIn";
import SignUp from "./views/SignUp";
//import GlobalComponents from "./views/partials/GlobalComponents";
// TODO: !!!!
// <GlobalComponents />
class App extends React.Component {
render() {
return (
<Container>
<AuthGlobals />
{this.props.children}
</Container>
);
@@ -50,96 +52,29 @@ class App extends React.Component {
export function initialize({cookies, isServer, currentLocation, userAgent} = {}) {
const reducer = combineReducers({
auth: authStateReducer,
app: mainReducer,
router: routerStateReducer
//demoButtons,
//demoUi
});
//let store;
//// access control method, used above in the "account" route
//const requireAuth = (nextState, transition, cb) => {
// // the setTimeout is necessary because of this bug:
// // https://github.com/rackt/redux-router/pull/62
// // this will result in a bunch of warnings, but it doesn't seem to be a serious problem
// setTimeout(() => {
// if (!store.getState().auth.getIn(["user", "isSignedIn"])) {
// transition(null, "/login");
// }
// cb();
// }, 0);
//};
const requireAuthentication = (Component) => {
class AuthenticatedComponent extends React.Component {
componentWillMount() {
this.checkAuth();
}
componentWillReceiveProps(nextProps) {
this.checkAuth();
}
checkAuth() {
debugger;
if (!this.props.isAuthenticated) {
let redirectAfterLogin = this.props.location.pathname;
this.props.dispatch(pushState(null, `/signin?next=${redirectAfterLogin}`));
}
}
render() {
debugger;
return (
<div>
{this.props.isAuthenticated === true
? <Component {...this.props}/>
: null
}
</div>
)
}
}
const mapStateToProps = (state) => ({
token: state.auth.token,
userName: state.auth.userName,
isAuthenticated: state.auth.isAuthenticated
});
return connect(mapStateToProps)(AuthenticatedComponent);
};
// define app routes
// <Route path="account" component={Account} onEnter={requireAuth} />
const routes = (
<Route path="/" component={App}>
<IndexRoute component={Main} />
<IndexRoute component={requireAuthentication(MyAccounts)} />
<Route path="signin" component={SignIn} />
<Route path="register" component={SignUp} />
<Route path="account" component={requireAuthentication(Account)} />
<Route path="account/:accountId" component={requireAuthentication(Account)} />
</Route>
);
// these methods will differ from server to client
var reduxReactRouter = clientRouter;
var createHistoryMethod = createHashHistory;
if (isServer) {
reduxReactRouter = serverRouter;
createHistoryMethod = createMemoryHistory;
}
const reduxReactRouter = isServer ? serverRouter : clientRouter;
const createHistoryMethod = isServer ? createMemoryHistory : createHashHistory;
// create the redux store
const store = compose(
applyMiddleware(thunk),
applyMiddleware(thunk, createLogger()),
reduxReactRouter({
createHistory: createHistoryMethod,
routes
routes,
createHistory: createHistoryMethod
})
)(createStore)(reducer);
@@ -150,35 +85,38 @@ export function initialize({cookies, isServer, currentLocation, userAgent} = {})
return store.dispatch(reduxAuthConfigure([
{
default: {
//apiUrl: __API_URL__
apiUrl: '/',
emailSignInPath: 'login',
emailRegistrationPath: 'customers'
//apiUrl: '/',
emailSignInPath: '/login',
customersPath: '/customers',
currentUserPath: '/user',
accountsPath: '/accounts',
transfersPath: '/transfers'
}
}
//, {
// evilUser: {
// //apiUrl: __API_URL__,
// apiUrl: '/api',
// signOutPath: "/mangs/sign_out",
// emailSignInPath: "/mangs/sign_in",
// emailRegistrationPath: "/mangs",
// accountUpdatePath: "/mangs",
// accountDeletePath: "/mangs",
// passwordResetPath: "/mangs/password",
// passwordUpdatePath: "/mangs/password",
// tokenValidationPath: "/mangs/validate_token",
// authProviderPaths: {
// github: "/mangs/github",
// facebook: "/mangs/facebook",
// google: "/mangs/google_oauth2"
// }
// }
//}
], {
cookies,
isServer,
currentLocation
currentLocation,
storage: 'localStorage',
tokenFormat: {
"access-token": "{{ access-token }}"
},
handleLoginResponse: function(resp) {
debugger;
return resp.data;
},
handleAccountUpdateResponse: function(resp) {
debugger;
return resp.data;
},
handleTokenValidationResponse: function(resp) {
debugger;
return resp.data;
}
})).then(({ redirectPath, blank } = {}) => {
// hack for material-ui server-side rendering.
// see https://github.com/callemall/material-ui/pull/2007
@@ -192,7 +130,7 @@ export function initialize({cookies, isServer, currentLocation, userAgent} = {})
return <noscript />;
}
console.log(`redirect path: ${redirectPath}`)
console.log(`redirect path: ${redirectPath}`);
return ({
blank,

View File

@@ -0,0 +1,27 @@
/**
* Created by andrew on 25/02/16.
*/
export function configureStart({...props} = {}) {
return {
...props,
type: T.AUTH.CONFIGURE_START
};
}
export function configureComplete({config, ...props} = {}) {
return {
...props,
type: T.AUTH.CONFIGURE_COMPLETE,
config
};
}
export function configureError({errors, ...props} = {}) {
return {
...props,
type: T.AUTH.CONFIGURE_ERROR,
error: errors
};
}

View File

@@ -0,0 +1,86 @@
/**
* Created by andrew on 26/02/16.
*/
import T from '../constants/ACTION_TYPES';
import {
getCurrentSettings,
setCurrentSettings,
getInitialEndpointKey,
setDefaultEndpointKey,
setCurrentEndpoint,
setCurrentEndpointKey,
retrieveData,
persistData,
destroySession,
persistUserData,
retrieveUserData,
retrieveHeaders
} from "../utils/sessionStorage";
import {
apiGetCurrentUser
} from '../utils/api';
import {entityReceived } from './entities';
export function authenticateStart() {
return { type: T.AUTH.AUTHENTICATE_START };
}
export function authenticateComplete(user) {
return { type: T.AUTH.AUTHENTICATE_COMPLETE, user };
}
export function authenticateError(errors) {
return { type: T.AUTH.AUTHENTICATE_ERROR, errors };
}
export function authenticate(forceReread) {
return dispatch => {
dispatch(authenticateStart());
const savedUserPromise = new Promise((rs, rj) => {
const currentHeaders = retrieveHeaders();
const accessToken = currentHeaders["access-token"];
if (!accessToken) {
return rj({ reason: 'no token'});
}
const savedUser = retrieveUserData();
if (savedUser && !forceReread) {
return rs(savedUser);
}
return apiGetCurrentUser().then((userData) => {
persistUserData(userData);
dispatch(entityReceived(userData.id, userData));
rs(userData);
}, (err) => {
debugger;
rj(err);
});
});
return savedUserPromise
.then(user => {
dispatch(authenticateComplete(user));
return user;
})
.catch(({reason} = {}) => {
dispatch(authenticateError([reason]));
return Promise.resolve({reason});
});
};
}

View File

@@ -0,0 +1,52 @@
/**
* Created by andrew on 26/02/16.
*/
import * as C from "../utils/constants";
import {
authenticate,
authenticateStart,
authenticateComplete,
authenticateError
} from "./authenticate";
import {
retrieveData,
} from "../utils/sessionStorage";
import {applyConfig} from "../utils/clientSettings";
//import {
// showFirstTimeLoginSuccessModal,
// showFirstTimeLoginErrorModal,
// showPasswordResetSuccessModal,
// showPasswordResetErrorModal
//} from "./ui";
import getRedirectInfo from "../utils/parseUrl";
import { pushState } from "redux-router";
import root from '../utils/root';
export const SET_ENDPOINT_KEYS = "SET_ENDPOINT_KEYS";
export const STORE_CURRENT_ENDPOINT_KEY = "STORE_CURRENT_ENDPOINT_KEY";
export function setEndpointKeys(endpoints, currentEndpointKey, defaultEndpointKey) {
return { type: SET_ENDPOINT_KEYS, endpoints, currentEndpointKey, defaultEndpointKey };
}
export function storeCurrentEndpointKey(currentEndpointKey) {
return { type: STORE_CURRENT_ENDPOINT_KEY, currentEndpointKey };
}
export function configure(endpoint={}, settings={}) {
return dispatch => {
return applyConfig({ dispatch, endpoint, settings })
.then(() => {
return dispatch(authenticate());
});
};
}

View File

@@ -1,9 +0,0 @@
import T from '../constants/ACTION_TYPES'
export function updateQuery(query) {
return {
type: T.DOCUMENT_LIST_VIEW.SET_QUERY,
query,
}
}

View File

@@ -1,60 +0,0 @@
import uuid from '../utils/uuid'
import documentValidator from '../validators/documentValidator'
import T from '../constants/ACTION_TYPES'
import * as navigation from './navigation'
export function updateChanges(id, data) {
return [
{
type: T.DOCUMENT_VIEW.UPDATE_DATA,
id,
data,
},
{
type: T.DOCUMENT_VIEW.REMOVE_STALE_ERRORS,
id,
errors: documentValidator(data),
},
]
}
export function clearChanges(id) {
return {
type: T.DOCUMENT_VIEW.CLEAR,
id,
}
}
export function cancelChanges(id) {
return [
clearChanges(id),
navigation.start('documentList'),
]
}
export function submitChanges(id) {
return (dispatch, getState) => {
const { view } = getState()
const data = view.document.unsavedChanges[id]
const errors = documentValidator(data)
if (errors) {
dispatch({
type: T.DOCUMENT_VIEW.SET_ERRORS,
id,
errors,
})
}
else {
const newId = id == 'new' ? uuid() : id
dispatch(navigation.start('documentEdit', {id: newId}))
dispatch(clearChanges(id))
dispatch({
type: T.DOCUMENT_DATA.UPDATE,
id: newId,
data,
})
}
}
}

View File

@@ -0,0 +1,237 @@
/**
* Created by andrew on 27/02/16.
*/
import T from '../constants/ACTION_TYPES';
import { makeActionCreator } from '../utils/actions';
import * as api from '../utils/api';
import { authenticate } from './authenticate';
export const entityRequested = makeActionCreator(T.ENTITIES.REQUESTED, 'id');
export const entityReceived = makeActionCreator(T.ENTITIES.RECEIVED, 'id', 'entity');
export const accountsListRequested = makeActionCreator(T.ACCOUNTS.LIST_START);
export const accountsListReceived = makeActionCreator(T.ACCOUNTS.LIST_COMPLETE, 'payload');
export const accountsListError = makeActionCreator(T.ACCOUNTS.LIST_ERROR, 'error');
export const accountsRefListReceived = makeActionCreator(T.ACCOUNTS.LIST_REF_COMPLETE, 'payload');
export const accountCreateStart = makeActionCreator(T.ACCOUNTS.CREATE_START);
export const accountCreateComplete = makeActionCreator(T.ACCOUNTS.CREATE_COMPLETE, 'payload');
export const accountCreateError = makeActionCreator(T.ACCOUNTS.CREATE_ERROR, 'error');
export const accountCreateFormUpdate = makeActionCreator(T.ACCOUNTS.CREATE_FORM_UPDATE, 'key', 'value');
export const accountRefCreateStart = makeActionCreator(T.ACCOUNTS.CREATE_REF_START);
export const accountRefCreateComplete = makeActionCreator(T.ACCOUNTS.CREATE_REF_COMPLETE, 'payload');
export const accountRefCreateError = makeActionCreator(T.ACCOUNTS.CREATE_REF_ERROR, 'error');
export const accountRefCreateFormUpdate = makeActionCreator(T.ACCOUNTS.CREATE_REF_FORM_UPDATE, 'key', 'value');
export const accountRequested = makeActionCreator(T.ACCOUNT.SINGLE_START);
export const accountComplete = makeActionCreator(T.ACCOUNT.SINGLE_COMPLETE, 'payload');
export const accountError = makeActionCreator(T.ACCOUNT.SINGLE_ERROR, 'error');
export function accountsList(userId) {
return dispatch => {
dispatch(accountsListRequested());
return api.apiRetrieveAccounts(userId)
.then(list => {
dispatch(accountsListReceived(list));
})
.catch(err => {
dispatch(accountsListError(err));
return Promise.resolve({ error: err });
})
};
}
export function accountCreate(customerId, payload) {
return dispatch => {
dispatch(accountCreateStart());
return api.apiCreateAccount(customerId, payload)
.then(({ accountId }) => {
dispatch(accountCreateComplete({
id: accountId,
...payload
}));
// dispatch(entityReceived(accountId, payload));
dispatch(authenticate(true));
return accountId;
})
.catch(err => {
debugger;
dispatch(accountCreateError(err));
// return Promise.resolve({ error: err });
});
};
}
export function accountRefCreate(customerId, payload) {
return dispatch => {
dispatch(accountRefCreateStart());
return api.apiCreateRefAccount(customerId, payload)
.then(({ id }) => {
dispatch(accountRefCreateComplete({
...payload,
id
}));
dispatch(entityReceived(id, payload));
return dispatch(authenticate(true));
})
.catch(err => {
debugger;
dispatch(accountRefCreateError(err));
return Promise.resolve({ error: err });
})
};
}
export function fetchOwnAccounts(customerId) {
return dispatch => {
//dispatch(accountsListRequested());
return api.apiRetrieveAccounts(customerId)
.then(data => {
dispatch(accountsListReceived(data));
});
};
}
export function fetchAccount(accountId) {
return dispatch => {
dispatch(accountRequested());
return api.apiRetrieveAccount(accountId)
.then(data => {
dispatch(accountComplete(data));
})
.catch(err => {
dispatch(accountError(err));
});
};
}
export const deleteAccountRequested = makeActionCreator(T.ACCOUNT.DELETE_START);
export const deleteAccountComplete = makeActionCreator(T.ACCOUNT.DELETE_COMPLETE);
export const deleteAccountError = makeActionCreator(T.ACCOUNT.DELETE_ERROR);
export function deleteAccount(customerId, accountId) {
return dispatch => {
dispatch(deleteAccountRequested());
return api.apiDeleteAccount(accountId)
.then(data => {
//debugger;
dispatch(deleteAccountComplete());
return Promise.resolve('ok');
})
.catch(err => {
dispatch(deleteAccountError());
return Promise.reject(err);
})
};
}
export const errorMessageStart = makeActionCreator(T.ERROR.START, 'payload');
export const errorMessageStop = makeActionCreator(T.ERROR.STOP);
export function errorMessageTimedOut(error, timeout) {
return dispatch => {
dispatch(errorMessageStart(error));
setTimeout(() => {
dispatch(errorMessageStop());
}, timeout || 5000);
};
}
export const createRefOwnerLookupStart = makeActionCreator(T.ACCOUNTS.CREATE_REF_OWNER_LOOKUP_START, 'payload');
export const createRefOwnerLookupComplete = makeActionCreator(T.ACCOUNTS.CREATE_REF_OWNER_LOOKUP_COMPLETE, 'payload');
export const createRefAccountLookupStart = makeActionCreator(T.ACCOUNTS.CREATE_REF_ACCOUNT_LOOKUP_START, 'payload');
export const createRefAccountLookupComplete = makeActionCreator(T.ACCOUNTS.CREATE_REF_ACCOUNT_LOOKUP_COMPLETE, 'payload');
export const createRefOwnerLookup = lookup => {
return dispatch => {
dispatch(createRefOwnerLookupStart(lookup));
return api.apiRetrieveUsers(lookup)
.then(data => {
const { customers = [] } = data || {};
const arr = customers.map(c => {
const { id, name, email } = c;
const fullName = ([name.firstName, name.lastName]).filter(i => i).join(' ');
const label = email ? `${ fullName } (${ email })` : fullName;
return {
value: id,
label
};
});
dispatch(createRefOwnerLookupComplete(arr));
return { options: arr };
})
.catch(err => {
dispatch(createRefOwnerLookupComplete([]));
return { options: [] };
});
};
};
export const createRefAccountLookup = customerId => {
return dispatch => {
dispatch(createRefAccountLookupStart());
return api.apiRetrieveAccounts(customerId)
.then(data => {
const arr = data.map(({ accountId, title }) => ({
value: accountId,
label: title
}));
dispatch(createRefAccountLookupComplete(arr));
return { options: arr };
})
.catch(err => {
dispatch(createRefAccountLookupComplete([]));
return { options: [] };
});
};
};
export const makeTransferRequested = makeActionCreator(T.TRANSFERS.MAKE_START, 'payload');
export const makeTransferComplete = makeActionCreator(T.TRANSFERS.MAKE_COMPLETE, 'payload');
export const makeTransferError = makeActionCreator(T.TRANSFERS.MAKE_ERROR, 'error');
export const makeTransferFormUpdate = makeActionCreator(T.TRANSFERS.MAKE_FORM_UPDATE, 'key', 'value');
export const makeTransfer = (accountId, payload) => {
return dispatch => {
dispatch(makeTransferRequested());
return api.apiMakeTransfer(accountId, payload)
.then(data => {
const { moneyTransferId } = data;
dispatch(makeTransferComplete(data));
return moneyTransferId;
})
.catch(err => {
dispatch(makeTransferError(err));
return err;
});
};
};
export const getTransfersRequested = makeActionCreator(T.TRANSFERS.LIST_START);
export const getTransfersComplete = makeActionCreator(T.TRANSFERS.LIST_COMPLETE, 'payload');
export const getTransfersError = makeActionCreator(T.TRANSFERS.LIST_ERROR, 'error');
export const getTransfers = (accountId) => {
return dispatch => {
dispatch(getTransfersRequested());
return api.apiRetrieveTransfers(accountId)
.then(data => {
dispatch(getTransfersComplete(data));
return data;
})
.catch(err => {
dispatch(getTransfersError(err));
return err;
});
};
};

View File

@@ -0,0 +1,51 @@
/**
* Created by andrew on 26/02/16.
*/
import {
setCurrentEndpointKey,
getCurrentEndpointKey,
persistUserData
} from "../utils/sessionStorage";
import { entityReceived } from './entities';
import { storeCurrentEndpointKey } from "./configure";
//import { parseResponse } from "../utils/handleFetchResponse";
//import fetch from "../utils/fetch";
import { apiSignIn } from '../utils/api';
import { makeActionCreator } from '../utils/actions';
import T from '../constants/ACTION_TYPES';
//import root from '../utils/root';
export const emailSignInFormUpdate = makeActionCreator(T.AUTH.SIGN_IN_FORM_UPDATE, 'key', 'value');
export const emailSignInStart = makeActionCreator(T.AUTH.SIGN_IN_START);
export const emailSignInComplete = makeActionCreator(T.AUTH.SIGN_IN_COMPLETE, 'user');
export const emailSignInError = makeActionCreator(T.AUTH.SIGN_IN_ERROR, 'error');
export function emailSignIn(body) {
return dispatch => {
dispatch(emailSignInStart());
return apiSignIn(body)
.then(function(data = {}) {
const { id } = data;
if (id ) {
dispatch(entityReceived(id, data));
}
return data;
})
.then((user) => {
persistUserData(user);
dispatch(emailSignInComplete(user));
})
.catch((errors) => {
// revert endpoint key to what it was before failed request
//setCurrentEndpointKey(prevEndpointKey);
//dispatch(storeCurrentEndpointKey(prevEndpointKey));
return dispatch(emailSignInError(errors));
});
};
}

View File

@@ -0,0 +1,40 @@
/**
* Created by andrew on 11/03/16.
*/
import {
getEmailSignInUrl,
setCurrentEndpointKey,
getCurrentEndpointKey
} from "../utils/sessionStorage";
import {destroySession} from "../utils/sessionStorage";
import { entityReceived } from './entities';
import { storeCurrentEndpointKey } from "./configure";
import { parseResponse } from "../utils/handleFetchResponse";
import fetch from "../utils/fetch";
import T from '../constants/ACTION_TYPES';
import root from '../utils/root';
export function signOutStart() {
return { type: T.AUTH.SIGN_OUT_START };
}
export function signOutComplete() {
return { type: T.AUTH.SIGN_OUT_COMPLETE };
}
export function signOut() {
return dispatch => {
dispatch(signOutStart());
destroySession();
dispatch(signOutComplete());
};
}

View File

@@ -0,0 +1,48 @@
/**
* Created by andrew on 11/03/16.
*/
import {
getEmailSignUpUrl
} from "../utils/sessionStorage";
import { entityReceived } from './entities';
import { storeCurrentEndpointKey } from "./configure";
//import { parseResponse } from "../utils/handleFetchResponse";
import { apiSignUp } from "../utils/api";
import { emailSignInFormUpdate } from './signIn';
import { push } from 'redux-router';
import T from '../constants/ACTION_TYPES';
export function emailSignUpFormUpdate(key, value) {
return { type: T.AUTH.SIGN_UP_FORM_UPDATE, key, value };
}
export function emailSignUpStart() {
return { type: T.AUTH.SIGN_UP_START };
}
export function emailSignUpComplete(user) {
return { type: T.AUTH.SIGN_UP_COMPLETE, user };
}
export function emailSignUpError(errors) {
return { type: T.AUTH.SIGN_UP_ERROR, errors };
}
export function emailSignUp(body) {
return dispatch => {
dispatch(emailSignUpStart());
return apiSignUp(body)
.then(({data}) => {
dispatch(emailSignUpComplete(data));
const { email } = body;
dispatch(emailSignInFormUpdate('email', email));
dispatch(push('/signin'));
})
.catch(({errors}) => dispatch(emailSignUpError(errors)));
};
}

View File

@@ -0,0 +1,46 @@
/**
* Created by andrew on 3/22/16.
*/
import React from "react";
import { connect } from 'react-redux';
import Spinner from "react-loader";
import * as BS from "react-bootstrap";
import * as A from '../actions/entities';
// import { Money } from '../components/Money';
export class AccountInfo extends React.Component {
componentWillMount() {
this.ensureData(this.props);
}
componentWillReceiveProps(nextProps) {
this.ensureData(nextProps);
}
ensureData({ dispatch, entities, accountId }) {
if (entities[accountId]) {
return;
}
dispatch(A.fetchAccount(accountId));
}
render() {
const { entities, accountId } = this.props;
const account = entities[accountId];
if (!account) {
return (<div>{ accountId } <Spinner ref="spinner" loaded={false} /></div>)
}
const { title } = account;
return (<div>{ title } </div>);
}
}
export default connect(({ app }) => ({
entities: app.data.entities
}))(AccountInfo);

View File

@@ -0,0 +1,57 @@
/**
* Created by andrew on 21/02/16.
*/
import React from 'react';
import { connect } from 'react-redux';
import { pushState } from 'redux-router';
import read from '../utils/readProp';
export function requireAuthentication(Component) {
class AuthComponent extends React.Component {
checkRedirect(props) {
if (!props.isAuthenticated) {
// redirect to login and add next param so we can redirect again after login
const redirectAfterLogin = props.location.pathname;
props.dispatch(pushState(null, `/signin?next=${redirectAfterLogin}`));
}
}
componentWillMount() {
this.checkRedirect(this.props);
}
componentWillReceiveProps(nextProps) {
this.checkRedirect(nextProps);
}
render() {
const { isAuthenticated = false } = this.props;
if (isAuthenticated) {
// render the component that requires auth (passed to this wrapper)
return (
<Component {...this.props} />
)
}
return (<div className="panel">
<h2 className="text-danger">No anonymous access!</h2>
</div>);
}
}
const mapStateToProps =
(state) => {
return ({
isAuthenticated: read(state, 'app.auth.user.isSignedIn', false)
})
};
return connect(mapStateToProps)(AuthComponent);
}

View File

@@ -1,75 +0,0 @@
import './DocumentForm.less'
import React, {PropTypes} from 'react'
import * as actions from '../actions/documentView'
import { pacomoTransformer } from '../utils/pacomo'
function updater(original, prop, fn) {
return e => fn(Object.assign({}, original, {[prop]: e.target.value}))
}
function preventDefault(fn) {
return e => {
e.preventDefault()
fn && fn(e)
}
}
const errorMap = (error, i) => <li className='error' key={i}>{error}</li>
const DocumentForm = ({
data,
errors,
onUpdate,
onSubmit,
onCancel,
}) =>
<form
onSubmit={preventDefault(onSubmit)}
noValidate={true}
>
<ul className='errors'>
{errors && Object.values(errors).map(errorMap)}
</ul>
<input
type='text'
className='title'
placeholder='Title'
onChange={updater(data, 'title', onUpdate)}
value={data.title || ''}
autoFocus
/>
<textarea
type='text'
className='content'
onChange={updater(data, 'content', onUpdate)}
value={data.content || ''}
/>
<footer className='buttons'>
<button
type='button'
className='cancel'
onClick={onCancel}
>
Cancel
</button>
<button
type='submit'
className='submit'
disabled={!onSubmit}
>
Save
</button>
</footer>
</form>
DocumentForm.propTypes = {
data: PropTypes.object.isRequired,
errors: PropTypes.object,
onUpdate: PropTypes.func.isRequired,
onSubmit: PropTypes.func,
onCancel: PropTypes.func.isRequired,
}
export default pacomoTransformer(DocumentForm)

View File

@@ -1,25 +0,0 @@
.app-DocumentForm {
width: 100%;
padding: 5px;
&-errors {
list-style: none;
padding: 0;
margin: 0;
}
&-error {
color: red;
margin-bottom: 5px;
}
&-title,
&-content {
display: block;
width: 100%;
margin-bottom: 5px;
}
&-cancel {
margin-right: 5px;
}
}

View File

@@ -1,71 +0,0 @@
import './DocumentList.less'
import React, {PropTypes} from 'react'
import * as actions from '../actions/documentListView'
import { pacomoTransformer } from '../utils/pacomo'
import Link from './Link'
function mapValue(fn) {
return e => fn(e.target.value)
}
const DocumentList = ({
id: activeId,
query,
documents,
onChangeQuery,
}) =>
<div>
<header className='header'>
<input
className='query'
type="text"
placeholder="Search..."
value={query}
onChange={mapValue(onChangeQuery)}
/>
</header>
<ul className='list'>
{documents.map(([id, data]) =>
<li
key={id}
className={{
'document-item': true,
'document-item-active': activeId == id,
}}
>
<Link
className='document-link'
name='documentEdit'
options={{id}}
>
{data.title}
</Link>
</li>
)}
<li
className={{
'add-item': true,
'add-item-active': activeId == 'new',
}}
>
<Link
className='add-link'
name='documentEdit'
options={{id: 'new'}}
>
Add Document
</Link>
</li>
</ul>
</div>
DocumentList.propTypes = {
id: PropTypes.string,
query: PropTypes.string,
documents: PropTypes.array.isRequired,
onChangeQuery: PropTypes.func.isRequired,
}
export default pacomoTransformer(DocumentList)

View File

@@ -1,33 +0,0 @@
.app-DocumentList {
width: 100%;
&-header {
width: 100%;
padding: 5px;
}
&-query {
width: 100%;
}
&-list {
list-style: none;
margin: 0;
padding: 0;
}
&-document-item-active,
&-add-item-active {
background-color: #f0f0f0;
}
&-document-link,
&-add-link {
display: block;
padding: 5px;
&:hover {
background-color: #f8f8f8;
}
}
}

View File

@@ -0,0 +1,65 @@
/**
* Created by andrew on 11/03/16.
*/
import React from 'react';
import { connect } from 'react-redux';
import { LinkContainer } from "react-router-bootstrap";
import read from '../utils/readProp';
//import { PageHeader, OverlayTrigger, Tooltip, Grid, Col, Row, Nav, NavItem, ButtonGroup, Button, Table } from "react-bootstrap";
import * as BS from "react-bootstrap";
import { Link, IndexLink } from "react-router";
import { signOut } from '../actions/signOut';
export class HeaderLinks extends React.Component {
signOut(evt, key) {
this.props.dispatch(signOut());
}
render() {
let buttonSet = null;
const isSignedIn = read(this.props.auth, 'user.isSignedIn', false);
const { location } = this.props.router;
const isRegister = location.pathname == '/register';
const isLogin = location.pathname == '/signin';
const condition = isSignedIn ? 2 : (isRegister ? 1 : 0);
switch (condition) {
case 0:
buttonSet = (<LinkContainer to="/register">
<BS.NavItem>Register</BS.NavItem>
</LinkContainer>);
break;
case 1:
buttonSet = (<LinkContainer to="/signin">
<BS.NavItem>Log In</BS.NavItem>
</LinkContainer>);
break;
case 2:
buttonSet = (<BS.NavItem onClick={this.signOut.bind(this)} eventKey="2">Sign Out</BS.NavItem>);
break;
}
return (
<BS.Nav pullRight={true}>
{ buttonSet }
</BS.Nav>
);
}
}
export default connect(({
//dispatch,
router,
app
}) => ({
//dispatch,
router,
auth: app.auth
}))(HeaderLinks);

View File

@@ -0,0 +1,28 @@
/**
* Created by andrew on 3/22/16.
*/
import React from 'react';
export const moneyText = (amount) => {
if (Number.isNaN(Number(amount))) {
return '';
}
const absNum = Math.abs(Number(amount) / 100);
if (absNum < 0) {
return `$(${absNum.toFixed(2)})`;
}
return `$${absNum.toFixed(2)}`;
};
export const Money = ({ amount }) => {
if (Number.isNaN(Number(amount))) {
return (<span />);
}
const absNum = Math.abs(Number(amount) / 100);
if (absNum < 0) {
return (<span className="text-danger">($${ absNum.toFixed(2) })</span>)
}
return (<span>${ absNum.toFixed(2) }</span>);
};

View File

@@ -0,0 +1,61 @@
/**
* Created by andrew on 3/22/16.
*/
import React from "react";
import Spinner from "react-loader";
import * as BS from "react-bootstrap";
import TimeAgo from 'react-timeago';
import { Money } from './Money';
import AccountInfo from './AccountInfo';
export class TransfersTable extends React.Component {
render() {
const { loading, data, errors } = this.props;
if (loading) {
return (<h2><Spinner ref="spinner" loaded={false} /> Loading..</h2>);
}
if (Object.keys(errors).length) {
return (<div className="text-danger">Errors..</div>);
}
const transfers = data.length ? data.map(({
amount,
fromAccountId,
toAccountId,
transactionId,
description = '',
date = null,
status = ''
}, idx) => (<tr key={idx}>
<td><TimeAgo date={date} /></td>
<td><AccountInfo accountId={ fromAccountId } /></td>
<td><AccountInfo accountId={ toAccountId } /></td>
<td><Money amount={ amount } /></td>
<td>{ description || 'N/a'}</td>
<td>{ status || 'N/a' }</td>
</tr>)) : (<tr>
<td colSpan={6}>No transfers for this account just yet.</td>
</tr>);
return (
<BS.Table striped bordered condensed hover>
<thead>
<tr>
<th>Date</th>
<th>What</th>
<th>Counter Account</th>
<th>Amount</th>
<th>Description</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{ transfers }
</tbody>
</BS.Table>
);
}
}

View File

@@ -2,9 +2,16 @@
* Created by andrew on 17/02/16.
*/
import React, { PropTypes } from "react";
import { Grid, Navbar, NavItem, Nav, NavbarBrand } from "react-bootstrap";
import { Grid, Col, Navbar, NavItem, Nav, NavbarBrand, Footer } from "react-bootstrap";
import { LinkContainer } from "react-router-bootstrap";
import HeaderLinks from '../HeaderLinks';
//import { SignOutButton } from "redux-auth/bootstrap-theme";
//const SignOutButton = () => (<div>SignOutButton!</div>);
//if (!global.__SERVER__ && !global.__TEST__) {
// require("../../styles/main.scss");
//}
@@ -25,17 +32,25 @@ class Container extends React.Component {
<LinkContainer to="/" onlyActiveOnIndex={true}>
<NavItem eventKey={1}>Home</NavItem>
</LinkContainer>
<LinkContainer to="/account">
<NavItem eventKey={2}>Account</NavItem>
</LinkContainer>
</Nav>
<div>
<HeaderLinks />
</div>
</Navbar>
<Grid className="content">
{this.props.children}
</Grid>
<Navbar fixedBottom={true} className="footer-navigation">
<Col xs={12} sm={6}>&copy; 2016 Eventuate.io</Col>
<Col xs={12} sm={6} className="text-right">
<a href="#">Terms</a> |&nbsp;
<a href="#">Policy</a> |&nbsp;
<a href="#">Contact</a> |&nbsp;
<a href="#">About</a>
</Col>
</Navbar>
</div>
);
}

View File

@@ -4,33 +4,78 @@ export default defineActionTypes({
/*
* View model
*/
DOCUMENT_LIST_VIEW: `
SET_QUERY
AUTH: `
CONFIGURE_START
CONFIGURE_COMPLETE
CONFIGURE_ERROR
AUTHENTICATE_START
AUTHENTICATE_COMPLETE
AUTHENTICATE_ERROR
SIGN_IN_START
SIGN_IN_COMPLETE
SIGN_IN_ERROR
SIGN_IN_FORM_UPDATE
SIGN_UP_START
SIGN_UP_COMPLETE
SIGN_UP_ERROR
SIGN_UP_FORM_UPDATE
SIGN_OUT_START
SIGN_OUT_COMPLETE
`,
DOCUMENT_VIEW: `
UPDATE_DATA
SET_ERRORS
REMOVE_STALE_ERRORS
CLEAR
ENTITIES: `
REQUESTED
RECEIVED
RECEIVED_LIST
`,
/*
* Data model
*/
DOCUMENT_DATA: `
UPDATE
ACCOUNTS: `
LIST_START
LIST_COMPLETE
LIST_ERROR
LIST_REF_START
LIST_REF_COMPLETE
LIST_REF_ERROR
CREATE_START
CREATE_COMPLETE
CREATE_ERROR
CREATE_FORM_UPDATE
EDIT_START
EDIT_COMPLETE
EDIT_ERROR
EDIT_FORM_UPDATE
CREATE_REF_START
CREATE_REF_COMPLETE
CREATE_REF_ERROR
CREATE_REF_FORM_UPDATE
CREATE_REF_OWNER_LOOKUP_START
CREATE_REF_OWNER_LOOKUP_COMPLETE
CREATE_REF_ACCOUNT_LOOKUP_START
CREATE_REF_ACCOUNT_LOOKUP_COMPLETE
`,
/*
* Application
*/
ACCOUNT: `
SINGLE_START
SINGLE_COMPLETE
SINGLE_ERROR
DELETE_START
DELETE_COMPLETE
DELETE_ERROR
`,
TRANSFERS: `
MAKE_START
MAKE_COMPLETE
MAKE_ERROR
MAKE_FORM_UPDATE
LIST_START
LIST_COMPLETE
LIST_ERROR
`,
NAVIGATION: `
ERROR: `
START
COMPLETE
`,
STOP
`
})

View File

@@ -0,0 +1,53 @@
/**
* Created by andrew on 15/02/16.
*/
import React, { PropTypes } from "react";
import { Glyphicon } from "react-bootstrap";
class AuxErrorLabel extends React.Component {
static propTypes = {
label: PropTypes.string,
errors: PropTypes.array
};
static defaultProps = {
label: '',
errors: []
};
// <Input {...this.props}
// bsStyle={(this.props.errors.length) ? "error" : null}
// onChange={this.handleInput.bind(this)} />
render () {
const { errors } = this.props;
if (errors.length) {
return (
<div className='has-error'>
{ errors.map((err, i) => {
return (
<p className="control-label inline-error-item"
style={{paddingLeft: "20px", position: "relative", marginBottom: "28px"}}
key={i}>
<Glyphicon glyph="exclamation-sign"
style={{
position: "absolute",
left: 0,
top: 2
}}
/> {this.props.label} {err}
</p>
);
})}
</div>
);
} else {
return <span />;
}
}
}
export default AuxErrorLabel;

View File

@@ -2,12 +2,15 @@
* Created by andrew on 15/02/16.
*/
import React, {PropTypes} from "react";
import auth from "redux-auth";
import { connect } from "react-redux";
import read from '../../utils/readProp';
import * as BS from "react-bootstrap";
import Input from "./Input";
import ButtonLoader from "./ButtonLoader";
import { emailSignInFormUpdate, emailSignIn } from "redux-auth";
import { Glyphicon } from "react-bootstrap";
import { connect } from "react-redux";
import AuxErrorLabel from './AuxErrorLabel';
import { emailSignInFormUpdate, emailSignIn } from "../../actions/signIn";
/*
<Input type="password"
@@ -22,6 +25,7 @@ import { connect } from "react-redux";
*/
class EmailSignInForm extends React.Component {
static propTypes = {
endpoint: PropTypes.string,
inputProps: PropTypes.shape({
@@ -39,47 +43,56 @@ class EmailSignInForm extends React.Component {
}
};
getEndpoint () {
return (
this.props.endpoint ||
this.props.auth.getIn(["configure", "currentEndpointKey"]) ||
this.props.auth.getIn(["configure", "defaultEndpointKey"])
);
}
handleInput (key, val) {
this.props.dispatch(emailSignInFormUpdate(this.getEndpoint(), key, val));
this.props.dispatch(emailSignInFormUpdate(key, val));
}
handleSubmit (event) {
event.preventDefault();
let formData = this.props.auth.getIn(["emailSignIn", this.getEndpoint(), "form"]).toJS();
debugger;
this.props.dispatch(emailSignIn(formData, this.getEndpoint()));
let formData = { ...this.props.auth.signIn.form };
this.props.dispatch(emailSignIn(formData));
}
render () {
let disabled = (
this.props.auth.getIn(["user", "isSignedIn"]) ||
this.props.auth.getIn(["emailSignIn", this.getEndpoint(), "loading"])
try {
const disabled = (
this.props.auth.user.isSignedIn ||
this.props.auth.signIn.loading
);
return (
//const error = read(this.props.auth, 'signIn.errors.email', null);
//debugger;
const formErrors = read(this.props.auth, 'signIn.errors.errors', '');
return (
<form className='redux-auth email-sign-in-form clearfix'
onSubmit={this.handleSubmit.bind(this)}>
<div className="form-group" style={{
display: formErrors ? 'block' : 'none'
}}>
<AuxErrorLabel
label="Form:"
errors={formErrors.length ? [formErrors] : [] }
/>
</div>
<Input type="text"
className="email-sign-in-email"
label="Email"
placeholder="Email"
name="email"
disabled={disabled}
value={this.props.auth.getIn(["emailSignIn", this.getEndpoint(), "form", "email"])}
errors={this.props.auth.getIn(["emailSignIn", this.getEndpoint(), "errors", "email"])}
value={read(this.props.auth, 'signIn.form.email', '')}
errors={read(this.props.auth, 'signIn.errors.email', [])}
onChange={this.handleInput.bind(this, "email")}
{...this.props.inputProps.email} />
<ButtonLoader loading={this.props.auth.getIn(["emailSignIn", this.getEndpoint(), "loading"])}
<ButtonLoader loading={read(this.props.auth, 'signIn.loading', false)}
type="submit"
icon={<Glyphicon glyph="log-in" />}
icon={<BS.Glyphicon glyph="log-in" />}
className='email-sign-in-submit pull-right'
disabled={disabled}
onClick={this.handleSubmit.bind(this)}
@@ -88,7 +101,11 @@ class EmailSignInForm extends React.Component {
</ButtonLoader>
</form>
);
} catch (ex){
console.error('Render exception: ', ex);
return [' ERROR '];
}
}
}
export default connect(({auth}) => ({auth}))(EmailSignInForm);
export default connect(({app}) => ({auth: app.auth}))(EmailSignInForm);

View File

@@ -2,33 +2,22 @@
* Created by andrew on 15/02/16.
*/
import React, {PropTypes} from "react";
import auth from "redux-auth";
//import auth from "redux-auth";
import Input from "./Input";
import ButtonLoader from "./ButtonLoader";
import { emailSignUpFormUpdate, emailSignUp } from "redux-auth";
//import { emailSignUpFormUpdate, emailSignUp } from "redux-auth";
import IndexPanel from "./../../components/partials/IndexPanel";
import { customerInfoMap } from '../../entities/formToPayloadMappers';
import read from '../../utils/readProp';
import { Glyphicon } from "react-bootstrap";
import { connect } from "react-redux";
class EmailSignUpForm extends React.Component {
static propTypes = {
endpoint: PropTypes.string,
inputProps: PropTypes.shape({
email: PropTypes.object,
password: PropTypes.object,
passwordConfirmation: PropTypes.object,
submit: PropTypes.object
})
};
import {emailSignUpFormUpdate, emailSignUp} from '../../actions/signUp';
static defaultProps = {
inputProps: {
email: {},
password: {},
submit: {}
}
};
class EmailSignUpForm extends React.Component {
getEndpoint () {
return (
@@ -39,57 +28,58 @@ class EmailSignUpForm extends React.Component {
}
handleInput (key, val) {
this.props.dispatch(emailSignUpFormUpdate(this.getEndpoint(), key, val));
this.props.dispatch(emailSignUpFormUpdate(key, val));
}
handleSubmit (event) {
event.preventDefault();
let formData = this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form"]).toJS();
debugger;
this.props.dispatch(emailSignUp(formData, this.getEndpoint()));
let formData = { ...this.props.auth.signUp.form };
this.props.dispatch(emailSignUp(customerInfoMap(formData)));
}
render () {
let disabled = (
this.props.auth.getIn(["user", "isSignedIn"]) ||
this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "loading"])
);
const disabled = (
this.props.auth.user.isSignedIn ||
this.props.auth.signUp.loading
);
return (
<form className='redux-auth email-sign-up-form clearfix'
onSubmit={this.handleSubmit.bind(this)}>
<IndexPanel header="basic">
<Input type="text"
label="First name"
placeholder="First name"
className="email-sign-up-email"
disabled={disabled}
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "fname"])}
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "fname"])}
value={read(this.props.auth, 'signUp.form.fname', '')}
errors={read(this.props.auth, 'signUp.errors.fname', [])}
onChange={this.handleInput.bind(this, "fname")}
{...this.props.inputProps.fname} />
/>
<Input type="text"
label="Last name"
placeholder="Last name"
className="email-sign-up-email"
disabled={disabled}
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "lname"])}
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "lname"])}
value={read(this.props.auth, 'signUp.form.lname', '')}
errors={read(this.props.auth, 'signUp.errors.lname', [])}
onChange={this.handleInput.bind(this, "lname")}
{...this.props.inputProps.lname} />
/>
<Input type="text"
label="Email"
placeholder="Email"
className="email-sign-up-email"
disabled={disabled}
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "email"])}
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "email"])}
value={read(this.props.auth, 'signUp.form.email', '')}
errors={read(this.props.auth, 'signUp.errors.email', [])}
onChange={this.handleInput.bind(this, "email")}
{...this.props.inputProps.email} />
/>
</IndexPanel>
@@ -99,88 +89,96 @@ class EmailSignUpForm extends React.Component {
label="SSN"
placeholder="SSN"
className="email-sign-up-email"
bsSize="small"
disabled={disabled}
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "ssn"])}
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "ssn"])}
value={read(this.props.auth, 'signUp.form.ssn', '')}
errors={read(this.props.auth, 'signUp.errors.ssn', [])}
onChange={this.handleInput.bind(this, "ssn")}
{...this.props.inputProps.ssn} />
/>
<Input type="text"
label="Phone"
placeholder="Phone"
className="email-sign-up-email"
bsSize="small"
disabled={disabled}
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "phone"])}
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "phone"])}
onChange={this.handleInput.bind(this, "phone")}
{...this.props.inputProps.phone} />
value={read(this.props.auth, 'signUp.form.phoneNumber', '')}
errors={read(this.props.auth, 'signUp.errors.phoneNumber', [])}
onChange={this.handleInput.bind(this, "phoneNumber")}
/>
<Input type="text"
label="Address 1"
placeholder="Address 1"
className="email-sign-up-email"
bsSize="small"
disabled={disabled}
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "address1"])}
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "address1"])}
value={read(this.props.auth, 'signUp.form.address1', '')}
errors={read(this.props.auth, 'signUp.errors.address1', [])}
onChange={this.handleInput.bind(this, "address1")}
{...this.props.inputProps.address1} />
/>
<Input type="text"
label="Address 2"
placeholder="Address 2"
className="email-sign-up-email"
bsSize="small"
disabled={disabled}
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "address2"])}
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "address2"])}
value={read(this.props.auth, 'signUp.form.address2', '')}
errors={read(this.props.auth, 'signUp.errors.address2', [])}
onChange={this.handleInput.bind(this, "address2")}
{...this.props.inputProps.address2} />
/>
<Input type="text"
label="City"
placeholder="City"
className="email-sign-up-email"
bsSize="small"
disabled={disabled}
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "city"])}
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "city"])}
value={read(this.props.auth, 'signUp.form.city', '')}
errors={read(this.props.auth, 'signUp.errors.city', {})}
onChange={this.handleInput.bind(this, "city")}
{...this.props.inputProps.city} />
/>
<Input type="text"
label="State"
placeholder="State"
className="email-sign-up-email"
bsSize="small"
disabled={disabled}
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "state"])}
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "state"])}
value={read(this.props.auth, 'signUp.form.state', '')}
errors={read(this.props.auth, 'signUp.errors.state', [])}
onChange={this.handleInput.bind(this, "state")}
{...this.props.inputProps.state} />
/>
<Input type="text"
label="ZIP"
placeholder="ZIP"
className="email-sign-up-email"
bsSize="small"
disabled={disabled}
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "zip"])}
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "zip"])}
value={read(this.props.auth, 'signUp.form.zip', '')}
errors={read(this.props.auth, 'signUp.errors.zip', [])}
onChange={this.handleInput.bind(this, "zip")}
{...this.props.inputProps.zip} />
/>
</IndexPanel>
<ButtonLoader loading={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "loading"])}
<ButtonLoader loading={read(this.props.auth, 'signUp.loading', false)}
type="submit"
className="email-sign-up-submit pull-right"
icon={<Glyphicon glyph="send" />}
disabled={disabled}
onClick={this.handleSubmit.bind(this)}
{ ...this.props.inputProps.submit } >
>
Sign Up
</ButtonLoader>
</form>
);
}
}
export default connect(({auth}) => ({auth}))(EmailSignUpForm);
export default connect(({app}) => ({auth: app.auth}))(EmailSignUpForm);

View File

@@ -3,19 +3,18 @@
*/
import React, { PropTypes } from "react";
import { Input, Glyphicon } from "react-bootstrap";
import Immutable from "immutable";
class AuthInput extends React.Component {
static propTypes = {
label: PropTypes.string,
value: PropTypes.string,
errors: PropTypes.object
errors: PropTypes.array
};
static defaultProps = {
label: "",
label: '',
value: null,
errors: Immutable.fromJS([])
errors: []
};
handleInput (ev) {
@@ -23,9 +22,10 @@ class AuthInput extends React.Component {
}
renderErrorList () {
if (this.props.errors.size) {
if (this.props.errors.length) {
return (
<div className='auth-error-message has-error'>
<div className="auth-error-message has-error">
{this.props.errors.map((err, i) => {
return (
<p className="control-label inline-error-item"
@@ -53,7 +53,7 @@ class AuthInput extends React.Component {
return (
<div>
<Input {...this.props}
bsStyle={(this.props.errors.size) ? "error" : null}
bsStyle={(this.props.errors.length) ? "error" : null}
onChange={this.handleInput.bind(this)} />
{this.renderErrorList()}
</div>

View File

@@ -0,0 +1,30 @@
/**
* Created by andrew on 21/02/16.
*/
export const customerInfoMap = ({
ssn,
address1,
address2,
city, //: "Moscow"
email, //: "arevinsky@gmail.com"
fname, //: "Andrew"
lname, //: "Revinsky"
phoneNumber, //: "+79031570864"
state, //: "Kentucky"
zip //: "125315"
}) => ({
"name": {
"firstName": fname,
"lastName": lname
},
email,
ssn,
"phoneNumber": phoneNumber,
"address": {
"street1": address1,
"street2": address2,
city,
state,
"zipCode": zip
}
});

View File

@@ -44,3 +44,29 @@ textarea {
height: 100%;
min-height: 100%;
}
body {
padding-bottom: 50px;
/* height: 100%; */
/* min-height: 100%; */
height: auto;
}
.footer-navigation {
height: 1px;
& > .container {
height: 100%;
& > * {
top: 50%;
transform: translateY(-50%);
}
}
}
.page-header {
padding-bottom: 9px;
margin: 0px 0 20px;
border-bottom: 1px solid #eee;
}
h1 {
margin-top: .5em;
}

View File

@@ -0,0 +1,38 @@
/**
* Created by andrew on 25/02/16.
*/
import T from '../../constants/ACTION_TYPES';
const initialState = {
loading: false,
valid: false,
errors: null
};
export const authReducer = (state = {...initialState}, action) => {
switch(action.type) {
case T.AUTH.AUTHENTICATE_START:
return {
...state,
loading: true
};
case T.AUTH.AUTHENTICATE_COMPLETE:
return {
...state,
loading: false,
errors: null,
valid: true
};
case T.AUTH.AUTHENTICATE_ERROR:
return {
...state,
loading: false,
errors: "Invalid token",
valid: false
};
default: return state;
}
};

View File

@@ -0,0 +1,56 @@
/**
* Created by andrew on 25/02/16.
*/
import T from '../../constants/ACTION_TYPES';
import createDataReducer from '../createDataReducer';
export const configReducer = createDataReducer([
T.AUTH.CONFIGURE_START,
T.AUTH.CONFIGURE_COMPLETE,
T.AUTH.CONFIGURE_ERROR
],
'config',
'config',
(c = {}) => ({ ...c })
);
//
// const configInitialState = {
// loading: true,
// errors: null,
// config: null
// };
//
// export const configReducer = (state = {...configInitialState}, action) => {
// switch(action.type) {
// case T.AUTH.CONFIGURE_START:
// {
// return {
// ...state,
// loading: true
// };
// }
//
// case T.AUTH.CONFIGURE_COMPLETE: {
// const { config } = action;
// return {
// ...state,
// loading: false,
// errors: null,
// config
// };
// }
//
// case T.AUTH.CONFIGURE_ERROR:
// {
// const { errors } = action;
// return {
// ...state,
// loading: false,
// errors
// };
// }
//
// default:
// return state;
// }
// };

View File

@@ -0,0 +1,22 @@
/**
* Created by andrew on 25/02/16.
*/
import { combineReducers } from 'redux';
import { configReducer } from './configure';
import { authReducer } from './authenticate';
import { signInReducer } from './signin';
import { signUpReducer } from './signup';
import { signOutReducer } from './signout';
import { userReducer } from './user';
const authStateReducer = combineReducers({
configure: configReducer,
signIn: signInReducer,
signUp: signUpReducer,
signOut: signOutReducer,
authentication: authReducer,
user: userReducer
});
export default authStateReducer;

View File

@@ -0,0 +1,12 @@
/**
* Created by andrew on 25/02/16.
*/
import T from '../../constants/ACTION_TYPES';
import createFormReducer from '../createFormReducer';
export const signInReducer = createFormReducer([
T.AUTH.SIGN_IN_START,
T.AUTH.SIGN_IN_COMPLETE,
T.AUTH.SIGN_IN_ERROR,
T.AUTH.SIGN_IN_FORM_UPDATE
]);

View File

@@ -0,0 +1,26 @@
/**
* Created by andrew on 25/02/16.
*/
import T from '../../constants/ACTION_TYPES';
const signOutInitialState = {
loading: false,
errors: null
};
export const signOutReducer = (state = {...signOutInitialState}, action) => {
switch(action.type) {
case T.AUTH.SIGN_OUT_START:
return {
...state,
loading: true
};
case T.AUTH.SIGN_OUT_COMPLETE:
return {
...state,
loading: false,
errors: null
};
default: return state;
}
};

View File

@@ -0,0 +1,12 @@
/**
* Created by andrew on 25/02/16.
*/
import T from '../../constants/ACTION_TYPES';
import createFormReducer from '../createFormReducer';
export const signUpReducer = createFormReducer([
T.AUTH.SIGN_UP_START,
T.AUTH.SIGN_UP_COMPLETE,
T.AUTH.SIGN_UP_ERROR,
T.AUTH.SIGN_UP_FORM_UPDATE
]);

View File

@@ -0,0 +1,28 @@
/**
* Created by andrew on 25/02/16.
*/
import T from '../../constants/ACTION_TYPES';
const initalState = {
attributes: null,
isSignedIn: false
};
export const userReducer = (state = {...initalState}, action) => {
switch(action.type) {
case T.AUTH.AUTHENTICATE_COMPLETE:
case T.AUTH.SIGN_IN_COMPLETE: {
const { user } = action;
return {...state,
attributes: user,
isSignedIn: !!user
};
}
case T.AUTH.SIGN_OUT_COMPLETE:
case T.AUTH.AUTHENTICATE_ERROR:
return {
...initalState
};
default: return state;
}
};

View File

@@ -0,0 +1,43 @@
/**
* Created by andrew on 3/22/16.
*/
const createDataReducer = ([KEY_REQUEST, KEY_SUCCESS, KEY_ERROR], payloadActionNameProp = 'payload', payloadStateNameProp = 'data', payloadAssignFn = (k = []) => [...k]) => {
const initialState = {
loading: false,
errors: {},
[payloadStateNameProp]: payloadAssignFn()
};
return function formReducer(state = {...initialState}, action) {
switch(action.type) {
case KEY_REQUEST: {
return {
...state,
loading: true
}
}
case KEY_SUCCESS: {
const payload = action[payloadActionNameProp];
return {
...initialState,
[payloadStateNameProp]: payloadAssignFn(payload)
};
}
case KEY_ERROR:
{
const {error} = action;
return {
...state,
loading: false,
errors: Object.isSealed(error) ? {aggregate: error} : {...error}
}
}
default:
return state;
}
};
};
export default createDataReducer;

View File

@@ -0,0 +1,56 @@
/**
* Created by andrew on 3/22/16.
*/
const createFormReducer = ([KEY_REQUEST, KEY_SUCCESS, KEY_ERROR, KEY_UPDATE]) => {
const initialState = {
loading: false,
form: {},
errors: {}
};
return function formReducer(state = {...initialState}, action) {
switch(action.type) {
case KEY_REQUEST: {
return {
...state,
loading: true
}
}
case KEY_ERROR: {
const { error } = action;
return {
...state,
loading: false,
errors: error
}
}
case KEY_SUCCESS: {
return {
...initialState
}
}
case KEY_UPDATE: {
const { key, value } = action;
return {
...state,
form: {
...state.form,
[key]: value
},
errors: {
...state.errors,
aggregate: null,
[key]: null
}
}
}
default:
return state;
}
};
};
export default createFormReducer;

View File

@@ -0,0 +1,69 @@
/**
* Created by andrew on 15/03/16.
*/
/**
* Created by andrew on 15/03/16.
*/
import T from '../../constants/ACTION_TYPES';
import { combineReducers } from 'redux';
import createFormReducer from '../createFormReducer';
const ownAccountsReducer = (state = [], action ) => {
switch (action.type) {
case T.ACCOUNTS.LIST_COMPLETE: {
const { payload = [] } = action;
//const accounts = Object.keys(payload).map(key => payload[key]);
return [
...payload
];
}
default: return state;
}
};
const otherAccountsReducer = (state = [], action ) => {
switch (action.type) {
case T.AUTH.AUTHENTICATE_COMPLETE:
case T.AUTH.SIGN_IN_COMPLETE: {
const { user } = action;
const { toAccounts = [] } = user;
return otherAccountsReducer(state, {
type: T.ACCOUNTS.LIST_REF_COMPLETE,
payload: toAccounts
});
}
case T.ACCOUNTS.LIST_REF_COMPLETE: {
const { payload = {} } = action;
const accounts = Object.keys(payload).map(key => payload[key]);
return [
...accounts
];
}
default: return state;
}
};
const createAccountReducer = createFormReducer([
T.ACCOUNTS.CREATE_START,
T.ACCOUNTS.CREATE_COMPLETE,
T.ACCOUNTS.CREATE_ERROR,
T.ACCOUNTS.CREATE_FORM_UPDATE
]);
const editAccountReducer = createFormReducer([
T.ACCOUNTS.EDIT_START,
T.ACCOUNTS.EDIT_COMPLETE,
T.ACCOUNTS.EDIT_ERROR,
T.ACCOUNTS.EDIT_FORM_UPDATE
]);
export const accounts = combineReducers({
own: ownAccountsReducer,
other: otherAccountsReducer,
create: createAccountReducer,
edit: editAccountReducer
});

View File

@@ -0,0 +1,119 @@
/**
* Created by andrew on 18/03/16.
*/
import T from '../../constants/ACTION_TYPES';
const optionsLoaderInitialState = {
loading: false,
options: [],
value: ''
};
const initialState = {
loading: false,
form: {},
errors: {},
accountsDisabled: true,
ownersLookup: {
...optionsLoaderInitialState
},
accountsLookup: {
...optionsLoaderInitialState
}
};
const optionsLoaderReducer = (state = {...optionsLoaderInitialState}, action) => {
switch (action.type) {
case T.ACCOUNTS.CREATE_REF_OWNER_LOOKUP_START:
case T.ACCOUNTS.CREATE_REF_ACCOUNT_LOOKUP_START: {
const value = action.payload;
return {
...state,
loading: true,
value
};
}
case T.ACCOUNTS.CREATE_REF_OWNER_LOOKUP_COMPLETE:
case T.ACCOUNTS.CREATE_REF_ACCOUNT_LOOKUP_COMPLETE: {
const { payload } = action;
return {
...state,
loading: false,
options: payload === null ? state.options : payload
};
}
default:
return state;
}
};
export const bookmarkAccount = (state = {...initialState}, action) => {
switch (action.type) {
case T.ACCOUNTS.CREATE_REF_START: {
return {
...state,
loading: true
};
}
case T.ACCOUNTS.CREATE_REF_COMPLETE:{
return {
...initialState
};
}
case T.ACCOUNTS.CREATE_REF_ERROR: {
const { error } = action;
return {
...state,
loading: false,
errors: error
};
}
case T.ACCOUNTS.CREATE_REF_FORM_UPDATE:{
const { key, value } = action;
const isOwnerSetBlank = ((key == 'owner') && !value);
const isOwnerSelected = ((key == 'owner') && value);
const nextAccountsDisabled = isOwnerSelected ? false : state.accountsDisabled;
const nextForm = isOwnerSetBlank ? {
...state.form,
account: null,
[key]: value
} : {
...state.form,
[key]: value
};
const nextErrors = {
...state.errors,
[key]: null
};
return {
...state,
accountsDisabled: nextAccountsDisabled,
form: nextForm,
errors: nextErrors
};
}
case T.ACCOUNTS.CREATE_REF_OWNER_LOOKUP_START:
case T.ACCOUNTS.CREATE_REF_OWNER_LOOKUP_COMPLETE: {
return {
...state,
ownersLookup:
optionsLoaderReducer(state.ownersLookup, action)
};
}
case T.ACCOUNTS.CREATE_REF_ACCOUNT_LOOKUP_START:
case T.ACCOUNTS.CREATE_REF_ACCOUNT_LOOKUP_COMPLETE: {
return {
...state,
accountsLookup:
optionsLoaderReducer(state.accountsLookup, action)
};
}
default:
return state;
}
};

View File

@@ -1,13 +0,0 @@
import typeReducers from '../../utils/typeReducers'
import ACTION_TYPES from '../../constants/ACTION_TYPES'
const defaultState = {}
export default typeReducers(ACTION_TYPES.DOCUMENT_DATA, defaultState, {
UPDATE: (state, {id, data}) => ({
...state,
[id]: { ...state[id], ...data },
})
})

View File

@@ -0,0 +1,71 @@
/**
* Created by andrew on 15/03/16.
*/
import T from '../../constants/ACTION_TYPES';
const initialState = {
};
const nodeInitialState = {
loading: false,
data: {}
};
export const entities = (state = {...initialState}, action) => {
switch(action.type) {
case T.ENTITIES.REQUESTED: {
const { id } = action;
return {
...state,
[id]: null
}
}
case T.ENTITIES.RECEIVED: {
const { id, entity = {} } = action;
return {
...state,
[id]: {
...entity
}
}
}
case T.AUTH.AUTHENTICATE_COMPLETE:
case T.AUTH.SIGN_IN_COMPLETE:
{
const { user } = action;
const { toAccounts = [] } = user;
return {
...state,
...toAccounts
};
}
case T.ACCOUNTS.LIST_COMPLETE: {
const { payload } = action;
const hashMap = payload.reduce((memo, item) => {
memo[item.accountId] = item;
return memo;
}, {});
return {
...state,
...hashMap
};
}
case T.ACCOUNT.SINGLE_COMPLETE: {
const { payload = {} } = action;
const { accountId } = payload;
if (!accountId) {
return state;
}
return {
...state,
[accountId]: payload
};
}
case T.ENTITIES.RECEIVED_LIST:
default:
return state;
}
};

View File

@@ -0,0 +1,18 @@
/**
* Created by andrew on 15/03/16.
*/
import { combineReducers } from 'redux';
import { accounts } from './accounts';
import { transfers } from './transfers';
import { entities } from './entities';
import { bookmarkAccount } from './bookmarkAccount';
const dataReducer = combineReducers({
transfers,
entities,
accounts,
bookmarkAccount
});
export default dataReducer;

View File

@@ -0,0 +1,14 @@
/**
* Created by andrew on 15/03/16.
*/
/**
* Created by andrew on 15/03/16.
*/
import T from '../../constants/ACTION_TYPES';
import createListReducer from '../createDataReducer';
export const transfers = createListReducer([
T.TRANSFERS.LIST_START,
T.TRANSFERS.LIST_COMPLETE,
T.TRANSFERS.LIST_ERROR
]);

View File

@@ -1,21 +1,16 @@
import { combineReducers } from 'redux'
/**
* Created by andrew on 18/03/16.
*/
import { combineReducers } from 'redux';
import authStateReducer from './auth';
import appStateReducer from './data'
import uiReducer from './ui'
import navigation from './navigationReducer'
const mainReducer = combineReducers({
auth: authStateReducer,
data: appStateReducer,
ui: uiReducer
});
import documentListView from './view/documentListViewReducer'
import documentView from './view/documentViewReducer'
import documentData from './data/documentDataReducer'
export default combineReducers({
navigation,
view: combineReducers({
documentList: documentListView,
document: documentView,
}),
data: combineReducers({
document: documentData,
}),
})
export default mainReducer;

View File

@@ -1,19 +0,0 @@
import typeReducers from '../utils/typeReducers'
import ACTION_TYPES from '../constants/ACTION_TYPES'
const defaultState = {
transitioning: true,
location: null,
}
export default typeReducers(ACTION_TYPES.NAVIGATION, defaultState, {
START: () => ({
transitioning: true,
}),
COMPLETE: (state, {location}) => ({
transitioning: false,
location,
}),
})

View File

@@ -0,0 +1,42 @@
/**
* Created by andrew on 15/03/16.
*/
/**
* Created by andrew on 15/03/16.
*/
import T from '../../constants/ACTION_TYPES';
import { combineReducers } from 'redux';
const initialState = {
loading: false,
errors: []
};
export const account = (state = { ...initialState }, action ) => {
switch(action.type) {
case T.ACCOUNT.SINGLE_START: {
return {
...state,
loading: true
};
}
case T.ACCOUNT.SINGLE_COMPLETE: {
return {
...initialState
};
}
case T.ACCOUNT.SINGLE_ERROR: {
const { error } = action;
return {
...state,
loading: false,
errors: [ error ]
};
}
default:
return state;
}
};

View File

@@ -0,0 +1,9 @@
/**
* Created by andrew on 18/03/16.
*/
export const bookmarkAccount = (state = {}, action) => {
switch (action.type) {
default:
return state;
}
};

View File

@@ -0,0 +1,18 @@
/**
* Created by andrew on 18/03/16.
*/
import T from '../../constants/ACTION_TYPES';
export const error = (state = null, action ) => {
switch (action.type) {
case T.ERROR.STOP: {
return null;
}
case T.ERROR.START:
return action.payload;
default:
return state;
}
};

View File

@@ -0,0 +1,22 @@
/**
* Created by andrew on 15/03/16.
*/
/**
* Created by andrew on 25/02/16.
*/
import { combineReducers } from 'redux';
import { account } from './account';
import { error } from './errors';
import { bookmarkAccount } from './bookmarkAccount';
import { transfersMake } from './transfersMake';
const uiReducer = combineReducers({
account,
error,
bookmarkAccount,
transfersMake
});
export default uiReducer;

View File

@@ -0,0 +1,15 @@
/**
* Created by andrew on 15/03/16.
*/
/**
* Created by andrew on 15/03/16.
*/
import T from '../../constants/ACTION_TYPES';
import createFormReducer from '../createFormReducer';
export const transfersMake = createFormReducer([
T.TRANSFERS.MAKE_START,
T.TRANSFERS.MAKE_COMPLETE,
T.TRANSFERS.MAKE_ERROR,
T.TRANSFERS.MAKE_FORM_UPDATE
]);

View File

@@ -1,10 +0,0 @@
import typeReducers from '../../utils/typeReducers'
import ACTION_TYPES from '../../constants/ACTION_TYPES'
const defaultState = ''
export default typeReducers(ACTION_TYPES.DOCUMENT_LIST_VIEW, defaultState, {
SET_QUERY: (state, {query}) => query,
})

View File

@@ -1,53 +0,0 @@
import pick from 'object-pick'
import typeReducers from '../../utils/typeReducers'
import compact from '../../utils/compact'
import ACTION_TYPES from '../../constants/ACTION_TYPES'
const defaultState = {
unsavedChanges: {},
saveErrors: {},
}
export default typeReducers(ACTION_TYPES.DOCUMENT_VIEW, defaultState, {
// Update the current document data
UPDATE_DATA: (state, {id, data}) => ({
...state,
unsavedChanges: {
...state.unsavedChanges,
[id]: { ...state.unsavedChanges[id], ...data },
},
}),
// If there are fields marked as invalid which are now valid,
// mark them as valid
REMOVE_STALE_ERRORS: (state, {id, errors}) => ({
...state,
saveErrors: {
...state.saveErrors,
[id]: compact(pick(state.saveErrors[id], Object.keys(errors || {}))),
},
}),
// Set the errors to the passed in object
SET_ERRORS: (state, {id, errors}) => ({
...state,
saveErrors: {
...state.saveErrors,
[id]: errors
},
}),
// Remove errors/data for an id
CLEAR: (state, {id}) => ({
unsavedChanges: {
...state.unsavedChanges,
[id]: null,
},
saveErrors: {
...state.saveErrors,
[id]: null,
},
}),
})

View File

@@ -0,0 +1,12 @@
/**
* Created by andrew on 15/03/16.
*/
export function makeActionCreator(type, ...argNames) {
return function(...args) {
const action = { type };
argNames.forEach((arg, index) => {
action[argNames[index]] = args[index]
});
return action;
};
}

View File

@@ -0,0 +1,173 @@
/**
* Created by andrew on 12/03/16.
*/
import fetch from './fetch';
import {
getEmailSignInUrl,
getEmailSignUpUrl,
getCurrentUserUrl,
getAccountsUrl,
getCustomersUrl,
getTransfersUrl
} from "./sessionStorage";
import root from './root';
import { parseResponse } from "./handleFetchResponse";
function makeQuery(params) {
return Object.keys(params).map(key => [encodeURIComponent(key), encodeURIComponent(params[key])].join('=')).join('&');
}
export function apiSignIn(body) {
return fetch(getEmailSignInUrl(), {
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: "post",
body: root.JSON.stringify(body)
}).then(parseResponse);
}
export function apiSignUp(body) {
return fetch(getEmailSignUpUrl(), {
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: "post",
body: root.JSON.stringify(body)
}).then(parseResponse);
}
export function apiGetCurrentUser() {
return fetch(getCurrentUserUrl(), {
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: "get"
}).then(parseResponse);
}
export function apiCreateAccount(customerId, {
title, balance: initialBalance, description }) {
//{
//"accountId": "0000015377cf131b-a250093f26850000"
//}
return fetch(getAccountsUrl(), {
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: "post",
body: root.JSON.stringify({
customerId,
title,
initialBalance,
description })
}).then(parseResponse);
}
export function apiCreateRefAccount(customerId, {
owner, account: accountId, title, description }) {
return fetch(`${getCustomersUrl()}/${customerId}/toaccounts`, {
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: "post",
body: root.JSON.stringify({
owner,
id: accountId,
title,
description })
}).then(parseResponse);
}
export function apiMakeTransfer(fromAccountId, {
account, amount, description }) {
return fetch(getTransfersUrl(), {
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: "post",
body: root.JSON.stringify({
"amount": amount,
"fromAccountId": fromAccountId,
"toAccountId": account,
description
})
}).then(parseResponse);
}
export function apiRetrieveAccounts(customerId) {
return fetch(`${getAccountsUrl()}?${makeQuery({ customerId })}`, {
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: "get"
}).then(parseResponse);
}
export function apiRetrieveTransfers(accountId) {
return fetch(`${getAccountsUrl()}/${accountId}/history`, {
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: "get"
}).then(parseResponse);
}
export function apiRetrieveAccount(accountId) {
return fetch(`${getAccountsUrl()}/${accountId}`, {
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: "get"
}).then(parseResponse);
}
export function apiDeleteAccount(accountId) {
return Promise.reject({
message: '\'Delete Account\' is not implemented.'
});
return fetch(`${getAccountsUrl()}/${accountId}`, {
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: "delete"
}).then(parseResponse);
}
export function apiRetrieveUsers(email) {
return fetch(`${getCustomersUrl()}?${makeQuery({ email })}`, {
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: "get"
}).then(parseResponse);
}
export function apiRetrieveUser(customerId) {
return fetch(`${getCustomersUrl()}/${ customerId }`, {
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: "get"
}).then(parseResponse);
}

View File

@@ -0,0 +1,122 @@
/**
* Created by andrew on 26/02/16.
*/
import * as C from "./constants";
import fetch from "./fetch";
import parseEndpointConfig from "./parseEndpointConfig";
import { setEndpointKeys } from "../actions/configure";
import {
getCurrentSettings,
setCurrentSettings,
getInitialEndpointKey,
setDefaultEndpointKey,
setCurrentEndpoint,
setCurrentEndpointKey,
retrieveData,
persistData,
destroySession
} from "./sessionStorage";
// can't use "window" with node app
var root = Function("return this")() || (42, eval)("this");
const defaultSettings = {
//proxyIf: function() { return false; },
//proxyUrl: "/proxy",
forceHardRedirect: false,
storage: "cookies",
cookieExpiry: 14,
cookiePath: "/",
initialCredentials: null,
passwordResetSuccessUrl: function() {
return root.location.href;
},
confirmationSuccessUrl: function() {
return root.location.href;
},
tokenFormat: {
"access-token": "{{ access-token }}"
//"token-type": "Bearer",
//client: "{{ client }}",
//expiry: "{{ expiry }}",
//uid: "{{ uid }}"
},
parseExpiry: function(headers){
// convert from ruby time (seconds) to js time (millis)
return (parseInt(headers["expiry"], 10) * 1000) || null;
},
handleLoginResponse: function(resp) {
return resp.data;
},
handleAccountUpdateResponse: function(resp) {
return resp.data;
},
handleTokenValidationResponse: function(resp) {
return resp.data;
}
};
// save session configuration
export function applyConfig({ dispatch, endpoint={}, settings={}, reset=false } = {}) {
if (settings.currentLocation && settings.currentLocation.match(/blank=true/)) {
return Promise.resolve({blank: true});
}
let currentEndpointKey;
if (reset) {
resetConfig();
}
if (settings.initialCredentials) {
currentEndpointKey = settings.initialCredentials.currentEndpointKey;
}
setCurrentSettings({ ...defaultSettings, ...settings });
const currentHeaders = retrieveData(C.SAVED_CREDS_KEY) || {};
const accessToken = currentHeaders["access-token"];
//if (authRedirectHeaders && authRedirectHeaders["access-token"]) {
if (!accessToken) {
destroySession();
}
let { defaultEndpointKey, currentEndpoint } = parseEndpointConfig(
endpoint, getInitialEndpointKey()
);
if (!currentEndpointKey) {
currentEndpointKey = defaultEndpointKey;
}
// persist default config key with session storage
setDefaultEndpointKey(defaultEndpointKey);
setCurrentEndpoint(currentEndpoint);
dispatch(setEndpointKeys(
Object.keys(currentEndpoint),
currentEndpointKey,
defaultEndpointKey));
setCurrentEndpointKey(currentEndpointKey);
return Promise.resolve();
}

View File

@@ -0,0 +1,8 @@
/**
* Created by andrew on 26/02/16.
*/
export const INITIAL_CONFIG_KEY = "default";
export const DEFAULT_CONFIG_KEY = "defaultConfigKey";
export const SAVED_CONFIG_KEY = "currentConfigName";
export const SAVED_CREDS_KEY = "authHeaders";
export const SAVED_USER_INFO = "user-info";

View File

@@ -0,0 +1,83 @@
/**
* Created by andrew on 26/02/16.
*/
import originalFetch from "isomorphic-fetch";
import * as C from "./constants";
import {
retrieveData,
persistData,
getTokenFormat,
isApiRequest
} from "./sessionStorage";
function getAuthHeaders(url) {
if (isApiRequest(url)) {
// fetch current auth headers from storage
let currentHeaders = retrieveData(C.SAVED_CREDS_KEY) || {},
nextHeaders = {};
if (currentHeaders === 'undefined') {
currentHeaders = {};
}
// bust IE cache
nextHeaders["If-Modified-Since"] = "Mon, 26 Jul 1997 05:00:00 GMT";
// set header for each key in `tokenFormat` config
for (var key in getTokenFormat()) {
if (key in currentHeaders) {
nextHeaders[key] = currentHeaders[key];
}
}
return nextHeaders;
} else {
return {};
}
}
function updateAuthCredentials(resp) {
// check config apiUrl matches the current response url
if (isApiRequest(resp.url)) {
// set header for each key in `tokenFormat` config
var newHeaders = {};
// set flag to ensure that we don't accidentally nuke the headers
// if the response tokens aren't sent back from the API
var blankHeaders = true;
// set header key + val for each key in `tokenFormat` config
for (var key in getTokenFormat()) {
newHeaders[key] = resp.headers.get(key);
if (newHeaders[key]) {
blankHeaders = false;
}
}
// persist headers for next request
if (!blankHeaders) {
persistData(C.SAVED_CREDS_KEY, newHeaders);
}
}
return resp;
}
export default function (url, options = {}) {
if (!options.headers) {
options.headers = {}
}
options.headers = {
...options.headers,
...getAuthHeaders(url)
};
//extend(options.headers, getAuthHeaders(url));
return originalFetch(url, options)
.then(resp => updateAuthCredentials(resp));
}

View File

@@ -0,0 +1,33 @@
/**
* Created by andrew on 26/02/16.
*/
export function parseResponse (response) {
let json = response.json();
if (response.status >= 200 && response.status < 300) {
return json;
} else {
//error: "Bad Request"
//exception: "org.springframework.web.bind.MethodArgumentNotValidException"
//message: "Validation failed for argument at index 0 in method: public rx.Observable<net.chrisrichardson.eventstore.javaexamples.banking.common.customers.CustomerResponse> net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.customers.CustomerController.createCustomer(net.chrisrichardson.eventstore.javaexamples.banking.common.customers.CustomerInfo), with 3 error(s): [Field error in object 'customerInfo' on field 'ssn': rejected value [null]; codes [NotNull.customerInfo.ssn,NotNull.ssn,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [customerInfo.ssn,ssn]; arguments []; default message [ssn]]; default message [may not be null]] [Field error in object 'customerInfo' on field 'email': rejected value [null]; codes [NotNull.customerInfo.email,NotNull.email,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [customerInfo.email,email]; arguments []; default message [email]]; default message [may not be null]] [Field error in object 'customerInfo' on field 'phoneNumber': rejected value [null]; codes [NotNull.customerInfo.phoneNumber,NotNull.phoneNumber,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [customerInfo.phoneNumber,phoneNumber]; arguments []; default message [phoneNumber]]; default message [may not be null]] "
//path: "/customers"
//status: 400
//timestamp: 1458002123103
return json.then(({ message, ...rest }) => {
if (!message) {
return rest;
}
const jvmPattern = /\[Field error in object '\w+' on field '(\w+)'/gm;
let errors = {};
message.replace(jvmPattern, (m, name) => {
errors[name] = ['Required'];
});
if (Object.keys(errors).length) {
return { errors };
}
return { errors: message };
}).then(err => Promise.reject(err));
}
}

View File

@@ -0,0 +1,65 @@
/**
* Created by andrew on 26/02/16.
*/
import * as C from "./constants";
// base endpoint that other endpoints extend from
const defaultEndpoint = {
apiUrl: "/api",
signOutPath: "/auth/sign_out",
emailSignInPath: "/auth/sign_in",
emailRegistrationPath: "/auth",
accountUpdatePath: "/auth",
accountDeletePath: "/auth",
passwordResetPath: "/auth/password",
passwordUpdatePath: "/auth/password",
tokenValidationPath: "/auth/validate_token",
authProviderPaths: {
github: "/auth/github",
facebook: "/auth/facebook",
google: "/auth/google_oauth2"
}
};
function getFirstObjectKey (obj) {
for (var key in obj) {
return key;
}
}
export default function parseEndpointConfig(endpoint, defaultEndpointKey = null) {
// normalize so opts is always an array of objects
if (endpoint.constructor !== Array) {
// single config will always be called 'default' unless set
// by previous session
defaultEndpointKey = C.INITIAL_CONFIG_KEY;
// config should look like {default: {...}}
var defaultConfig = {};
defaultConfig[defaultEndpointKey] = endpoint;
// endpoint should look like [{default: {...}}]
endpoint = [defaultConfig];
}
let currentEndpoint = {};
// iterate over config items, extend each from defaults
for (var i = 0; i < endpoint.length; i++) {
var configName = getFirstObjectKey(endpoint[i]);
// set first as default config
if (!defaultEndpointKey) {
defaultEndpointKey = configName;
}
// save config to `configs` hash
currentEndpoint[configName] = {
...defaultEndpoint,
...endpoint[i][configName]
};
}
return { defaultEndpointKey, currentEndpoint };
}

View File

@@ -0,0 +1,134 @@
/**
* Created by andrew on 26/02/16.
*/
import querystring from "querystring";
export function normalizeTokenKeys (params) {
// normalize keys
if (params.token) {
params["access-token"] = params.token;
delete params.token;
}
if (params.auth_token) {
params["access-token"] = params.auth_token;
delete params.auth_token;
}
if (params.client_id) {
params.client = params.client_id;
delete params.client_id;
}
if (params.config) {
params.endpointKey = params.config;
delete params.config;
}
return params;
};
const getAnchorSearch = function(location) {
const rawAnchor = location.hash || "",
arr = rawAnchor.split("?");
return (arr.length > 1) ? arr[1] : null;
};
const getSearchQs = function(location) {
const rawQs = location.search || "",
qs = rawQs.replace("?", ""),
qsObj = (qs) ? querystring.parse(qs) : {};
return qsObj;
};
const getAnchorQs = function(location) {
const anchorQs = getAnchorSearch(location),
anchorQsObj = (anchorQs) ? querystring.parse(anchorQs) : {};
return anchorQsObj;
};
const stripKeys = function(obj, keys) {
for (var q in keys) {
delete obj[keys[q]];
}
return obj;
};
export function getAllParams (location) {
return {
...getAnchorQs(location),
...getSearchQs(location)
};
};
const buildCredentials = function(location, keys) {
const params = getAllParams(location);
let authHeaders = {};
for (var key of keys) {
authHeaders[key] = params[key];
}
return normalizeTokenKeys(authHeaders);
};
// this method is tricky. we want to reconstruct the current URL with the
// following conditions:
// 1. search contains none of the supplied keys
// 2. anchor search (i.e. `#/?key=val`) contains none of the supplied keys
// 3. all of the keys NOT supplied are presevered in their original form
// 4. url protocol, host, and path are preserved
const getLocationWithoutParams = function(currentLocation, keys) {
// strip all values from both actual and anchor search params
let newSearch = querystring.stringify(stripKeys(getSearchQs(currentLocation), keys)),
newAnchorQs = querystring.stringify(stripKeys(getAnchorQs(currentLocation), keys)),
newAnchor = (currentLocation.hash || "").split("?")[0];
if (newSearch) {
newSearch = "?" + newSearch;
}
if (newAnchorQs) {
newAnchor += "?" + newAnchorQs;
}
if (newAnchor && !newAnchor.match(/^#/)) {
newAnchor = "#/" + newAnchor;
}
// reconstruct location with stripped auth keys
const newLocation = currentLocation.pathname + newSearch + newAnchor;
return newLocation;
};
export default function getRedirectInfo(currentLocation) {
if (!currentLocation) {
return {};
} else {
let authKeys = [
"access-token",
"token",
"auth_token",
"config",
"client",
"client_id",
"expiry",
"uid",
"reset_password",
"account_confirmation_success"
];
var authRedirectHeaders = buildCredentials(currentLocation, authKeys);
var authRedirectPath = getLocationWithoutParams(currentLocation, authKeys);
if (authRedirectPath !== currentLocation) {
return { authRedirectHeaders, authRedirectPath };
} else {
return {};
}
}
}

View File

@@ -0,0 +1,16 @@
/**
* Created by andrew on 11/03/16.
*/
export default function read(src, path = '', defaultVal = null) {
const [pathItem = null, ...rest] = path.split('.');
if (pathItem === null ) {
return src || defaultVal;
} else if (rest.length === 0) {
if (!src) { return defaultVal; }
return src[pathItem] || defaultVal;
}
if (!src) { return defaultVal; }
return read(src[pathItem], rest.join('.'), defaultVal);
}

View File

@@ -0,0 +1,6 @@
/**
* Created by andrew on 27/02/16.
*/
// even though this code shouldn't be used server-side, node will throw
// errors if "window" is used
export default Function("return this")() || (42, eval)("this");

View File

@@ -0,0 +1,208 @@
/**
* Created by andrew on 26/02/16.
*/
import Cookies from "js-cookie";
import * as C from "./constants";
import root from './root';
//import "babel-polyfill";
// stateful variables that persist throughout session
root.authState = {
currentSettings: {},
currentEndpoint: {},
defaultEndpointKey: 'default'
};
export function setCurrentSettings (s) {
root.authState.currentSettings = s;
}
export function getCurrentSettings () {
return root.authState.currentSettings;
}
export function setCurrentEndpoint (e) {
root.authState.currentEndpoint = e;
}
export function getCurrentEndpoint () {
return root.authState.currentEndpoint;
}
/**
* @deprecated
* @param k
*/
export function setCurrentEndpointKey (k) {
persistData(C.SAVED_CONFIG_KEY, k || getDefaultEndpointKey());
}
export function getCurrentEndpointKey () {
return getDefaultEndpointKey();
}
/**
* @deprecated
* @param k
*/
export function setDefaultEndpointKey (k) {
persistData(C.DEFAULT_CONFIG_KEY, k);
}
export function getDefaultEndpointKey () {
return retrieveData(C.DEFAULT_CONFIG_KEY);
}
// reset stateful variables
export function resetConfig () {
root.authState = root.authState || {};
root.authState.currentSettings = {};
root.authState.currentEndpoint = {};
destroySession();
}
export function destroySession () {
var sessionKeys = [
C.SAVED_CREDS_KEY,
C.SAVED_CONFIG_KEY,
C.SAVED_USER_INFO
];
for (var key in sessionKeys) {
key = sessionKeys[key];
// kill all local storage keys
if (root.localStorage) {
root.localStorage.removeItem(key);
}
// remove from base path in case config is not specified
Cookies.remove(key, {
path: root.authState.currentSettings.cookiePath || "/"
});
}
}
function unescapeQuotes (val) {
return val && val.replace(/("|')/g, "");
}
export function getInitialEndpointKey () {
return unescapeQuotes(
Cookies.get(C.SAVED_CONFIG_KEY) ||
(root.localStorage && root.localStorage.getItem(C.SAVED_CONFIG_KEY))
);
}
export function isApiRequest(url) {
return true;
}
export function getSessionEndpointKey () {
return getCurrentEndpointKey();
}
export function getSessionEndpoint (k) {
return getCurrentEndpoint()[getSessionEndpointKey()];
}
//// only should work for current session
//export function getSignOutUrl (endpointKey) {
// return `${getApiUrl(endpointKey)}${getSessionEndpoint(endpointKey).signOutPath}`
//}
export function getEmailSignInUrl () {
return `${getSessionEndpoint().emailSignInPath}`
}
export function getEmailSignUpUrl () {
return getCustomersUrl();
}
export function getCurrentUserUrl () {
return `${getSessionEndpoint().currentUserPath}`
}
export function getAccountsUrl () {
return `${getSessionEndpoint().accountsPath}`
}
export function getCustomersUrl () {
return `${getSessionEndpoint().customersPath}`
}
export function getTransfersUrl () {
return `${getSessionEndpoint().transfersPath}`
}
/**
* @deprecated
* @param key
* @returns {string|string}
*/
export function getApiUrl(key) {
let configKey = getSessionEndpointKey(key);
return root.authState.currentEndpoint[configKey].apiUrl;
}
export function getTokenFormat() {
return root.authState.currentSettings.tokenFormat;
}
export function persistUserData(user) {
persistData(C.SAVED_USER_INFO, user);
}
export function retrieveUserData() {
return retrieveData(C.SAVED_USER_INFO);
}
export function retrieveHeaders() {
return retrieveData(C.SAVED_CREDS_KEY) || {};
}
export function persistData (key, val) {
val = root.JSON.stringify(val);
switch (root.authState.currentSettings.storage) {
case "localStorage":
root.localStorage.setItem(key, val);
break;
default:
Cookies.set(key, val, {
expires: root.authState.currentSettings.cookieExpiry,
path: root.authState.currentSettings.cookiePath
});
break;
}
}
export function retrieveData (key) {
var val = null;
switch (root.authState.currentSettings.storage) {
case "localStorage":
val = root.localStorage && root.localStorage.getItem(key);
break;
default:
val = Cookies.get(key);
break;
}
// if value is a simple string, the parser will fail. in that case, simply
// unescape the quotes and return the string.
try {
// return parsed json response
return JSON.parse(val);
} catch (err) {
// unescape quotes
return unescapeQuotes(val);
}
}

View File

@@ -1,10 +0,0 @@
import compact from '../utils/compact'
export default function documentValidator(data) {
return compact({
titleExists:
!data.title &&
"You must specify a title for your document",
})
}

View File

@@ -2,18 +2,244 @@
* Created by andrew on 12/02/16.
*/
import React from "react";
import { PageHeader } from "react-bootstrap";
//import { PageHeader } from "react-bootstrap";
import { connect } from "react-redux";
import { PageHeader, OverlayTrigger, Tooltip, Grid, Col, Row, Nav, NavItem, ButtonGroup, Button, Table } from "react-bootstrap";
import * as BS from "react-bootstrap";
import Select from "react-select";
import Spinner from "react-loader";
import Input from "../controls/bootstrap/Input";
import { Money, moneyText } from '../components/Money';
import { TransfersTable } from '../components/TransfersTable';
import { Link, IndexLink} from "react-router";
import IndexPanel from "./../components/partials/IndexPanel";
import * as Modals from './modals';
import * as A from '../actions/entities';
import read from '../utils/readProp';
const resetModals = {
showAccountModal: false
};
export class Account extends React.Component {
constructor(...args) {
super(...args);
this.state = { ...resetModals };
}
loadAccountInfo() {
const {
id: customerId
} = this.props.auth.user.attributes;
this.props.dispatch(A.fetchOwnAccounts(customerId));
const { dispatch, params } = this.props;
const { accountId } = params;
dispatch(A.fetchAccount(accountId));
dispatch(A.getTransfers(accountId));
}
componentWillMount() {
this.loadAccountInfo();
}
createAccountModal() {
this.setState({
showAccountModal: true
});
}
createAccountModalConfirmed() {
debugger;
}
close() {
this.setState({
...resetModals
});
}
handleInput(key, value) {
this.props.dispatch(A.makeTransferFormUpdate(key, value));
}
initiateTransfer(){
const { dispatch, params, transfer } = this.props;
const { accountId } = params;
dispatch(A.makeTransfer(accountId, transfer.form ))
.then(() => {
setTimeout(() => {
this.loadAccountInfo();
}, 500);
});
}
render () {
const { showAccountModal } = this.state;
const { params } = this.props;
const { loading, errors } = this.props.ui;
const { entities, accounts } = this.props.data;
const { accountId } = params;
const account = entities[accountId];
if (loading) {
return (<h2><Spinner ref="spinner" loaded={false} /> Loading..</h2>);
}
if (!account) {
if (errors.length) {
return (<h2>Error loading specified account</h2>);
} else {
return (<h2><Spinner ref="spinner" loaded={false} /> Loading..</h2>);
}
}
const transferTo = [].concat(accounts.own.reduce((memo, item, idx) => {
const { balance, title, accountId: itemAccountId } = item;
if (itemAccountId != accountId) {
memo.push({
value: itemAccountId ,
label: `${title}: ${ moneyText(balance) }`
});
}
return memo;
}, []),
accounts.other.reduce((memo, item, idx) => {
if (!((item.id == accountId) || (item.accountId == accountId))) {
memo.push({
value: item.accountId || item.id,
label: `${item.title}${ item.description ? ': ' + item.description.substr(0, 10): '' }`
});
}
return memo;
}, []));
const { title: titleRaw, description: descriptionRaw, balance } = account;
const title = titleRaw || '[No title]';
const description = descriptionRaw || '[No description]';
const transferDisabled = this.props.transfer.loading;
return (
<div>
<PageHeader>Account page</PageHeader>
<p>This page should only visible to authenticated users.</p>
<PageHeader>
Account
<Nav pullRight={true}>
<ButtonGroup>
<Button bsStyle={"link"} onClick={this.createAccountModal.bind(this)}>Edit</Button>
</ButtonGroup>
</Nav>
</PageHeader>
<Row>
<IndexPanel header="Account Info:">
<Row>
<Col xs={4}>Title:</Col>
<Col xs={8}><strong>{ title }</strong></Col>
</Row>
<Row>
<Col xs={4}>Balance:</Col>
<Col xs={8}><strong><Money amount={balance} /></strong></Col>
</Row>
<Row>
<Col xs={4}>Description:</Col>
<Col xs={8}><strong>{ description }</strong></Col>
</Row>
</IndexPanel>
</Row>
<Row>
<Col xs={12}>
<h3>You can transfer money to accounts:</h3>
</Col>
</Row>
<Row>
<Col sm={4}>
<label>Transfer To:</label>
<Select
value={read(this.props.transfer, 'form.account', '')}
clearable={true}
options={transferTo}
disabled={transferDisabled}
onChange={this.handleInput.bind(this, 'account')}
/>
</Col>
<Col sm={3}>
<Input type="text"
className=""
label="Amount:"
placeholder="Amount"
name="amount"
addonBefore={
(<BS.Glyphicon glyph="usd" />)
}
addonAfter=".00"
disabled={transferDisabled}
value={read(this.props.transfer, 'form.amount', '')}
errors={read(this.props.transfer, 'errors.amount', []) || []}
onChange={this.handleInput.bind(this, 'amount')}
/>
</Col>
<Col sm={3}>
<Input type="textarea"
className=""
label="Description:"
placeholder="Description"
name="description"
disabled={transferDisabled}
value={read(this.props.transfer, 'form.description', '') || ''}
errors={read(this.props.transfer, 'errors.description', []) || []}
onChange={this.handleInput.bind(this, 'description')}
/>
</Col>
<Col sm={2}>
<br/>
<Button bsStyle="primary"
onClick={this.initiateTransfer.bind(this)}>Transfer</Button>
</Col>
</Row>
<Row>
<Col xs={12}>
<h3>Account History:</h3>
</Col>
</Row>
<TransfersTable { ...this.props.transfers } />
<Modals.NewAccountModal show={showAccountModal}
action={this.createAccountModalConfirmed.bind(this)}
account={{ loading: true }}
onHide={this.close.bind(this)}
key={0} />
</div>
);
}
}
export default connect(({auth}) => ({auth}))(Account);
export default connect(({
app
}) => ({
auth: app.auth,
data: app.data,
transfers: app.data.transfers,
ui: app.ui.account,
transfer: app.ui.transfersMake
}))(Account);

View File

@@ -1,136 +0,0 @@
/**
* Created by andrew on 17/02/16.
*/
import React from "react";
import IndexPanel from "./../components/partials/IndexPanel";
import { PageHeader, OverlayTrigger, Tooltip, Row, ButtonGroup, Table } from "react-bootstrap";
import { Link, IndexLink} from "react-router";
import { connect } from "react-redux";
import * as BSTheme from "redux-auth/bootstrap-theme";
import * as DefaultTheme from "redux-auth";
import Select from "react-select";
class Main extends React.Component {
updateTheme (theme) {
//this.props.dispatch(updateDemoTheme(theme));
}
updateEndpoint (endpoint) {
//this.props.dispatch(updateDemoEndpoint(endpoint));
}
render () {
console.log("page endpoint", this.props.pageEndpoint);
let Theme = BSTheme;
let themePath = "/material-ui-theme";
let endpointAttr = (this.props.pageEndpoint === "default")
? ""
: "endpoint=\"evilUser\"";
switch(this.props.theme) {
case "default":
Theme = DefaultTheme;
themePath = "";
break;
case "bootstrap":
Theme = BSTheme;
themePath = "/bootstrap-theme";
break;
}
const deployTooltip = (<Tooltip>
Create a new instance of this demo on your own Heroku server.
</Tooltip>);
return (
<div>
<PageHeader>
Money Transfer Demo
<OverlayTrigger overlay={deployTooltip} placement="left">
<a
className="pull-right"
href="https://heroku.com/deploy?template=https://github.com/lynndylanhurley/redux-auth-demo">
<img src="https://www.herokucdn.com/deploy/button.svg" />
</a>
</OverlayTrigger>
</PageHeader>
<Row>
<IndexPanel header="Current User">
<label>current user provider</label>
<p>{this.props.currentUserUid}</p>
<label>current user uid</label>
<p>{this.props.currentUserProvider}</p>
<IndexLink to="/">Home</IndexLink><br/>
<Link to="/signin">Login</Link><br/>
<Link to="/register">Register</Link><br/>
<Link to="/account">Account</Link><br/>
<label>currently selected theme</label>
<Select
value={this.props.theme}
clearable={false}
options={[
{value: "default", label: "Default"},
{value: "bootstrap", label: "Bootstrap"},
{value: "materialUi", label: "Material UI"}
]}
onChange={this.updateTheme.bind(this)} />
<label>currently selected endpoint</label>
<Select
value={this.props.pageEndpoint}
clearable={false}
options={[
{value: "default", label: "Default User Class"},
{value: "evilUser", label: "Alternate User Class"}
]}
onChange={this.updateEndpoint.bind(this)} />
<Table>
<thead>
<tr>
<th colSpan={2}>
ajax test
</th>
</tr>
</thead>
<tbody>
<tr>
<td>Default user:</td>
<td></td>
</tr>
<tr>
<td>Alternate user class:</td>
<td></td>
</tr>
<tr>
<td>Group that includes both user classes:</td>
<td></td>
</tr>
</tbody>
</Table>
</IndexPanel>
<IndexPanel header="Other User"></IndexPanel>
</Row>
</div>
);
}
}
export default connect(({auth, demoUi = new Map()}) => {
return ({
currentUserUid: auth.getIn(["user", "attributes", "provider"]) || "none",
currentUserProvider: auth.getIn(["user", "attributes", "uid"]) || "none",
currentUserEndpoint: auth.getIn(["user", "endpointKey"]) || "none",
theme: demoUi.get("theme"),
pageEndpoint: demoUi.get("endpoint")
})
})(Main);

View File

@@ -0,0 +1,279 @@
/**
* Created by andrew on 17/02/16.
*/
import React from "react";
import { PageHeader, OverlayTrigger, Tooltip, Grid, Col, Row, Nav, NavItem, ButtonGroup, Button, Table } from "react-bootstrap";
import * as BS from "react-bootstrap";
import { Link, IndexLink} from "react-router";
import { connect } from "react-redux";
//import * as DefaultTheme from "redux-auth";
import Select from "react-select";
import * as Modals from './modals';
import IndexPanel from "./../components/partials/IndexPanel";
import * as A from '../actions/entities';
import read from '../utils/readProp';
import { Money } from '../components/Money';
const resetModals = {
showAccountModal: false,
show3rdPartyAccountModal: false,
showDeleteAccountModal: false
};
class MyAccounts extends React.Component {
constructor(...args) {
super(...args);
this.state = { ...resetModals };
}
componentWillMount() {
const {
id: customerId
} = this.props.auth.user.attributes;
this.props.dispatch(A.fetchOwnAccounts(customerId));
}
createAccountModal() {
this.setState({
showAccountModal: true
});
}
createAccountModalConfirmed(payload) {
const {
id: customerId
} = this.props.auth.user.attributes;
this.props.dispatch(A.accountCreate(customerId, payload))
.then((accountId) => {
this.close();
return this.props.dispatch(A.fetchOwnAccounts(customerId));
})
.catch(err => {
debugger;
this.props.dispatch(A.accountCreateError(err));
});
}
create3rdPartyAccountModal() {
this.setState({
show3rdPartyAccountModal: true
});
}
create3rdPartyAccountModalConfirmed(payload) {
const {
id: customerId
} = this.props.auth.user.attributes;
this.props.dispatch(A.accountRefCreate(customerId, payload))
.then(() => {
this.close();
return this.props.dispatch(A.fetchOwnAccounts(customerId));
})
.catch(err => {
debugger;
this.props.dispatch(A.accountRefCreateError(err));
});
}
remove3rdPartyAccountModal(accountId, evt) {
const account = this.props.app.entities[accountId];
this.setState({
accountToRemove: account,
showDeleteAccountModal: true
});
}
remove3rdPartyAccountModalConfirmed(accountId) {
const { customerId } = this.props;
this.props.dispatch(A.deleteAccount(customerId, accountId))
.then(() => {
this.close();
},
err => {
this.props.dispatch(A.errorMessageTimedOut(err && err.message || err));
this.close();
});
}
close() {
this.setState({
...resetModals
});
}
render () {
//const deployTooltip = (<Tooltip>
// Create a new instance of this demo on your own Heroku server.
//</Tooltip>);
const user = this.props.auth.user.attributes;
const {
id: customerId,
email = '',
ssn = '',
name = {},
phoneNumber = '',
address,
toAccounts
} = user;
const firstName = name.firstName || '';
const lastName = name.lastName || '';
const {
city,
state,
street1,
street2,
zipCode
} = address;
const {
showAccountModal,
show3rdPartyAccountModal,
showDeleteAccountModal } = this.state;
const { accountToRemove = null} = this.state;
const { error } = this.props;
const errorLine = error ? (<BS.Panel bsStyle="danger"><strong>{ error }</strong></BS.Panel>) : [];
const ownAccountsData = this.props.app.accounts.own || [];
//accountId: "000001537c2cf075-a250093f26850000"
//balance: 0
//description: null
//title: "Sample"
const ownAccounts = ownAccountsData.map(({
accountId, balance, description = '', title
}, idx) => (
<tr key={`own_${idx}`}>
<td key={0}><Link to={`/account/${accountId}`}>{ title }</Link>{
(description) ? [
(<br />),
<span>{ description }</span>
]: null
}</td>
<td key={1}><Money amount={balance} /></td>
<td key={2}><BS.Button bsStyle={"link"} onClick={this.remove3rdPartyAccountModal.bind(this, accountId)}><BS.Glyphicon glyph="remove" /></BS.Button></td>
</tr>
));
const refAccountsData = this.props.app.accounts.other || [];
const refAccounts = refAccountsData.map(({
title,
description = '',
id
}, idx) => (
<tr key={`ref_${idx}`}>
<td key={0}><Link to={`/account/${id}`}>{ title }</Link>{
(description) ? [
(<br />),
<span>{ description }</span>
]: null
}
</td>
<td key={1}></td>
<td key={2}><Button pullRight={true} bsStyle={"link"} onClick={this.remove3rdPartyAccountModal.bind(this, id)}><BS.Glyphicon glyph="remove" /></Button>
</td>
</tr>
));
const accounts = (!!(ownAccounts.length + refAccounts.length)) ? [].concat(ownAccounts, refAccounts) : (<tr>
<td colSpan={3}>No account exists: <Button bsStyle={"link"} onClick={this.createAccountModal.bind(this)}>create a new one</Button> or <Button bsStyle={"link"} onClick={this.create3rdPartyAccountModal.bind(this)}>add a recipient</Button></td>
</tr>);
return (
<div>
<PageHeader>
My Accounts
<Nav pullRight={true}>
<ButtonGroup>
<Button bsStyle={"link"} onClick={this.createAccountModal.bind(this)}>Create Account</Button>
<Button bsStyle={"link"} onClick={this.create3rdPartyAccountModal.bind(this)}>Add 3rd Party Recipients</Button>
</ButtonGroup>
</Nav>
</PageHeader>
{ errorLine }
<Row>
<IndexPanel header="Personal Info:">
<Row>
<Col xs={4}>Customer:</Col>
<Col xs={8}><strong>{ `${firstName} ${lastName}` }</strong></Col>
</Row>
<Row>
<Col xs={4}>Email:</Col>
<Col xs={8}><strong>{ email }</strong></Col>
</Row>
<Row>
<Col xs={4}>Phone:</Col>
<Col xs={8}><strong>{ phoneNumber }</strong></Col>
</Row>
<Row>
<Col xs={4}>SSN:</Col>
<Col xs={8}><strong>{ ssn }</strong></Col>
</Row>
</IndexPanel>
</Row>
<Table>
<thead>
<tr>
<th>Account Title</th>
<th>Balance</th>
<th></th>
</tr>
</thead>
<tbody>
{ accounts }
</tbody>
</Table>
<Modals.NewAccountModal show={showAccountModal}
action={this.createAccountModalConfirmed.bind(this)}
account={ this.props.app.accounts.create }
onHide={this.close.bind(this)}
key={0} />
<Modals.Add3rdPartyAccountModal show={show3rdPartyAccountModal}
action={this.create3rdPartyAccountModalConfirmed.bind(this)}
onHide={this.close.bind(this)}
key={1} />
<Modals.RemoveAccountBookmarkModal show={showDeleteAccountModal}
account={accountToRemove}
action={this.remove3rdPartyAccountModalConfirmed.bind(this)}
onHide={this.close.bind(this)}
key={2} />
</div>
);
}
}
export default connect(({ app }) => {
return ({
auth: app.auth,
app: app.data,
customerId: read(app, 'auth.user.isSignedIn', false) ? read(app, 'auth.user.attributes.id', null): null,
error: app.ui.error
})
})(MyAccounts);

View File

@@ -6,8 +6,12 @@ import { PageHeader } from "react-bootstrap";
import { connect } from "react-redux";
//import ButtonLoader from "./ButtonLoader";
import { Input } from "react-bootstrap";
import ButtonLoader from "../controls/bootstrap/ButtonLoader";
import * as BS from "react-bootstrap";
//import ButtonLoader from "../controls/bootstrap/ButtonLoader";
import {pushState} from "redux-router";
//export {bootstrap, materialUi} from "./views";
@@ -16,9 +20,25 @@ import ButtonLoader from "../controls/bootstrap/ButtonLoader";
//import { EmailSignInForm } from "redux-auth/bootstrap-theme";
import EmailSignInForm from "../controls/bootstrap/EmailSignInForm";
export class SignIn extends React.Component {
checkRedirect(props) {
if (props.auth.user.isSignedIn) {
props.dispatch(pushState(null, props.location.query.next));
//// redirect to login and add next param so we can redirect again after login
//const redirectAfterLogin = this.props.location.pathname;
//this.props.dispatch(pushState(null, `/signin?next=${redirectAfterLogin}`));
}
}
componentWillMount() {
this.checkRedirect(this.props);
}
componentWillReceiveProps(nextProps) {
this.checkRedirect(nextProps);
}
render () {
const signInProps = {
inputProps: {
@@ -31,13 +51,20 @@ export class SignIn extends React.Component {
}
};
return <EmailSignInForm />;
//return (
// <div>
// <PageHeader>Sign In First</PageHeader>
// <p>Unauthenticated users can't access the account page.</p>
// </div>
//);
return (
<BS.Well>
<PageHeader>Sign In</PageHeader>
<EmailSignInForm {...this.props} />
</BS.Well>
);
}
}
export default connect(({routes}) => ({routes}))(SignIn);
export default connect(({
//dispatch,
routes,
app
}) => ({
//dispatch,
routes,
auth: app.auth
}))(SignIn);

View File

@@ -4,25 +4,51 @@
import React from "react";
//import { PageHeader } from "react-bootstrap";
import { connect } from "react-redux";
import { pushState } from 'redux-router';
import read from '../utils/readProp';
import { PageHeader, OverlayTrigger, Tooltip, Row, ButtonGroup, Table } from "react-bootstrap";
import * as BS from "react-bootstrap";
import { Link, IndexLink} from "react-router";
//import { EmailSignUpForm } from "redux-auth/bootstrap-theme"
import EmailSignUpForm from "../controls/bootstrap/EmailSignUpForm";
export class SignUp extends React.Component {
checkRedirect(props) {
if (props.isAuthenticated) {
// redirect to login and add next param so we can redirect again after login
// const redirectAfterLogin = props.location.pathname;
props.dispatch(pushState(null, `/`));
}
}
componentWillMount() {
this.checkRedirect(this.props);
}
componentWillReceiveProps(nextProps) {
this.checkRedirect(nextProps);
}
render () {
return (
<div>
<PageHeader>
Register
</PageHeader>
<Row>
<EmailSignUpForm endpoint="default" />
</Row>
<BS.Well>
<EmailSignUpForm />
</BS.Well>
</div>
);
}
}
export default connect(({routes}) => ({routes}))(SignUp);
export default connect(({
routes,
app
}) => ({routes,
isAuthenticated: read(app, 'auth.user.isSignedIn', false)
}))(SignUp);

View File

@@ -0,0 +1,190 @@
/**
* Created by andrew on 20/02/16.
*/
import React from "react";
// import { PageHeader, OverlayTrigger, Modal, Tooltip, Grid, Col, Row, Nav, NavItem, ButtonGroup, Button, Table } from "react-bootstrap";
import * as BS from "react-bootstrap";
import ButtonLoader from "../../controls/bootstrap/ButtonLoader";
import Input from "../../controls/bootstrap/Input";
import AuxErrorLabel from "../../controls/bootstrap/AuxErrorLabel";
import read from '../../utils/readProp';
import { Link, IndexLink} from "react-router";
import { connect } from "react-redux";
import Select from "react-select";
import * as A from '../../actions/entities';
const formValidation = (payload) => ['owner', 'account', 'title', 'description'].reduce((memo, prop) => {
let result = [];
const value = (payload[prop] || '').replace(/(^\s+)|(\s+$)/g, '');
switch (prop) {
case 'owner':
case 'account':
case 'title':
if (/^$/.test(value)) {
result.push('required');
}
}
switch (prop) {
case 'description':
if (value.length > 400) {
result.push('need to less than 400 symbols long');
}
}
if (result.length) {
memo[prop] = result;
memo.hasErrors = true;
}
return memo;
}, {});
export class Add3rdPartyAccountModal extends React.Component {
handleInput(key, value) {
this.props.dispatch(A.accountRefCreateFormUpdate(key, value));
switch(key) {
case 'owner':
if (value) {
this.props.dispatch(A.createRefAccountLookup(value));
} else {
this.props.dispatch(A.createRefAccountLookupComplete([]));
}
}
}
handleSubmit(evt) {
evt.preventDefault();
const payload = { ...this.props.data.form };
const validationErrors = formValidation(payload);
if (validationErrors.hasErrors) {
this.props.dispatch(A.accountRefCreateError(validationErrors));
return;
}
const { action } = this.props;
if (action) {
action(payload);
}
}
onHide() {
this.props.dispatch(A.accountRefCreateComplete({}));
if (this.props.onHide) {
this.props.onHide();
}
}
getOwnersOptions(input) {
if (!input) {
return Promise.resolve({ options: [] });
}
return this.props.dispatch(A.createRefOwnerLookup(input));
}
render() {
const disabled = read(this.props.data, 'loading', false);
const ownersLoading = read(this.props.data, 'ownersLookup.loading', false);
const formErrors = read(this.props.data, 'errors.errors', '');
return (
<BS.Modal show={this.props.show} onHide={this.onHide.bind(this)} key={1}>
<BS.Modal.Header closeButton>
<BS.Modal.Title>Add 3rd Party Account</BS.Modal.Title>
</BS.Modal.Header>
<BS.Modal.Body>
<form>
<div className="form-group" style={{
display: formErrors ? 'block' : 'none'
}}>
<AuxErrorLabel
label="Form:"
errors={formErrors.length ? [formErrors] : [] }
/>
</div>
<label>Owner:</label>
<div className="form-group">
<Select
name="owner"
onBlurResetsInput={false}
asyncOptions={this.getOwnersOptions.bind(this)}
matchProp="label"
onChange={this.handleInput.bind(this, 'owner')}
value={read(this.props.data, 'form.owner', '')}
disabled={disabled}
/>
<AuxErrorLabel
label="Owner:"
errors={read(this.props.data, 'errors.owner', [])}
/>
</div>
<label>Account:</label>
<div className="form-group">
<Select
name="account"
value={read(this.props.data, 'form.account', '')}
disabled={ownersLoading || disabled}
clearable={false}
searchable={false}
options={read(this.props.data, 'accountsLookup.options', [])}
onChange={this.handleInput.bind(this, 'account')} />
<AuxErrorLabel
label="Owner:"
errors={read(this.props.data, 'errors.account', [])}
/>
</div>
<Input type="text"
className="account-create-description"
label="Title:"
placeholder="Title"
name="title"
disabled={disabled}
value={read(this.props.data, 'form.title', '')}
errors={read(this.props.data, 'errors.title', [])}
onChange={this.handleInput.bind(this, 'title')} />
<Input type="textarea"
className="account-create-description"
label="Description:"
placeholder="Description"
name="description"
disabled={disabled}
value={read(this.props.data, 'form.description', '')}
errors={read(this.props.data, 'errors.description', [])}
onChange={this.handleInput.bind(this, 'description')} />
</form>
</BS.Modal.Body>
<BS.Modal.Footer>
<BS.Button onClick={this.onHide.bind(this)}>Cancel</BS.Button>
<ButtonLoader loading={read(this.props.data, 'loading', false)}
type="submit"
bsStyle="primary"
icon={<BS.Glyphicon glyph="plus" />}
disabled={disabled}
onClick={this.handleSubmit.bind(this)}>
Add
</ButtonLoader>
</BS.Modal.Footer>
</BS.Modal>
);
}
}
const mapStateToProps = ({ app }) => ({
ui: app.ui.bookmarkAccount,
data: app.data.bookmarkAccount
});
export default connect(mapStateToProps)(Add3rdPartyAccountModal);

View File

@@ -0,0 +1,152 @@
/**
* Created by andrew on 20/02/16.
*/
import React, { PropTypes } from "react";
import { connect } from "react-redux";
import * as BS from "react-bootstrap";
import Input from "../../controls/bootstrap/Input";
import ButtonLoader from "../../controls/bootstrap/ButtonLoader";
//import { PageHeader, OverlayTrigger, Modal, Tooltip, Grid, Col, Row, Nav, NavItem, ButtonGroup, Button, Table } from "react-bootstrap";
import { Link, IndexLink} from "react-router";
import read from '../../utils/readProp';
import { accountCreateFormUpdate, accountCreateError } from '../../actions/entities';
const formValidation = (payload) => ['title', 'balance', 'description'].reduce((memo, prop) => {
let result = [];
const value = (payload[prop] || '').replace(/(^\s+)|(\s+$)/g, '');
switch (prop) {
case 'title':
case 'balance':
if (/^$/.test(value)) {
result.push('required');
}
}
switch (prop) {
case 'balance':
if (!/\d+/.test(value)) {
result.push('need to be a number');
}
}
switch (prop) {
case 'description':
if (value.length > 400) {
result.push('need to less than 400 symbols long');
}
}
if (result.length) {
memo[prop] = result;
memo.hasErrors = true;
}
return memo;
}, {});
export class NewAccountModal extends React.Component {
static propTypes = {
action: PropTypes.func,
account: PropTypes.object.isRequired
};
handleSubmit(event) {
event.preventDefault();
const payload = { ...this.props.account.form };
const validationErrors = formValidation(payload);
if (validationErrors.hasErrors) {
this.props.dispatch(accountCreateError(validationErrors));
return;
}
const { action } = this.props;
if (action) {
action(payload);
}
}
handleInput(key, val) {
this.props.dispatch(accountCreateFormUpdate(key, val));
}
render() {
const disabled = (
this.props.account.loading
);
const actionLabel = 'Create';
return (<BS.Modal show={this.props.show} onHide={this.props.onHide} key={0}>
<BS.Modal.Header closeButton>
<BS.Modal.Title>New Account</BS.Modal.Title>
</BS.Modal.Header>
<BS.Modal.Body>
<form className='account-create-form clearfix'
onSubmit={this.handleSubmit.bind(this)}>
<Input type="text"
className="account-create-title"
label="Title"
placeholder="Title"
name="title"
disabled={disabled}
value={read(this.props.account, 'form.title', '')}
errors={read(this.props.account, 'errors.title', [])}
onChange={this.handleInput.bind(this, "title")}
/>
<Input type="text"
className="account-create-balance"
label="Balance"
placeholder="Balance"
name="balance"
addonBefore={
(<BS.Glyphicon glyph="usd" />)
}
addonAfter=".00"
disabled={disabled}
value={read(this.props.account, 'form.balance', '')}
errors={read(this.props.account, 'errors.balance', [])}
onChange={this.handleInput.bind(this, 'balance')}
/>
<Input type="textarea"
className="account-create-description"
label="Description"
placeholder="Description"
name="description"
disabled={disabled}
value={read(this.props.account, 'form.description', '') || ''}
errors={read(this.props.account, 'errors.description', [])}
onChange={this.handleInput.bind(this, 'description')}
/>
</form>
</BS.Modal.Body>
<BS.Modal.Footer>
<BS.Button onClick={this.props.onHide}>Cancel</BS.Button>
<ButtonLoader loading={read(this.props.account, 'loading', false)}
type="submit"
bsStyle="primary"
icon={<BS.Glyphicon glyph="plus" />}
disabled={disabled}
onClick={this.handleSubmit.bind(this)}
>
{actionLabel}
</ButtonLoader>
</BS.Modal.Footer>
</BS.Modal>);
}
}
export default connect()(NewAccountModal);

View File

@@ -0,0 +1,68 @@
/**
* Created by andrew on 20/02/16.
*/
import React, { PropTypes } from "react";
import * as BS from 'react-bootstrap';
import { PageHeader, OverlayTrigger, Modal, Tooltip, Grid, Col, Row, Nav, NavItem, ButtonGroup, Button, Table } from "react-bootstrap";
import { Link, IndexLink} from "react-router";
import { connect } from "react-redux";
import Select from "react-select";
export class RemoveAccountBookmarkModal extends React.Component {
static propTypes = {
action: PropTypes.func,
account: PropTypes.object
};
handleAction(evt) {
evt.preventDefault();
const { action } = this.props;
const { account } = this.props;
const {
id,
accountId
} = account || {};
if (action) {
action(id || accountId);
}
}
render() {
const { account } = this.props;
const { title: titleRaw,
description: descriptionRaw,
balance: balanceRaw,
id,
accountId } = account || {};
const entityId = id || accountId;
const title = titleRaw || '[No title]';
const balance = ((balanceRaw > 0 && balanceRaw < 1) ? '$0' : '$') + Number(balanceRaw).toFixed(2);
const description = descriptionRaw || '[No description]';
return (<Modal show={this.props.show} onHide={this.props.onHide} key={0}>
<Modal.Header closeButton>
<Modal.Title>Remove Account Bookmark</Modal.Title>
</Modal.Header>
<Modal.Body>
<form className="form-horizontal">
<BS.FormControls.Static label="Title" labelClassName="col-xs-2" wrapperClassName="col-xs-10">{ title }</BS.FormControls.Static>
<BS.FormControls.Static label="Balance" labelClassName="col-xs-2" wrapperClassName="col-xs-10">{ balance }</BS.FormControls.Static>
<BS.FormControls.Static label="Description" labelClassName="col-xs-2" wrapperClassName="col-xs-10">{ description }</BS.FormControls.Static>
</form>
</Modal.Body>
<Modal.Footer>
<Button onClick={this.props.onHide}>Cancel</Button>
<Button bsStyle="danger" eventKey={ entityId } onClick={this.handleAction.bind(this)}>Remove</Button>
</Modal.Footer>
</Modal>);
}
}
export default RemoveAccountBookmarkModal;

View File

@@ -0,0 +1,6 @@
/**
* Created by andrew on 20/02/16.
*/
export { default as Add3rdPartyAccountModal } from './Add3rdPartyAccountModal';
export { default as NewAccountModal } from './NewAccountModal';
export { default as RemoveAccountBookmarkModal } from './RemoveAccountModal';

View File

@@ -9,7 +9,7 @@ export default (DEBUG, PATH, PORT=3000) => ({
] : []).concat([
'./src/main.less',
'babel-polyfill',
'./src/client',
'./src/client'
]),
output: {
@@ -22,20 +22,22 @@ export default (DEBUG, PATH, PORT=3000) => ({
debug: DEBUG,
// For options, see http://webpack.github.io/docs/configuration.html#devtool
devtool: DEBUG && "eval",
//devtool: DEBUG && "eval",
devtool: DEBUG && "cheap-module-eval-source-map",
module: {
loaders: [
// Load ES6/JSX
{ test: /\.jsx?$/,
include: [
path.resolve(__dirname, "src"),
path.resolve(__dirname, "node_modules/redux-auth/src/views/bootstrap")
path.resolve(__dirname, "src")
//,
//path.resolve(__dirname, "node_modules/redux-auth/src/views/bootstrap")
],
loader: "babel-loader",
query: {
plugins: ['transform-runtime'],
presets: ['es2015', 'stage-0', 'react'],
presets: ['es2015', 'react', 'stage-0']
}
},
@@ -65,7 +67,9 @@ export default (DEBUG, PATH, PORT=3000) => ({
},
plugins: DEBUG
? []
? [
//new
]
: [
new webpack.DefinePlugin({'process.env.NODE_ENV': '"production"'}),
new ExtractTextPlugin("style.css", {allChunks: false}),
@@ -75,11 +79,11 @@ export default (DEBUG, PATH, PORT=3000) => ({
mangle: {screw_ie8: true, keep_fnames: true}
}),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.optimize.AggressiveMergingPlugin(),
new webpack.optimize.AggressiveMergingPlugin()
],
resolveLoader: {
root: path.join(__dirname, "node_modules"),
root: path.join(__dirname, "node_modules")
},
resolve: {
@@ -94,6 +98,6 @@ export default (DEBUG, PATH, PORT=3000) => ({
},
// Allow to omit extensions when requiring these files
extensions: ["", ".js", ".jsx"],
extensions: ["", ".js", ".jsx"]
}
});

View File

@@ -18,7 +18,7 @@
<div id="react-app"></div>
<!-- inject:app:js -->
<script src="/main-9aec0a6fd78376cd52bb.js"></script>
<script src="/main-6840e3c9a53c4afc34e9.js"></script>
<!-- endinject -->
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
@import url(http://fonts.googleapis.com/css?family=Roboto:300,400,500);*,:after,:before{box-sizing:border-box}#react-app,body,html,main{position:relative;height:100%;min-height:100%}*{margin:0}body,html,main{font-family:Roboto}body{-webkit-tap-highlight-color:transparent}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}
@import url(http://fonts.googleapis.com/css?family=Roboto:300,400,500);*,:after,:before{box-sizing:border-box}#react-app,body,html,main{position:relative;height:100%;min-height:100%}*{margin:0}body,html,main{font-family:Roboto}body{-webkit-tap-highlight-color:transparent;padding-bottom:50px;height:auto}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}.footer-navigation{height:1px}.footer-navigation>.container{height:100%}.footer-navigation>.container>*{top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.page-header{padding-bottom:9px;margin:0 0 20px;border-bottom:1px solid #eee}h1{margin-top:.5em}