Skip to content

Commit 3ca1e98

Browse files
committed
Initial commit
0 parents  commit 3ca1e98

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+3359
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/config/local.json
2+
/Gemfile.lock
3+
node_modules

Gemfile

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Execute bundler hook (analogous to sourcing a dotfile)
2+
['~/.', '/etc/'].any? do |file|
3+
File.lstat(path = File.expand_path(file + 'bundle-gemfile-hook')) rescue next
4+
eval(File.read(path), binding, path); break true
5+
end || source('https://rubygems.org/')
6+
7+
gem 'sass'

Gruntfile.js

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
module.exports = function(grunt) {
2+
// configuration
3+
grunt.initConfig({
4+
pkg: grunt.file.readJSON('package.json'),
5+
6+
uglify: {
7+
options: {
8+
banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n',
9+
report: 'min'
10+
},
11+
build: {
12+
files: {
13+
'public/assets/js/shop.min.js': ['public/assets/js/bag.js',
14+
'public/assets/js/underscore.min.js',
15+
'public/assets/js/requestanimationframe.js',
16+
'public/assets/js/tween.min.js', 'public/assets/js/spin.min.js',
17+
'public/assets/js/animation.js', 'public/assets/js/shop.js',
18+
'public/assets/js/ga.js'],
19+
}
20+
}
21+
},
22+
23+
sass: {
24+
options: {
25+
style: 'compressed',
26+
bundleExec: true,
27+
},
28+
build: {
29+
files: {
30+
'public/assets/css/shop.min.css': 'public/assets/css/shop.scss'
31+
}
32+
}
33+
},
34+
35+
watch: {
36+
css: {
37+
files: 'public/assets/css/shop.scss',
38+
tasks: 'sass'
39+
},
40+
js: {
41+
files: ['public/assets/js/bag.js', 'public/assets/js/underscore.min.js',
42+
'public/assets/js/requestanimationframe.js',
43+
'public/assets/js/tween.min.js', 'public/assets/js/spin.min.js',
44+
'public/assets/js/animation.js', 'public/assets/js/shop.js',
45+
'public/assets/js/ga.js'],
46+
tasks: 'uglify'
47+
}
48+
}
49+
});
50+
51+
// plugins
52+
grunt.loadNpmTasks('grunt-contrib-uglify');
53+
grunt.loadNpmTasks('grunt-contrib-sass');
54+
grunt.loadNpmTasks('grunt-contrib-watch');
55+
56+
// tasks
57+
grunt.registerTask('default', ['uglify', 'sass', 'watch']);
58+
};

