Use redux with management interface (fixes #217)

This commit is contained in:
Stuart Colville 2015-07-22 14:39:53 +01:00
Родитель a3f42c000f
Коммит 790b185253
18 изменённых файлов: 312 добавлений и 156 удалений

Просмотреть файл

@ -21,7 +21,7 @@
"require": false,
},
"rules": {
"block-scoped-var": 2,
"block-scoped-var": 0,
"camelcase": 0,
"comma-dangle": [2, "always-multiline"],
"comma-style": [2, "last"],

Просмотреть файл

@ -1,12 +1,10 @@
'use strict';
var actionTypes = require('constants/action-types');
import * as actionTypes from 'constants/action-types';
module.exports = {
error: function(debugMessage) {
return {
type: actionTypes.APP_ERROR,
error: {debugMessage: debugMessage},
};
},
};
export function error(debugMessage) {
return {
type: actionTypes.APP_ERROR,
error: {debugMessage: debugMessage},
};
}

Просмотреть файл

@ -0,0 +1,39 @@
'use strict';
import $ from 'jquery';
import * as actionTypes from 'constants/action-types';
export function error(debugMessage) {
return {
type: actionTypes.APP_ERROR,
error: {debugMessage: debugMessage},
};
}
export function manageCards() {
return function(dispatch) {
$.ajax({
method: 'get',
url: '/api/braintree/mozilla/paymethod/',
context: this,
}).then(function(data) {
dispatch({
type: actionTypes.MANAGE_CARD_LIST,
management: {
paymentMethods: data,
},
});
}).fail(function() {
console.log('Retrieving cards failed');
dispatch(error('Retrieving cards failed'));
});
};
}
export function closeModal() {
console.log('closeModal');
return {
type: actionTypes.MANAGE_CLOSE_MODAL,
};
}

Просмотреть файл

@ -1,22 +1,20 @@
'use strict';
var actionTypes = require('constants/action-types');
import * as actionTypes from 'constants/action-types';
// TODO: expand these actions to encapsulate the Ajax
// logic more directly. This will allow the Ajax requests to
// be tested more easily. CardForm and CardChoice will need
// to be refactored.
exports.complete = function() {
export function complete() {
return {
type: actionTypes.COMPLETE_PURCHASE,
};
};
}
exports.payWithNewCard = function() {
export function payWithNewCard() {
return {
type: actionTypes.PAY_WITH_NEW_CARD,
};
};
}

Просмотреть файл

@ -5,42 +5,40 @@ var $ = require('jquery');
var appActions = require('./app');
var actionTypes = require('constants/action-types');
module.exports = {
signIn: function(accessToken) {
return function(dispatch) {
$.ajax({
data: {
access_token: accessToken,
export function signIn(accessToken) {
return function(dispatch) {
$.ajax({
data: {
access_token: accessToken,
},
method: 'post',
url: '/api/auth/sign-in/',
context: this,
}).then(function(data) {
console.log('setting CSRF token for subsequent requests:',
data.csrf_token);
$.ajaxSetup({
headers: {
'X-CSRFToken': data.csrf_token,
},
method: 'post',
url: '/api/auth/sign-in/',
context: this,
}).then(function(data) {
console.log('setting CSRF token for subsequent requests:',
data.csrf_token);
$.ajaxSetup({
headers: {
'X-CSRFToken': data.csrf_token,
},
});
console.log('login succeeded, setting user');
dispatch({
type: actionTypes.USER_SIGNED_IN,
user: {
email: data.buyer_email,
payment_methods: data.payment_methods,
},
});
}).fail(function() {
console.log('login failed');
dispatch(appActions.error('user login failed'));
});
};
},
};
console.log('login succeeded, setting user');
dispatch({
type: actionTypes.USER_SIGNED_IN,
user: {
email: data.buyer_email,
payment_methods: data.payment_methods,
},
});
}).fail(function() {
console.log('login failed');
dispatch(appActions.error('user login failed'));
});
};
}

Просмотреть файл

@ -1,89 +1,67 @@
'use strict';
var React = require('react');
var {
Accordion,
AccordionContent,
AccordionSection} = require('components/accordion');
var Provider = require('redux/react').Provider;
var Connector = require('redux/react').Connector;
var bindActionCreators = require('redux').bindActionCreators;
var Modal = require('components/modal');
var reduxConfig = require('redux-config');
var managementActions = require('actions/management');
var gettext = require('utils').gettext;
var ModalError = require('views/modal-error');
var Management = require('views/management');
var ManageCards = require('views/manage-cards');
var Management = React.createClass({
var App = React.createClass({
displayName: 'ManagementApp',
render: function() {
selectData: function(state) {
return {
management: state.management,
};
},
renderChild(result) {
var actions = bindActionCreators(managementActions, result.dispatch);
var children = [];
if (result.management.error) {
children.push(
<ModalError {...actions} error={result.management.error} />
);
} else if (result.management.paymentMethods) {
children.push((
<ManageCards {...actions}
paymentMethods={result.management.paymentMethods} />
));
}
children.push(<Management {...actions} />);
return <div>{children}</div>;
},
render () {
return (
<div>
<header className="top-nav">
<h1 className="logo">Firefox Payments</h1>
<button>{gettext('Sign Out')}</button>
</header>
<div className="content">
<div className="user">
<p>Hello, placeholder@placeholder.com</p>
</div>
<Accordion>
<AccordionSection>
<header>
<h2>{gettext('Payment Accounts')}</h2>
<button data-activate>{gettext('Change')}</button>
</header>
<AccordionContent>
<p>Payment list will go here</p>
<ul>
<li>4111 1111 1111 1111</li>
<li>4222 2222 2222 2222</li>
</ul>
</AccordionContent>
</AccordionSection>
<AccordionSection>
<header>
<h2>{gettext('Receipts and Subscriptions')}</h2>
<button data-activate>{gettext('View/Change')}</button>
</header>
<AccordionContent>
<p>Placeholder</p>
</AccordionContent>
</AccordionSection>
<AccordionSection>
<header>
<h2>{gettext('Email Address and Password')}</h2>
<a className="button"
href="https://mozilla.org/"
target="_blank">{gettext('Change')}</a>
<p>placeholder@placeholder.com</p>
</header>
</AccordionSection>
<AccordionSection>
<header>
<h2>{gettext('Delete Account')}</h2>
<button data-activate>{gettext('Delete')}</button>
</header>
<AccordionContent>
<p>Placeholder content</p>
</AccordionContent>
</AccordionSection>
</Accordion>
</div>
</div>
<main>
<Connector select={this.selectData}>
{this.renderChild}
</Connector>
</main>
);
},
});
module.exports = {
component: Management,
component: App,
init: function() {
React.render(<Modal />, document.getElementById('modal'));
React.render(<Management />, document.getElementById('view'));
React.render((
<Provider redux={reduxConfig.default}>
{function() {
return <App/>;
}}
</Provider>
), document.body);
},
};

Просмотреть файл

@ -11,7 +11,7 @@ var Accordion = React.createClass({
displayName: 'Accordion',
propTypes: {
children: React.PropTypes.array,
children: React.PropTypes.node.isRequired,
},
getInitialState: function() {
@ -57,8 +57,8 @@ var AccordionSection = React.createClass({
displayName: 'AccordionSection',
propTypes: {
activate: React.PropTypes.function,
children: React.PropTypes.array,
activate: React.PropTypes.func,
children: React.PropTypes.node.isRequired,
isActive: React.PropTypes.bool,
},
@ -82,7 +82,7 @@ var AccordionContent = React.createClass({
displayName: 'AccordionContent',
propTypes: {
children: React.PropTypes.array,
children: React.PropTypes.node.isRequired,
},
render: function() {

Просмотреть файл

@ -18,13 +18,6 @@ module.exports = React.createClass({
onCardChange: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
card: (this.props.cards.length === 1 ?
this.props.cards[0].resource_uri : null),
};
},
render: function() {
var cardData = this.props.cards;
var cardList = [];

Просмотреть файл

@ -1,8 +1,9 @@
'use strict';
module.exports = {
APP_ERROR: 'APP_ERROR',
USER_SIGNED_IN: 'USER_SIGNED_IN',
COMPLETE_PURCHASE: 'COMPLETE_PURCHASE',
PAY_WITH_NEW_CARD: 'PAY_WITH_NEW_CARD',
};
export const APP_ERROR = 'APP_ERROR';
export const APP_CLEAR_ERROR = 'APP_CLEAR_ERROR';
export const USER_SIGNED_IN = 'USER_SIGNED_IN';
export const COMPLETE_PURCHASE = 'COMPLETE_PURCHASE';
export const PAY_WITH_NEW_CARD = 'PAY_WITH_NEW_CARD';
export const MANAGE_CARD_LIST = 'MANAGE_CARD_LIST';
export const MANAGE_CLOSE_MODAL = 'MANAGE_CLOSE_MODAL';

Просмотреть файл

@ -11,11 +11,5 @@ export default function app(state, action) {
},
};
}
if (action.type === actionTypes.CLEAR_ERROR) {
return {};
}
return state || {};
}

Просмотреть файл

@ -5,21 +5,42 @@ var actionTypes = require('constants/action-types');
export default function management(state, action) {
console.log('management store: got action', action);
if (action.type === actionTypes.APP_ERROR) {
return {
loading: false,
error: {
debugMessage: action.error.debugMessage,
},
};
}
if (action.type === actionTypes.MANAGE_MODAL_LOADING) {
return {
loading: true,
error: null,
paymentMethods: null,
};
}
if (action.type === actionTypes.MANAGE_CARD_LIST) {
return {
showModal: true,
payment_methods: action.user.payment_methods,
loading: false,
paymentMethods: action.management.paymentMethods,
};
}
if (action.type === actionTypes.MANAGE_CLOSE_MODAL) {
return {
showModal: false,
loading: false,
error: null,
paymentMethods: null,
};
}
return state || {
showModal: false,
payment_methods: [],
error: null,
loading: false,
paymentMethods: null,
};
}

Просмотреть файл

@ -0,0 +1,31 @@
'use strict';
var React = require('react');
var Modal = require('components/modal');
var CardList = require('components/card-list');
var gettext = require('utils').gettext;
module.exports = React.createClass({
displayName: 'ManageCards',
propTypes: {
closeModal: React.PropTypes.func.isRequired,
paymentMethods: React.PropTypes.array.isRequired,
},
render: function() {
return (
<Modal
handleClose={this.props.closeModal}
title={gettext('Payment Methods')}>
<CardList cards={this.props.paymentMethods} />
</Modal>
);
},
});

Просмотреть файл

@ -0,0 +1,79 @@
'use strict';
var React = require('react');
var {
Accordion,
AccordionContent,
AccordionSection,
} = require('components/accordion');
var gettext = require('utils').gettext;
module.exports = React.createClass({
displayName: 'ManagementApp',
propTypes: {
manageCards: React.PropTypes.func.isRequired,
},
render: function() {
return (
<div>
<header className="top-nav">
<h1 className="logo">Firefox Payments</h1>
<button>{gettext('Sign Out')}</button>
</header>
<div className="content">
<div className="user">
<p>Hello, placeholder@placeholder.com</p>
</div>
<Accordion>
<AccordionSection>
<header>
<h2>{gettext('Payment Accounts')}</h2>
<button
onClick={this.props.manageCards}>{gettext('Change')}
</button>
</header>
</AccordionSection>
<AccordionSection>
<header>
<h2>{gettext('Receipts and Subscriptions')}</h2>
<button data-activate>{gettext('View/Change')}</button>
</header>
<AccordionContent>
<p>Placeholder</p>
</AccordionContent>
</AccordionSection>
<AccordionSection>
<header>
<h2>{gettext('Email Address and Password')}</h2>
<a className="button"
href="https://mozilla.org/"
target="_blank">{gettext('Change')}</a>
<p>placeholder@placeholder.com</p>
</header>
</AccordionSection>
<AccordionSection>
<header>
<h2>{gettext('Delete Account')}</h2>
<button data-activate>{gettext('Delete')}</button>
</header>
<AccordionContent>
<p>Placeholder content</p>
</AccordionContent>
</AccordionSection>
</Accordion>
</div>
</div>
);
},
});

Просмотреть файл

@ -0,0 +1,26 @@
'use strict';
var React = require('react');
var Modal = require('components/modal');
var ErrorMessage = require('components/error');
module.exports = React.createClass({
displayName: 'ModalError',
propTypes: {
closeModal: React.PropTypes.func.isRequired,
error: React.PropTypes.object.isRequired,
},
render: function() {
return (
<Modal handleClose={this.props.closeModal}>
<ErrorMessage error={this.props.error} />
</Modal>
);
},
});

Просмотреть файл

@ -14,7 +14,6 @@
.content {
margin: 0 auto;
max-width: 700px;
padding: 20px;
width: 100%;
}

Просмотреть файл

@ -6,6 +6,7 @@
background: rgba(0, 0, 0, 0.5);
bottom: 0;
height: 100%;
left: 0;
opacity: 0;
position: fixed;

Просмотреть файл

@ -14,11 +14,11 @@ newWebpackConfig.module.loaders[0].loaders.unshift('react-hot');
module.exports = {
options: {
host: 'localhost',
inline: true,
hot: true,
publicPath: '/dist/',
contentBase: 'public/',
historyApiFallback: true,
hot: true,
inline: true,
publicPath: '/dist/',
},
start: {
webpack: newWebpackConfig,

Просмотреть файл

@ -3,7 +3,7 @@
var actionTypes = require('constants/action-types');
var appActions = require('actions/app');
var purchaseActions = require('actions/purchase');
var dataStore = require('data-store');
var dataStore = require('stores');
describe('app', function() {