Purchase flow without a router
This commit is contained in:
Родитель
e595a4c122
Коммит
89919b8049
|
@ -3,4 +3,6 @@
|
|||
module.exports = {
|
||||
APP_ERROR: 'APP_ERROR',
|
||||
USER_SIGNED_IN: 'USER_SIGNED_IN',
|
||||
COMPLETE_PURCHASE: 'COMPLETE_PURCHASE',
|
||||
PAY_WITH_NEW_CARD: 'PAY_WITH_NEW_CARD',
|
||||
};
|
||||
|
|
|
@ -23,9 +23,44 @@ exports.user = function(state, action) {
|
|||
if (action.type === actionTypes.USER_SIGNED_IN) {
|
||||
console.log('user store: got action', action);
|
||||
return {
|
||||
signedIn: true,
|
||||
email: action.user.email,
|
||||
payment_methods: action.user.payment_methods,
|
||||
};
|
||||
}
|
||||
return state || {};
|
||||
return state || {
|
||||
signedIn: false,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
exports.purchase = function(state, action) {
|
||||
|
||||
if (action.type === actionTypes.COMPLETE_PURCHASE) {
|
||||
console.log('purchase store: got action', action);
|
||||
return {
|
||||
completed: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === actionTypes.PAY_WITH_NEW_CARD) {
|
||||
console.log('purchase store: got action', action);
|
||||
return {
|
||||
completed: false,
|
||||
payment_methods: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === actionTypes.USER_SIGNED_IN) {
|
||||
console.log('purchase store: got action', action);
|
||||
return {
|
||||
completed: false,
|
||||
payment_methods: action.user.payment_methods,
|
||||
};
|
||||
}
|
||||
|
||||
return state || {
|
||||
completed: false,
|
||||
payment_methods: [],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
'use strict';
|
||||
|
||||
var actionTypes = require('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() {
|
||||
return {
|
||||
type: actionTypes.COMPLETE_PURCHASE,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
exports.payWithNewCard = function() {
|
||||
return {
|
||||
type: actionTypes.PAY_WITH_NEW_CARD,
|
||||
};
|
||||
};
|
|
@ -6,6 +6,7 @@ var assign = require('object-assign');
|
|||
var actionTypes = require('action-types');
|
||||
var appActions = require('app-actions');
|
||||
|
||||
|
||||
module.exports = assign({}, {
|
||||
|
||||
signIn: function(accessToken) {
|
||||
|
@ -19,6 +20,14 @@ module.exports = assign({}, {
|
|||
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,
|
||||
|
@ -28,14 +37,6 @@ module.exports = assign({}, {
|
|||
},
|
||||
});
|
||||
|
||||
console.log('setting CSRF token for subsequent requests:',
|
||||
data.csrf_token);
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'X-CSRFToken': data.csrf_token,
|
||||
},
|
||||
});
|
||||
|
||||
}).fail(function() {
|
||||
|
||||
console.log('login failed');
|
||||
|
|
|
@ -1,42 +1,55 @@
|
|||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var Router = require('react-router');
|
||||
var Route = Router.Route;
|
||||
var RouteHandler = Router.RouteHandler;
|
||||
var Provider = require('redux/react').Provider;
|
||||
var Connector = require('redux/react').Connector;
|
||||
var bindActionCreators = require('redux').bindActionCreators;
|
||||
|
||||
var reduxConfig = require('redux-config');
|
||||
var CardDetails = require('views/card-details');
|
||||
var CardListing = require('views/card-listing');
|
||||
var CompletePayment = require('views/complete-payment');
|
||||
var ErrorMessage = require('components/error');
|
||||
var Login = require('views/login');
|
||||
var Purchase = require('views/purchase');
|
||||
var userActions = require('user-actions');
|
||||
|
||||
var products = require('products');
|
||||
|
||||
|
||||
function parseQuery(url) {
|
||||
// TODO: replace with querystring library or something.
|
||||
var urlParts = url.split('?');
|
||||
var query;
|
||||
var data = {};
|
||||
|
||||
if (urlParts.length > 1) {
|
||||
query = urlParts[1].split('&');
|
||||
|
||||
query.forEach(function(nameVal) {
|
||||
var parts = nameVal.split('=');
|
||||
data[parts[0]] = decodeURIComponent(parts[1] || '');
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
var App = React.createClass({
|
||||
|
||||
displayName: 'App',
|
||||
|
||||
contextTypes: {
|
||||
router: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
var {router} = this.context;
|
||||
var productId = router.getCurrentQuery().product;
|
||||
|
||||
var qs = parseQuery(window.location.href);
|
||||
// TODO: we should validate/clean this input to raise early errors.
|
||||
return {
|
||||
productId: productId,
|
||||
accessToken: qs.access_token,
|
||||
productId: qs.product,
|
||||
};
|
||||
},
|
||||
|
||||
selectData: function(state) {
|
||||
return {
|
||||
app: state.app,
|
||||
user: state.user,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -53,9 +66,19 @@ var App = React.createClass({
|
|||
if (result.app.error) {
|
||||
console.log('rendering app error');
|
||||
return <ErrorMessage error={result.app.error} />;
|
||||
} else if (!result.user.signedIn) {
|
||||
console.log('rendering login');
|
||||
return (
|
||||
<Login
|
||||
accessToken={state.accessToken}
|
||||
{...bindActionCreators(userActions, result.dispatch) }
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
console.log('rendering app route handler');
|
||||
return <RouteHandler productId={state.productId} />;
|
||||
console.log('rendering purchase flow');
|
||||
return (
|
||||
<Purchase user={result.user} productId={state.productId} />
|
||||
);
|
||||
}
|
||||
}}
|
||||
</Connector>
|
||||
|
@ -64,29 +87,16 @@ var App = React.createClass({
|
|||
},
|
||||
});
|
||||
|
||||
// declare our routes and their hierarchy
|
||||
var routes = (
|
||||
<Route handler={App}>
|
||||
<Route name="login" path="/" handler={Login}/>
|
||||
<Route name="card-form" path="/payment/card/" handler={CardDetails}/>
|
||||
<Route
|
||||
name="complete" path="/payment/complete/" handler={CompletePayment}/>
|
||||
<Route
|
||||
name="card-listing" path="/payment/card-list/" handler={CardListing}/>
|
||||
</Route>
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
component: App,
|
||||
init: function() {
|
||||
Router.run(routes, Router.HashLocation, function(Root) {
|
||||
React.render((
|
||||
<Provider redux={reduxConfig.default}>
|
||||
{function() {
|
||||
return <Root/>;
|
||||
}}
|
||||
</Provider>
|
||||
), document.body);
|
||||
});
|
||||
React.render((
|
||||
<Provider redux={reduxConfig.default}>
|
||||
{function() {
|
||||
return <App/>;
|
||||
}}
|
||||
</Provider>
|
||||
), document.body);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
|
||||
var $ = require('jquery');
|
||||
var React = require('react');
|
||||
var Navigation = require('react-router').Navigation;
|
||||
|
||||
var CardItem = require('components/card-item');
|
||||
var SubmitButton = require('components/submit-button');
|
||||
|
||||
var gettext = require('utils').gettext;
|
||||
var purchaseActions = require('purchase-actions');
|
||||
var reduxConfig = require('redux-config');
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -17,8 +18,6 @@ module.exports = React.createClass({
|
|||
cards: React.PropTypes.array.isRequired,
|
||||
},
|
||||
|
||||
mixins: [Navigation],
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
isSubmitting: false,
|
||||
|
@ -27,12 +26,7 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
router: React.PropTypes.func,
|
||||
},
|
||||
|
||||
handleSubmit: function(e) {
|
||||
var { router } = this.context;
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({isSubmitting: true});
|
||||
|
@ -48,7 +42,11 @@ module.exports = React.createClass({
|
|||
context: this,
|
||||
}).done(function() {
|
||||
console.log('Successfully subscribed with existing card');
|
||||
router.transitionTo('complete');
|
||||
|
||||
reduxConfig.default.dispatch(
|
||||
purchaseActions.complete()
|
||||
);
|
||||
|
||||
}).fail(function() {
|
||||
// TODO: handler errors.
|
||||
});
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
var $ = require('jquery');
|
||||
var CardValidator = require('card-validator');
|
||||
var Navigation = require('react-router').Navigation;
|
||||
var React = require('react');
|
||||
var braintree = require('braintree-web');
|
||||
|
||||
|
@ -11,6 +10,8 @@ var gettext = utils.gettext;
|
|||
|
||||
var CardInput = require('components/card-input');
|
||||
var SubmitButton = require('components/submit-button');
|
||||
var purchaseActions = require('purchase-actions');
|
||||
var reduxConfig = require('redux-config');
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -26,8 +27,6 @@ module.exports = React.createClass({
|
|||
'productId': React.PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
mixins: [Navigation],
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
isSubmitting: false,
|
||||
|
@ -78,10 +77,6 @@ module.exports = React.createClass({
|
|||
},
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
router: React.PropTypes.func,
|
||||
},
|
||||
|
||||
handleChange: function(e) {
|
||||
var fieldId = e.target.id;
|
||||
var val = e.target.value;
|
||||
|
@ -107,7 +102,6 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
handleSubmit: function(e) {
|
||||
var { router } = this.context;
|
||||
e.preventDefault();
|
||||
this.setState({isSubmitting: true});
|
||||
var that = this;
|
||||
|
@ -134,7 +128,11 @@ module.exports = React.createClass({
|
|||
context: that,
|
||||
}).done(function() {
|
||||
console.log('Successfully subscribed + completed payment');
|
||||
router.transitionTo('complete');
|
||||
|
||||
reduxConfig.default.dispatch(
|
||||
purchaseActions.complete()
|
||||
);
|
||||
|
||||
}).fail(function($xhr) {
|
||||
this.processApiErrors($xhr.responseJSON);
|
||||
});
|
||||
|
|
|
@ -14,6 +14,10 @@ module.exports = React.createClass({
|
|||
|
||||
displayName: 'CardDetailsView',
|
||||
|
||||
propTypes: {
|
||||
productId: React.PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
braintree_token: false,
|
||||
|
@ -22,6 +26,7 @@ module.exports = React.createClass({
|
|||
|
||||
componentDidMount: function() {
|
||||
console.log('Requesting braintree token');
|
||||
// TODO: move this to a purchase action.
|
||||
$.ajax({
|
||||
method: 'post',
|
||||
url: '/api/braintree/token/generate/',
|
||||
|
|
|
@ -1,61 +1,37 @@
|
|||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var Link = require('react-router').Link;
|
||||
var Navigation = require('react-router').Navigation;
|
||||
var Connector = require('redux/react').Connector;
|
||||
|
||||
var CardChoice = require('components/card-choice');
|
||||
var ProductDetail = require('components/product-detail');
|
||||
var Spinner = require('components/spinner');
|
||||
var gettext = require('utils').gettext;
|
||||
var purchaseActions = require('purchase-actions');
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
||||
displayName: 'CardListingView',
|
||||
displayName: 'CardListing',
|
||||
|
||||
mixins: [Navigation],
|
||||
|
||||
contextTypes: {
|
||||
router: React.PropTypes.func,
|
||||
},
|
||||
|
||||
selectData: function(state) {
|
||||
console.log('card-listing: selectData() firing with', state);
|
||||
return {
|
||||
user: state.user,
|
||||
}
|
||||
propTypes: {
|
||||
payWithNewCard: React.PropTypes.func.isRequired,
|
||||
paymentMethods: React.PropTypes.array.isRequired,
|
||||
productId: React.PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var component = this;
|
||||
return (
|
||||
<Connector select={this.selectData}>
|
||||
{function(result) {
|
||||
if (!result.user.email) {
|
||||
return <Spinner text={gettext('Loading')}/>;
|
||||
} else if (result.user.payment_methods.length) {
|
||||
console.log('user:', result.user);
|
||||
return (
|
||||
<div className="card-listing">
|
||||
<ProductDetail productId={component.props.productId} />
|
||||
<CardChoice
|
||||
cards={result.user.payment_methods}
|
||||
productId={component.props.productId}
|
||||
/>
|
||||
<Link
|
||||
className="card-add bottom-link"
|
||||
to="card-form">{gettext('Add new credit card')}</Link>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
console.log('No card data found, showing card entry');
|
||||
component.transitionTo('card-form');
|
||||
return <Spinner text={gettext('Loading')}/>;
|
||||
}
|
||||
}}
|
||||
</Connector>
|
||||
<div className="card-listing">
|
||||
<ProductDetail productId={this.props.productId} />
|
||||
<CardChoice
|
||||
cards={this.props.paymentMethods}
|
||||
productId={this.props.productId}
|
||||
/>
|
||||
<a className="card-add bottom-link"
|
||||
onClick={this.props.payWithNewCard}>
|
||||
{gettext('Add new credit card')}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
productId: React.PropTypes.string.isRequired,
|
||||
userEmail: React.PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
handleClick: function(e) {
|
||||
|
@ -34,32 +35,19 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
selectData: function(state) {
|
||||
console.log('complete-payment: selectData() firing with', state);
|
||||
return {
|
||||
user: state.user,
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var component = this;
|
||||
return (
|
||||
<Connector select={this.selectData}>
|
||||
{function(result) {
|
||||
return (
|
||||
<div className="complete">
|
||||
<ProductDetail productId={component.props.productId} />
|
||||
<p className="accepted">{gettext('Payment Accepted')}</p>
|
||||
<p className="receipt">
|
||||
{gettext('Your receipt has been sent to')}
|
||||
<span className="email">{result.user.email}</span>
|
||||
</p>
|
||||
<SubmitButton text={gettext('OK')}
|
||||
onClick={component.handleClick} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Connector>
|
||||
<div className="complete">
|
||||
<ProductDetail productId={component.props.productId} />
|
||||
<p className="accepted">{gettext('Payment Accepted')}</p>
|
||||
<p className="receipt">
|
||||
{gettext('Your receipt has been sent to')}
|
||||
<span className="email">{this.props.userEmail}</span>
|
||||
</p>
|
||||
<SubmitButton text={gettext('OK')}
|
||||
onClick={component.handleClick} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -2,57 +2,28 @@
|
|||
|
||||
var $ = require('jquery');
|
||||
var React = require('react');
|
||||
var Navigation = require('react-router').Navigation;
|
||||
var Connector = require('redux/react').Connector;
|
||||
|
||||
var userActions = require('user-actions');
|
||||
var Spinner = require('components/spinner');
|
||||
var gettext = require('utils').gettext;
|
||||
var reduxConfig = require('redux-config');
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
||||
displayName: 'LoginView',
|
||||
mixins: [Navigation],
|
||||
displayName: 'Login',
|
||||
|
||||
contextTypes: {
|
||||
router: React.PropTypes.func,
|
||||
propTypes: {
|
||||
accessToken: React.PropTypes.string.isRequired,
|
||||
signIn: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
var { router } = this.context;
|
||||
var defer = userActions.signIn(router.getCurrentQuery().access_token);
|
||||
defer(reduxConfig.default.dispatch);
|
||||
},
|
||||
|
||||
selectData: function(state) {
|
||||
console.log('login: selectData() firing with', state);
|
||||
return {
|
||||
user: state.user,
|
||||
}
|
||||
},
|
||||
|
||||
watchUser: function(user) {
|
||||
if (!user) {
|
||||
console.log('user is not signed in');
|
||||
return;
|
||||
}
|
||||
console.log('current user is signed in; continuing to card-listing');
|
||||
this.transitionTo('card-listing');
|
||||
this.props.signIn(this.props.accessToken);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var component = this;
|
||||
return (
|
||||
<Connector select={component.selectData}>
|
||||
{function(result) {
|
||||
console.log('login: rendering after state change', result);
|
||||
component.watchUser(result.user);
|
||||
return <Spinner text={gettext('Logging in')}/>;
|
||||
}}
|
||||
</Connector>
|
||||
);
|
||||
return <Spinner text={gettext('Signing in')}/>;
|
||||
},
|
||||
|
||||
});
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var bindActionCreators = require('redux').bindActionCreators;
|
||||
var Connector = require('redux/react').Connector;
|
||||
|
||||
var reduxConfig = require('redux-config');
|
||||
var CardDetails = require('views/card-details');
|
||||
var CardListing = require('views/card-listing');
|
||||
var CompletePayment = require('views/complete-payment');
|
||||
var purchaseActions = require('purchase-actions.js');
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
||||
displayName: 'Purchase',
|
||||
|
||||
propTypes: {
|
||||
user: React.PropTypes.object.isRequired,
|
||||
productId: React.PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
selectData: function(state) {
|
||||
return {
|
||||
purchase: state.purchase,
|
||||
};
|
||||
},
|
||||
|
||||
render () {
|
||||
var props = this.props;
|
||||
return (
|
||||
<Connector select={this.selectData}>
|
||||
{function(result) {
|
||||
if (result.purchase.completed) {
|
||||
return (
|
||||
<CompletePayment productId={props.productId}
|
||||
userEmail={props.user.email} />
|
||||
);
|
||||
} else if (result.purchase.payment_methods.length > 0) {
|
||||
console.log('rendering card listing');
|
||||
return (
|
||||
<CardListing
|
||||
productId={props.productId}
|
||||
paymentMethods={result.purchase.payment_methods}
|
||||
{...bindActionCreators(purchaseActions, result.dispatch)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
console.log('rendering card entry');
|
||||
return <CardDetails productId={props.productId} />;
|
||||
}
|
||||
}}
|
||||
</Connector>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -1,8 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
var assign = require('object-assign');
|
||||
var React = require('react');
|
||||
|
||||
var TestUtils = require('react/lib/ReactTestUtils');
|
||||
|
||||
module.exports = {
|
||||
|
@ -44,50 +42,16 @@ module.exports = {
|
|||
return TestUtils.findRenderedDOMComponentWithTag(component, tag);
|
||||
},
|
||||
|
||||
getRouterStub: function(routerStubs) {
|
||||
|
||||
function RouterStub() {}
|
||||
|
||||
assign(RouterStub, {
|
||||
makePath () {},
|
||||
makeHref () {},
|
||||
transitionTo (path) {
|
||||
console.log('RouterStub: transitionTo:', path);
|
||||
},
|
||||
replaceWith () {},
|
||||
goBack () {},
|
||||
getCurrentPath () {},
|
||||
getCurrentRoutes () {},
|
||||
getCurrentPathname () {},
|
||||
getCurrentParams () {
|
||||
return {};
|
||||
},
|
||||
getCurrentQuery () {
|
||||
return {};
|
||||
},
|
||||
isActive () {},
|
||||
getRouteAtDepth() {},
|
||||
setRouteComponentAtDepth() {},
|
||||
|
||||
}, routerStubs || {});
|
||||
|
||||
return RouterStub;
|
||||
},
|
||||
|
||||
getFluxContainer: function(redux, routerStubs) {
|
||||
getFluxContainer: function(redux) {
|
||||
//
|
||||
// Get a container component to set context stubs so you can use it
|
||||
// to wrap a component for testing.
|
||||
// You'd only need this to test a component that uses the router
|
||||
// and/or uses the redux Connector component.
|
||||
// You'd only need this to test a component that uses the
|
||||
// redux Connector component.
|
||||
//
|
||||
// componentProps
|
||||
// Optional object of properties to render the component with.
|
||||
//
|
||||
// routerStubs
|
||||
// Option object of addtional stub methods for the stub router.
|
||||
//
|
||||
var RouterStub = this.getRouterStub(routerStubs);
|
||||
|
||||
var FluxContainer = React.createClass({
|
||||
|
||||
|
@ -96,12 +60,11 @@ module.exports = {
|
|||
},
|
||||
|
||||
childContextTypes: {
|
||||
router: React.PropTypes.func.isRequired,
|
||||
redux: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
return {router: RouterStub, redux: redux};
|
||||
return {redux: redux};
|
||||
},
|
||||
|
||||
render () {
|
||||
|
@ -110,8 +73,6 @@ module.exports = {
|
|||
|
||||
});
|
||||
|
||||
FluxContainer.router = RouterStub;
|
||||
|
||||
return FluxContainer;
|
||||
},
|
||||
|
||||
|
@ -165,4 +126,13 @@ module.exports = {
|
|||
};
|
||||
},
|
||||
|
||||
stubComponent: function() {
|
||||
return React.createClass({
|
||||
displayName: 'StubComponent',
|
||||
render: function() {
|
||||
return <div></div>;
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
};
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
var React;
|
||||
var React = require('react');
|
||||
var TestUtils;
|
||||
var rewire = require('rewire');
|
||||
|
||||
var app = require('app');
|
||||
var actionTypes = require('action-types');
|
||||
var appActions = require('app-actions');
|
||||
var reduxConfig = require('redux-config');
|
||||
var ErrorMessage = require('components/error');
|
||||
|
@ -12,20 +13,29 @@ var helpers = require('./helpers');
|
|||
|
||||
describe('App', function() {
|
||||
|
||||
var accessToken = 'some-oauth-token';
|
||||
var productId = 'mozilla-concrete-brick';
|
||||
var FakeLogin = helpers.stubComponent();
|
||||
var FakePurchase = helpers.stubComponent();
|
||||
var redux;
|
||||
|
||||
beforeEach(function() {
|
||||
React = require('react');
|
||||
TestUtils = require('react/lib/ReactTestUtils');
|
||||
redux = reduxConfig.create();
|
||||
});
|
||||
|
||||
function mountView() {
|
||||
var FluxContainer = helpers.getFluxContainer(redux, {
|
||||
getCurrentQuery: function() {
|
||||
return {
|
||||
product: 'mozilla-concrete-brick',
|
||||
};
|
||||
var FluxContainer = helpers.getFluxContainer(redux);
|
||||
|
||||
var app = rewire('app');
|
||||
app.__set__({
|
||||
'Login': FakeLogin,
|
||||
'Purchase': FakePurchase,
|
||||
'window': {
|
||||
'location': {
|
||||
'href': ('http://pay.dev/?access_token=' + accessToken +
|
||||
'&product=' + productId),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -33,28 +43,50 @@ describe('App', function() {
|
|||
var container = TestUtils.renderIntoDocument(
|
||||
<FluxContainer>
|
||||
{function() {
|
||||
return (<App />);
|
||||
return <App />;
|
||||
}}
|
||||
</FluxContainer>
|
||||
);
|
||||
var component = TestUtils.findRenderedComponentWithType(
|
||||
return TestUtils.findRenderedComponentWithType(
|
||||
container, App
|
||||
);
|
||||
|
||||
return {
|
||||
component: component,
|
||||
transitionSpy: FluxContainer.transitionSpy,
|
||||
};
|
||||
}
|
||||
|
||||
it('should render an error', function() {
|
||||
var view = mountView();
|
||||
var View = mountView();
|
||||
redux.dispatch(appActions.error('this is some error'));
|
||||
var error = TestUtils.findRenderedComponentWithType(
|
||||
view.component, ErrorMessage
|
||||
View, ErrorMessage
|
||||
);
|
||||
// Maybe expand this test later if we pass in custom properties.
|
||||
assert.ok(error);
|
||||
});
|
||||
|
||||
it('should render a sign-in page', function() {
|
||||
var View = mountView();
|
||||
var login = TestUtils.findRenderedComponentWithType(
|
||||
View, FakeLogin
|
||||
);
|
||||
assert.equal(login.props.accessToken, accessToken);
|
||||
});
|
||||
|
||||
it('should render a purchase page', function() {
|
||||
var user = {
|
||||
email: 'f@f.com',
|
||||
payment_methods: [],
|
||||
signedIn: true,
|
||||
};
|
||||
|
||||
var View = mountView();
|
||||
redux.dispatch({
|
||||
type: actionTypes.USER_SIGNED_IN,
|
||||
user: user,
|
||||
});
|
||||
|
||||
var purchase = TestUtils.findRenderedComponentWithType(
|
||||
View, FakePurchase
|
||||
);
|
||||
assert.deepEqual(purchase.props.user, user);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -3,91 +3,41 @@
|
|||
var React;
|
||||
var TestUtils;
|
||||
|
||||
var actionTypes = require('action-types');
|
||||
var reduxConfig = require('redux-config');
|
||||
var CardChoice = require('components/card-choice');
|
||||
var CardListing = require('views/card-listing');
|
||||
|
||||
var helpers = require('./helpers');
|
||||
|
||||
var TestUtils;
|
||||
var savedVisa = {provider_id: '3vr3ym', type_name: 'Visa'};
|
||||
|
||||
describe('CardListingView', function() {
|
||||
|
||||
var redux;
|
||||
var payWithNewCardSpy;
|
||||
var View;
|
||||
var savedVisa = {provider_id: '3vr3ym', type_name: 'Visa'};
|
||||
|
||||
beforeEach(function() {
|
||||
React = require('react');
|
||||
TestUtils = require('react/lib/ReactTestUtils');
|
||||
redux = reduxConfig.create();
|
||||
|
||||
payWithNewCardSpy = sinon.spy();
|
||||
|
||||
View = TestUtils.renderIntoDocument(
|
||||
<CardListing payWithNewCard={payWithNewCardSpy}
|
||||
paymentMethods={[savedVisa]}
|
||||
productId='mozilla-concrete-brick' />
|
||||
);
|
||||
});
|
||||
|
||||
function mountView() {
|
||||
var FluxContainer = helpers.getFluxContainer(redux);
|
||||
var transitionSpy = sinon.spy(FluxContainer.router, 'transitionTo');
|
||||
|
||||
var container = TestUtils.renderIntoDocument(
|
||||
<FluxContainer>
|
||||
{function() {
|
||||
return (
|
||||
<CardListing productId='mozilla-concrete-brick' />
|
||||
);
|
||||
}}
|
||||
</FluxContainer>
|
||||
);
|
||||
var component = TestUtils.findRenderedComponentWithType(
|
||||
container, CardListing
|
||||
);
|
||||
|
||||
return {
|
||||
component: component,
|
||||
transitionSpy: transitionSpy,
|
||||
};
|
||||
}
|
||||
|
||||
function setUser(user) {
|
||||
user.email = user.email || 'f@f.com';
|
||||
redux.dispatch({
|
||||
type: actionTypes.USER_SIGNED_IN,
|
||||
user: user,
|
||||
});
|
||||
}
|
||||
|
||||
it('should redirect to card entry form when no saved cards', function() {
|
||||
setUser({
|
||||
payment_methods: [],
|
||||
});
|
||||
var view = mountView();
|
||||
assert.equal(view.transitionSpy.firstCall.args[0], 'card-form');
|
||||
});
|
||||
|
||||
it('should show the card form if user has saved cards', function() {
|
||||
setUser({
|
||||
payment_methods: [savedVisa],
|
||||
});
|
||||
var view = mountView();
|
||||
it('should show card choice', function() {
|
||||
var card = TestUtils.findRenderedComponentWithType(
|
||||
view.component, CardChoice
|
||||
View, CardChoice
|
||||
);
|
||||
assert.deepEqual(card.props.cards, [savedVisa]);
|
||||
});
|
||||
|
||||
it('should show card form when clicking add link', function() {
|
||||
// Need some payment methods to ensure the listing is shown.
|
||||
setUser({
|
||||
payment_methods: [savedVisa],
|
||||
});
|
||||
// Link.js checks which button is clicked, so we need to provide that
|
||||
// in the event.
|
||||
var event = {
|
||||
button: 0,
|
||||
};
|
||||
var view = mountView();
|
||||
it('should request to pay with new card when clicking link', function() {
|
||||
var addLink = TestUtils.findRenderedDOMComponentWithTag(
|
||||
view.component, 'a');
|
||||
TestUtils.Simulate.click(addLink.getDOMNode(), event);
|
||||
assert.equal(view.transitionSpy.firstCall.args[0], 'card-form');
|
||||
View, 'a');
|
||||
TestUtils.Simulate.click(addLink.getDOMNode());
|
||||
assert.ok(payWithNewCardSpy.called);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -2,10 +2,7 @@
|
|||
|
||||
var React;
|
||||
var TestUtils;
|
||||
var Provider = require('redux/react').Provider;
|
||||
|
||||
var actionTypes = require('action-types');
|
||||
var reduxConfig = require('redux-config');
|
||||
var CompletePayment = require('views/complete-payment');
|
||||
|
||||
var helpers = require('./helpers');
|
||||
|
@ -19,21 +16,11 @@ describe('CompletePayment', function() {
|
|||
React = require('react');
|
||||
TestUtils = require('react/lib/ReactTestUtils');
|
||||
|
||||
var redux = reduxConfig.create();
|
||||
|
||||
this.CompletePayment = TestUtils.renderIntoDocument(
|
||||
<Provider redux={redux}>
|
||||
{function() {
|
||||
return <CompletePayment productId='mozilla-concrete-brick' />;
|
||||
}}
|
||||
</Provider>
|
||||
<CompletePayment productId='mozilla-concrete-brick'
|
||||
userEmail={this.email} />
|
||||
);
|
||||
|
||||
redux.dispatch({
|
||||
type: actionTypes.USER_SIGNED_IN,
|
||||
user: {email: this.email},
|
||||
});
|
||||
|
||||
sinon.stub(window.parent, 'postMessage');
|
||||
});
|
||||
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
var actionTypes = require('action-types');
|
||||
var appActions = require('app-actions');
|
||||
var purchaseActions = require('purchase-actions');
|
||||
var dataStore = require('data-store');
|
||||
|
||||
|
||||
describe('appStore', function() {
|
||||
describe('app', function() {
|
||||
|
||||
function appWithError() {
|
||||
return {
|
||||
|
@ -45,10 +46,11 @@ describe('appStore', function() {
|
|||
});
|
||||
|
||||
|
||||
describe('userStore', function() {
|
||||
describe('user', function() {
|
||||
|
||||
function userData() {
|
||||
return {
|
||||
signedIn: true,
|
||||
email: 'f@f.com',
|
||||
payment_methods: [{provider_id: '1234'}],
|
||||
};
|
||||
|
@ -64,7 +66,9 @@ describe('userStore', function() {
|
|||
|
||||
it('should initialize an empty user', function() {
|
||||
var user = dataStore.user(undefined, {});
|
||||
assert.deepEqual(user, {});
|
||||
assert.deepEqual(user, {
|
||||
signedIn: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a user on sign-in', function() {
|
||||
|
@ -89,3 +93,39 @@ describe('userStore', function() {
|
|||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('purchase', function() {
|
||||
|
||||
it('should initialize a purchase', function() {
|
||||
var purchase = dataStore.purchase(undefined, {});
|
||||
assert.deepEqual(purchase, {
|
||||
completed: false,
|
||||
payment_methods: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle completed purchases', function() {
|
||||
var purchase = dataStore.purchase(undefined, purchaseActions.complete());
|
||||
assert.deepEqual(purchase, {
|
||||
completed: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should infer saved payment methods when user signs in', function() {
|
||||
var paymentMethods = [{type: 'Visa'}];
|
||||
|
||||
var purchase = dataStore.purchase(undefined, {
|
||||
type: actionTypes.USER_SIGNED_IN,
|
||||
user: {
|
||||
payment_methods: paymentMethods,
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(purchase, {
|
||||
completed: false,
|
||||
payment_methods: paymentMethods,
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -2,68 +2,29 @@
|
|||
|
||||
var React;
|
||||
var TestUtils;
|
||||
var rewire = require('rewire');
|
||||
|
||||
var reduxConfig = require('redux-config');
|
||||
var Login = require('views/login');
|
||||
|
||||
var helpers = require('./helpers');
|
||||
|
||||
describe('Login', function() {
|
||||
|
||||
var userActions;
|
||||
var redux;
|
||||
var accessToken = 'some-oauth-access-token';
|
||||
var signInSpy = sinon.spy();
|
||||
|
||||
beforeEach(function() {
|
||||
React = require('react');
|
||||
TestUtils = require('react/lib/ReactTestUtils');
|
||||
|
||||
userActions = {
|
||||
signIn: sinon.spy(function() {
|
||||
return function() {};
|
||||
}),
|
||||
};
|
||||
|
||||
redux = reduxConfig.create();
|
||||
});
|
||||
|
||||
function mountView() {
|
||||
var FluxContainer = helpers.getFluxContainer(redux);
|
||||
var transitionSpy = sinon.spy(FluxContainer.router, 'transitionTo');
|
||||
|
||||
var Login = rewire('views/login');
|
||||
Login.__set__({
|
||||
userActions: userActions,
|
||||
});
|
||||
|
||||
var container = TestUtils.renderIntoDocument(
|
||||
<FluxContainer>
|
||||
{function() {
|
||||
return (<Login />);
|
||||
}}
|
||||
</FluxContainer>
|
||||
return TestUtils.renderIntoDocument(
|
||||
<Login accessToken={accessToken} signIn={signInSpy} />
|
||||
);
|
||||
var component = TestUtils.findRenderedComponentWithType(
|
||||
container, Login
|
||||
);
|
||||
|
||||
return {
|
||||
component: component,
|
||||
transitionSpy: transitionSpy,
|
||||
};
|
||||
}
|
||||
|
||||
it('should sign in on mount', function() {
|
||||
mountView();
|
||||
assert.ok(userActions.signIn.called);
|
||||
});
|
||||
|
||||
it('should navigate to card-listing when user signs in', function() {
|
||||
var view = mountView();
|
||||
redux.dispatch({
|
||||
type: 'user-signed-in',
|
||||
user: {email: 'f@f.com'},
|
||||
});
|
||||
assert.equal(view.transitionSpy.firstCall.args[0], 'card-listing');
|
||||
assert.equal(signInSpy.firstCall.args[0], accessToken);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
'use strict';
|
||||
|
||||
var React;
|
||||
var TestUtils;
|
||||
var assign = require('object-assign');
|
||||
var rewire = require('rewire');
|
||||
|
||||
var actionTypes = require('action-types');
|
||||
var reduxConfig = require('redux-config');
|
||||
var purchaseActions = require('purchase-actions');
|
||||
|
||||
var helpers = require('./helpers');
|
||||
|
||||
describe('Purchase', function() {
|
||||
|
||||
var defaultUser = {
|
||||
email: 'f@f.com',
|
||||
payment_methods: [],
|
||||
};
|
||||
var productId = 'mozilla-concrete-brick';
|
||||
var FakeCompletePayment = helpers.stubComponent();
|
||||
var FakeCardListing = helpers.stubComponent();
|
||||
var FakeCardDetails = helpers.stubComponent();
|
||||
var redux;
|
||||
|
||||
beforeEach(function() {
|
||||
React = require('react');
|
||||
TestUtils = require('react/lib/ReactTestUtils');
|
||||
redux = reduxConfig.create();
|
||||
});
|
||||
|
||||
function mountView(userOverrides) {
|
||||
var user = assign({}, defaultUser, userOverrides);
|
||||
var FluxContainer = helpers.getFluxContainer(redux);
|
||||
|
||||
var Purchase = rewire('views/purchase');
|
||||
Purchase.__set__({
|
||||
'CompletePayment': FakeCompletePayment,
|
||||
'CardListing': FakeCardListing,
|
||||
'CardDetails': FakeCardDetails,
|
||||
});
|
||||
|
||||
var container = TestUtils.renderIntoDocument(
|
||||
<FluxContainer>
|
||||
{function() {
|
||||
return <Purchase user={user} productId={productId} />;
|
||||
}}
|
||||
</FluxContainer>
|
||||
);
|
||||
return TestUtils.findRenderedComponentWithType(
|
||||
container, Purchase
|
||||
);
|
||||
}
|
||||
|
||||
it('should render a card listing', function() {
|
||||
var paymentMethods = [{type: 'Visa'}];
|
||||
var View = mountView();
|
||||
|
||||
redux.dispatch({
|
||||
type: actionTypes.USER_SIGNED_IN,
|
||||
user: assign({}, defaultUser, {
|
||||
payment_methods: paymentMethods,
|
||||
}),
|
||||
});
|
||||
|
||||
var child = TestUtils.findRenderedComponentWithType(
|
||||
View, FakeCardListing
|
||||
);
|
||||
assert.equal(child.props.productId, productId);
|
||||
assert.equal(child.props.paymentMethods, paymentMethods);
|
||||
});
|
||||
|
||||
it('should render a card entry form', function() {
|
||||
var View = mountView();
|
||||
var child = TestUtils.findRenderedComponentWithType(
|
||||
View, FakeCardDetails
|
||||
);
|
||||
assert.equal(child.props.productId, productId);
|
||||
});
|
||||
|
||||
it('should render a payment completed page', function() {
|
||||
var View = mountView();
|
||||
|
||||
redux.dispatch(purchaseActions.complete());
|
||||
|
||||
var child = TestUtils.findRenderedComponentWithType(
|
||||
View, FakeCompletePayment
|
||||
);
|
||||
assert.equal(child.props.userEmail, defaultUser.email);
|
||||
});
|
||||
|
||||
it('should render new card entry on explicit request', function() {
|
||||
var paymentMethods = [{type: 'Visa'}];
|
||||
var View = mountView();
|
||||
|
||||
// Dispatch a user that would normally trigger a card listing.
|
||||
redux.dispatch({
|
||||
type: actionTypes.USER_SIGNED_IN,
|
||||
user: assign({}, defaultUser, {
|
||||
payment_methods: paymentMethods,
|
||||
}),
|
||||
});
|
||||
|
||||
redux.dispatch(purchaseActions.payWithNewCard());
|
||||
|
||||
var child = TestUtils.findRenderedComponentWithType(
|
||||
View, FakeCardDetails
|
||||
);
|
||||
assert.ok(child);
|
||||
});
|
||||
|
||||
});
|
Загрузка…
Ссылка в новой задаче