LICENSE.txt

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Copyright (c) 2013 Stripe
2+
3+
MIT License
4+
5+
Permission is hereby granted, free of charge, to any person obtaining
6+
a copy of this software and associated documentation files (the
7+
"Software"), to deal in the Software without restriction, including
8+
without limitation the rights to use, copy, modify, merge, publish,
9+
distribute, sublicense, and/or sell copies of the Software, and to
10+
permit persons to whom the Software is furnished to do so, subject to
11+
the following conditions:
12+
13+
The above copyright notice and this permission notice shall be
14+
included in all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Shop
2+
3+
![Shop](https://raw.github.com/stripe/shop/master/examples/screenshot.png)
4+
5+
This project contains the code behind the single-page store at [Stripe
6+
Shop](https://shop.stripe.com). We ported the backend to
7+
[Parse](https://parse.com/)'s Code Cloud so you can easily launch and
8+
modify your own copy. (Also, we didn't want to open-source the photo
9+
of Kat and Thairu, so we decided to take a replacement.)
10+
11+
Feel free to take whatever pieces you find useful! We ask only that
12+
you don't use it to sell actual Stripe T-shirts ☺. Improvements are
13+
welcome — just open a pull request.
14+
15+
## The details
16+
17+
We have a running [live demo](https://shop-demo.parseapp.com/) of the
18+
app. It's running in Stripe's [test
19+
mode](https://stripe.com/docs/testing), so you'll have to use
20+
`4242-4242-4242-4242` as the card (and we won't actually send you a
21+
shirt, sorry!).
22+
23+
## Getting up and running
24+
25+
To get your own instance of Shop up and running, you'll need to do the
26+
following:
27+
28+
1. Create a [new Parse app](https://parse.com/apps/new).
29+
1. Copy the Application ID and Master Key to `config/global.json`
30+
1. Set up the `parse` command line utility (you may find [their
31+
docs](https://parse.com/docs/cloud_code_guide) helpful).
32+
1. Create a `parseapp.com` subdomain for your app. The same docs
33+
should be helpful.
34+
1. Run `parse deploy`. You now have a running Shop!
35+
36+
Not required to get the app running, but you'll probaby also want to:
37+
38+
1. Create your own Stripe account and puts its keys into
39+
`cloud/config.js`. (By default, Shop uses a fixed test account.)
40+
1. Put your Google analytics tracking information into
41+
`public/assets/js/ga.js`.
42+
43+
## Contributors
44+
45+
- [Karthik Viswanathan](https://twitter.com/karthikvnet)
46+
- [Ludwig Pettersson](https://twitter.com/ludwig)
47+
- [Greg Cooper](https://twitter.com/awfy)
48+
- [Greg Brockman](https://twitter.com/thegdb)

cloud/config.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
var config = {
2+
// Price in dollars for each shirt
3+
price_per_shirt: 10,
4+
5+
// Set to your Stripe publishable key. (The one here is for a test
6+
// account, which may or may not be working.)
7+
stripe_publishable_key: 'pk_test_qZ3TjohtOSXEeXhPubJgY64y',
8+
9+
// Set to your Stripe secret key. (The one here is for a test account,
10+
// which may or may not be working.)
11+
stripe_secret_key: 'sk_test_ZBEMC1TnpX2tlRC6L1Aqm9yM',
12+
13+
// You should set this to a static (but secret) value, as it's
14+
// used to authenticate session data. `openssl rand -base64 24`
15+
// should do the trick. (It doesn't matter for the default app
16+
// since we only use the session for the CSRF token.)
17+
secret: 'set to a secret!!'
18+
};
19+
20+
module.exports = config;

cloud/main.js

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
var Stripe = require('stripe')
2+
, express = require('express')
3+
, app = express()
4+
, path = require('path')
5+
, models = require('cloud/models')
6+
, config = require('cloud/config')
7+
;
8+
9+
Stripe.initialize(config.stripe_secret_key);
10+
11+
app.set('views', 'cloud/views');
12+
app.set('view engine', 'ejs');
13+
app.use(express.bodyParser());
14+
app.use(express.cookieParser());
15+
app.use(express.cookieSession({
16+
secret: config.secret,
17+
cookie: { httpOnly: true }
18+
}));
19+
app.use(express.csrf());
20+
app.use(function(req, res, next) {
21+
res.locals.csrf_field = req.session._csrf;
22+
next();
23+
});
24+
25+
app.get('/', function(req, res) {
26+
res.render('index', {
27+
config: config
28+
});
29+
});
30+
31+
app.post('/pay', function(req, res) {
32+
var order = new models.Order()
33+
, token = null;
34+
35+
for (param in models.Order.schema) {
36+
order.set(param, req.body[param]);
37+
}
38+
39+
// Coerce to a string out of paranoia
40+
Stripe.Tokens.retrieve(req.body.stripe_token + '').then(function(result) {
41+
token = result
42+
if (!token.email) {
43+
return Parse.Promise.error('You did not provide an email address.\n');
44+
}
45+
46+
order.set('email', token.email);
47+
order.set('state', 'unpaid');
48+
return order.save();
49+
}).then(function(order) {
50+
return Stripe.Customers.create({
51+
description: order.get('name'),
52+
email: token.email,
53+
card: token.id
54+
})
55+
}).then(function(customer) {
56+
return Stripe.Charges.create({
57+
amount: order.calculateAmount(),
58+
description: order.get('name') +
59+
' <' + order.get('email') + ' > - Shop T-Shirt Order',
60+
currency: 'usd',
61+
customer: customer.id
62+
});
63+
}).then(function(charge) {
64+
order.set('charge_id', charge.id);
65+
order.set('state', 'paid');
66+
return order.save();
67+
}).then(function(order) {
68+
res.send('Success!\n');
69+
}, function(error) {
70+
console.log(error);
71+
res.send(400, error.message + '\n');
72+
})
73+
});
74+
75+
app.listen();

cloud/models.js

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
var config = require('cloud/config')
2+
, Order = Parse.Object.extend('Order')
3+
, orderTypeRegex = /^[UW]$/
4+
, orderColorRegex = /^[G]$/
5+
, orderSizeRegex = /^(S|M|L|XL|2XL)$/
6+
;
7+
8+
function getError(spec, name, value) {
9+
if (spec.required && value === undefined) {
10+
return name + ' field is missing';
11+
}
12+
13+
if (value !== undefined && spec.type && typeof(value) != spec.type) {
14+
return name + ' has the wrong type';
15+
}
16+
17+
if (spec.min_length && value.length < spec.min_length) {
18+
return name + ' must be at least ' + value.length + ' characters long.';
19+
}
20+
21+
if (spec.max_length && value.length > spec.max_length) {
22+
return name + ' must be at least ' + value.length + ' characters long.';
23+
}
24+
25+
if (spec.getError) {
26+
var error = spec.getError(value);
27+
if (error)
28+
return error;
29+
}
30+
}
31+
32+
function orderError(serializedOrder) {
33+
try {
34+
order = JSON.parse(serializedOrder);
35+
} catch (e) {
36+
return 'Your order format is invalid.';
37+
}
38+
39+
if (!(order instanceof Array)) {
40+
return 'Your order is not an array.';
41+
}
42+
43+
if (order.length == 0) {
44+
return 'Your order is empty.';
45+
}
46+
47+
for (var i = 0; i < order.length; i++) {
48+
var item = order[i];
49+
var type = item.type;
50+
var color = item.color;
51+
var size = item.size;
52+
var quantity = item.quantity;
53+
54+
if (Object.keys(item).length != 4)
55+
return 'Your order has an invalid item.';
56+
else if (!(typeof(type) == 'string') || !type.match(orderTypeRegex))
57+
return 'Your order has an invalid type for one item.';
58+
else if (!(typeof(color) == 'string') || !color.match(orderColorRegex))
59+
return 'Your order has an invalid type for one item.';
60+
else if (!(typeof(size) == 'string') || !size.match(orderSizeRegex))
61+
return 'Your order has an invalid size for one item.';
62+
else if (!(typeof(quantity) == 'number') || quantity == 0)
63+
return 'Your order has an invalid quantity for one item.';
64+
}
65+
}
66+
67+
Parse.Cloud.beforeSave('Order', function(request, response) {
68+
for (param in Order.schema) {
69+
var error = getError(Order.schema[param], param, request.object.get(param))
70+
if (error) {
71+
response.error(error);
72+
return;
73+
}
74+
}
75+
76+
response.success();
77+
});
78+
79+
Order.schema = {
80+
name: {required: true, min_length: 2, max_length: 100, type: 'string'},
81+
stripe_token: {required: true, min_length: 2, max_length: 100, type: 'string'},
82+
83+
address: {required: true, min_length: 2, max_length: 100, type: 'string'},
84+
address_line_1: {type: 'string'},
85+
address_line_2: {type: 'string'},
86+
address_city: {type: 'string'},
87+
address_state: {type: 'string'},
88+
address_zip: {type: 'string'},
89+
address_country: {type: 'string'},
90+
order: {required: true, getError: orderError, type: 'string'}
91+
}
92+
93+
Order.prototype.calculateAmount = function() {
94+
var order = JSON.parse(this.get('order'))
95+
, quantity = 0
96+
;
97+
98+
for (var i = 0; i < order.length; i++) {
99+
quantity += order[i].quantity
100+
}
101+
102+
return quantity * config.price_per_shirt * 100;
103+
}
104+
105+
exports.Order = Order;

0 commit comments

Comments
 (0)