Commit 5653a29a authored by lhorvath's avatar lhorvath Committed by Alexander Dietmüller

Frontend: Stripe payment processing

Stripe provides an easy to use Checkout modal, which we integrate in our
application.
parent 538641c2
module.exports = {
"extends": "airbnb-base",
"env": {
"browser": true,
"node": true
},
"rules": {
"no-underscore-dangle": 0,
}
extends: 'airbnb-base',
env: {
browser: true,
node: true,
},
rules: {
'no-underscore-dangle': 0,
},
globals: {
StripeCheckout: true,
},
settings: {
'import/resolver': {
webpack: {},
},
},
};
// AMIV API config
export const adminGroupName = 'PVK Admins';
export const amivApiUrl = 'https://amiv-api.ethz.ch';
// PVK backend
export const pvkApiUrl = 'http://pvk-api-dev.amiv.ethz.ch';
// Stripe
export const stripeKey = 'pk_test_Gp4vLvlcgFGUcjv89WiTb8m7';
// TODO: Most of the non-dev urls are not yet live, replace them with correct
// versions as soon as available
// AMIV API config
export const adminGroupName = 'PVK Admins';
export const amivApiUrl = 'https://api.amiv.ethz.ch';
// PVK backend
export const pvkApiUrl = 'http://pvk-api.amiv.ethz.ch';
// Stripe
export const stripeKey = 'pk_live_2cCPJUAK5OgRROXo6Wh1nOBl';
......@@ -6,6 +6,7 @@
<title>AMIV Bouncer</title>
</head>
<body>
<script src="https://checkout.stripe.com/checkout.js"></script>
<script src="dist/bundle.js"></script>
</body>
</html>
......@@ -28,6 +28,7 @@
"css-loader": "^0.28.7",
"eslint": "^4.10.0",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-import-resolver-webpack": "^0.8.4",
"eslint-plugin-import": "^2.8.0",
"file-loader": "^1.1.5",
"style-loader": "^0.19.0",
......
......@@ -4,6 +4,7 @@
// Includes the generic login / logout
import m from 'mithril';
import { Dialog } from 'polythene-mithril';
import session from './session';
import './layout.css';
......@@ -102,6 +103,7 @@ export default class Layout {
m('header', m(SidebarHeader)),
m('aside', m(vnode.attrs.sidebar)),
m('main', m(vnode.attrs.content)),
m(Dialog),
] : m(LoginPage);
}
}
......@@ -4,8 +4,8 @@ import { userCourses, courses } from './backend';
import SidebarCard from './components/SidebarCard';
class CourseView {
static view({ attrs: { /* _id, */ courseId, remove } }) {
class CourseViewDeletable {
static view({ attrs: { courseId, remove } }) {
// Get Lecture of Course
const course = courses.items[courseId];
// Otherwise display loading
......@@ -14,18 +14,20 @@ class CourseView {
title: course.lecture.title,
subtitle: [course.assistant, ' - ', course.room],
actionName: 'X',
action() { remove(); },
action: remove,
});
}
return m(SidebarCard, {
title: 'Loading ...',
});
/* m('li', [
m('span', course ? course.lecture.title : 'Loading...'),
// If there is no id, the element is not yet created,
// so a delete button does not make any sense
m('button', { onclick: remove, disabled: !_id }, 'X'),
]); */
}
}
class CourseView {
static view({ attrs: { courseId } }) {
// Same as deletable course view, but without action
return m(CourseViewDeletable, { courseId });
}
}
......@@ -40,7 +42,7 @@ export default class UserSidebar {
content: userCourses.selected.length ? [
userCourses.selected.map(({ _id, course: courseId }) =>
m(
CourseView,
CourseViewDeletable,
{ _id, courseId, remove() { userCourses.deselect(_id); } },
)),
] : 'No courses selected.',
......@@ -52,7 +54,7 @@ export default class UserSidebar {
title: 'Waiting List',
content: userCourses.waiting.map(({ _id, course: courseId }) =>
m(
CourseView,
CourseViewDeletable,
{ _id, courseId, remove() { userCourses.free(_id); } },
)),
}),
......@@ -63,17 +65,24 @@ export default class UserSidebar {
content: userCourses.reserved.length ? [
userCourses.reserved.map(({ _id, course: courseId }) =>
m(
CourseView,
CourseViewDeletable,
{ _id, courseId, remove() { userCourses.free(_id); } },
)),
] : 'No courses reserved.',
action() { userCourses.pay(); },
actionName: 'pay',
actionActive: userCourses.reserved.length > 0,
}),
m(SidebarCard, {
title: 'Accepted Courses',
content: 'No courses accepted.',
content: userCourses.accepted.length ? [
userCourses.accepted.map(({ course: courseId }) =>
m(
CourseView,
{ courseId },
)),
] : 'No courses accepted.',
}),
];
}
......
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
width="81.059502"
height="80.056625"
id="svg2"
xml:space="preserve"><metadata
id="metadata8"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs6"><clipPath
id="clipPath18"><path
d="m 0,849.563 1960.52,0 L 1960.52,0 0,0 0,849.563 z"
id="path20" /></clipPath></defs><g
transform="matrix(1.25,0,0,-1.25,-16.34525,92.96925)"
id="g10"><g
transform="scale(0.1,0.1)"
id="g12"><g
clip-path="url(#clipPath18)"
id="g16"><path
d="m 566.012,342.883 c -44.453,-61.184 -130.383,-74.797 -191.563,-30.344 -3.969,2.891 -7.719,5.957 -11.289,9.18 l 41.192,29.922 40.945,-56.375 51.351,117.707 37.684,-51.848 44.727,32.5 -40.387,55.598 41.469,30.132 c 19.257,-43.32 15.679,-95.437 -14.129,-136.472 m -235.504,23.465 c -19.887,43.554 -16.5,96.32 13.601,137.75 44.45,61.179 130.383,74.789 191.559,30.336 4.352,-3.161 8.391,-6.579 12.254,-10.125 l -41.762,-30.344 -40.558,55.82 -44.735,-32.5 40.563,-55.828 -0.067,-0.051 -127.726,-12.449 38.203,-52.578 -41.332,-30.031 z m 366.523,24.668 c 1.41,10.644 2.207,21.48 2.207,32.511 0,11.028 -0.797,21.86 -2.207,32.508 l -57.468,8.922 c -2.571,11.469 -6.196,22.711 -10.864,33.57 l 41.211,40.961 c -5.109,9.438 -10.828,18.676 -17.312,27.598 -6.481,8.922 -13.496,17.223 -20.899,25 l -51.679,-26.52 c -4.372,3.84 -8.93,7.532 -13.731,11.02 -4.84,3.512 -9.801,6.73 -14.84,9.719 l 9.258,57.351 c -9.676,4.641 -19.734,8.75 -30.238,12.16 -10.481,3.407 -21.039,5.993 -31.586,7.938 l -26.262,-51.918 c -11.742,1.07 -23.519,1.031 -35.199,-0.066 l -26.293,51.984 c -10.559,-1.945 -21.109,-4.531 -31.598,-7.938 -10.492,-3.41 -20.551,-7.519 -30.23,-12.148 l 9.269,-57.434 c -10.039,-5.925 -19.582,-12.859 -28.511,-20.707 l -51.746,26.559 c -7.407,-7.777 -14.422,-16.07 -20.903,-25 -6.492,-8.922 -12.211,-18.16 -17.32,-27.598 l 41.258,-41.011 c -4.715,-10.922 -8.36,-22.137 -10.887,-33.512 l -57.481,-8.93 c -1.421,-10.64 -2.218,-21.48 -2.218,-32.508 0,-11.031 0.797,-21.855 2.218,-32.511 l 57.563,-8.934 c 2.559,-11.445 6.168,-22.668 10.82,-33.496 L 240.09,307.57 c 5.109,-9.445 10.828,-18.683 17.32,-27.597 6.481,-8.926 13.488,-17.227 20.903,-25 l 51.675,26.523 c 4.41,-3.867 9,-7.59 13.84,-11.105 4.801,-3.485 9.723,-6.688 14.723,-9.657 l -9.258,-57.336 c 9.687,-4.636 19.746,-8.75 30.238,-12.156 10.489,-3.418 21.039,-5.996 31.598,-7.929 l 26.219,51.843 c 11.773,-1.093 23.57,-1.062 35.285,0.039 l 26.238,-51.894 c 10.559,1.945 21.117,4.523 31.598,7.941 10.504,3.406 20.551,7.52 30.238,12.149 l -9.246,57.285 c 10.078,5.957 19.648,12.898 28.617,20.789 l 51.621,-26.492 c 7.403,7.773 14.41,16.074 20.899,25 6.484,8.914 12.203,18.152 17.312,27.597 l -41.148,40.907 c 4.73,10.957 8.379,22.207 10.929,33.644 l 57.34,8.895"
id="path30"
style="fill:#f03d30;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g></svg>
\ No newline at end of file
// Api calls
import m from 'mithril';
import { pvkApiUrl } from 'config';
import session from './session';
// Development URL
const pvkApiUrl = 'http://pvk-api-dev.amiv.ethz.ch';
import handler from './stripe';
// Helper to filter temp out of list
function withoutTemp(list, temp) { return list.filter(item => item !== temp); }
// Request to the PVK backend
function request({
export function request({
resource,
method = 'GET',
id = '',
......@@ -223,6 +221,11 @@ export const userCourses = {
this.resources.signups.get();
},
getAll() {
this.resources.selections.getAll();
this.resources.signups.getAll();
},
get all() {
return [].concat(
this.resources.selections.list,
......@@ -290,8 +293,15 @@ export const userCourses = {
// TODO: Provide error feedback to user
},
// TODO: Implement
pay() {},
pay() {
// Trigger Stripe Checkout
handler.open({
name: 'AMIV PVK',
zipCode: false,
amount: 1000 * userCourses.reserved.length,
currency: 'CHF',
});
},
};
export const courses = new Resource('courses', { embedded: { lecture: 1 } });
......
......@@ -2,9 +2,7 @@
import m from 'mithril';
import ls from 'local-storage';
const adminGroupName = 'PVK Admins';
const amivApiUrl = 'https://amiv-api.ethz.ch';
import { amivApiUrl, adminGroupName } from 'config';
const storedSession = ls('session');
......
// Stripe Frontend
// stripe-checkout is loaded in index.html. Other methods of loading it are
// not officially supported. [Docs](https://stripe.com/docs/checkout)
import { Dialog } from 'polythene-mithril';
import { stripeKey } from 'config';
import { userCourses, request } from './backend';
import logo from './amiv_logo_no_text.svg';
const handler = StripeCheckout.configure({
key: stripeKey,
image: logo,
locale: 'auto',
allowRememberMe: false,
token(token) {
return request({
resource: 'payments',
method: 'POST',
data: {
signups: userCourses.reserved.map(signup => signup._id),
token: token.id,
},
}).then(() => {
// Update the sidebar
userCourses.getAll();
// Show a confirmation dialog
Dialog.show({
title: 'Thanks for your payment!',
body: 'You will receive a confirmation e-mail within the next ' +
'few minutes.',
});
}).catch((err) => {
// Show an error dialog
Dialog.show({
title: err.code === 500 ? 'Server error' : 'Payment failed',
body: err._error.message,
});
});
},
});
// Close Checkout on page navigation:
window.addEventListener('popstate', () => {
handler.close();
});
export default handler;
......@@ -51,6 +51,12 @@ const config = {
},
devtool: 'eval-source-map', // Default development sourcemap
resolve: {
alias: {
config: `${__dirname}/config.js`,
},
},
};
module.exports = config;
......@@ -21,4 +21,7 @@ config.plugins = [
}),
];
// Replace development with production config
config.resolve.alias.config = `${__dirname}/config.prod.js`;
module.exports = config;
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment