-
Notifications
You must be signed in to change notification settings - Fork 50
/
index.html
417 lines (361 loc) · 20.2 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
<link rel="stylesheet" type="text/css" href="tutorial/style/tutorial.css">
<h1>So You Wanna Build a Crowdfunding Site</h1>
<p><img src="tutorial/img/banner.png" alt="Splash Image"></p>
<p><strong>The tools to get funded by the crowd, should belong to the crowd.</strong></p>
<p>That's why I want to show you how to roll your own crowdfunding site, <i>in less than 300 lines of code</i>.
Everything in this tutorial is open source, and we'll only use other open-source technologies,
such as Node.js, MongoDB, and Balanced Payments.</p>
<p><strong>Here's the <a href="https://crowdtuts-1.herokuapp.com/">Live Demo</a>.</strong>
All source code and tutorial text is <a href="http://unlicense.org/">Unlicensed</a>.</p>
<h2>0. Quick Start</h2>
<p>If you just want the final crowdfunding site, clone this repo and go to the <code>/demo</code> folder.
All you need to do is set your configuration variables, and you're ready to go!
For everyone who wants the nitty gritty details, carry on.</p>
<h2>1. Setting up a basic Node.js app with Express</h2>
<p>If you haven't already done so, you'll need to <a href="http://nodejs.org/">install Node.js</a>. (duh)</p>
<p>Create a new folder for your app.
We'll be using the <a href="http://expressjs.com/">Express.js framework</a> to make things a lot more pleasant.
To install the Express node module, run this on the command line inside your app's folder:</p>
<pre><code>npm install express</code></pre>
<p>Next, create a file called <code>app.js</code>, which will be your main server logic.
The following code will initialize a simple Express app,
which just serves a basic homepage and funding page for your crowdfunding site.</p>
<pre><code>// Configuration
var CAMPAIGN_GOAL = 1000; // Your fundraising goal, in dollars
// Initialize an Express app
var express = require('express');
var app = express();
app.use("/static", express.static(__dirname + '/static')); // Serve static files
app.use(express.bodyParser()); // Can parse POST requests
app.listen(1337); // The best port
console.log("App running on http://localhost:1337");
// Serve homepage
app.get("/",function(request,response){
// TODO: Actually get fundraising total
response.send(
"<link rel='stylesheet' type='text/css' href='/static/fancy.css'>"+
"<h1>Your Crowdfunding Campaign</h1>"+
"<h2>raised ??? out of $"+CAMPAIGN_GOAL.toFixed(2)+"</h2>"+
"<a href='/fund'>Fund This</a>"
);
});
// Serve funding page
app.get("/fund",function(request,response){
response.sendfile("fund.html");
});</code></pre>
<p>Create another file named <code>fund.html</code>. This will be your funding page.</p>
<pre><code><link rel='stylesheet' type='text/css' href='/static/fancy.css'>
<h1>Donation Page:</h1></code></pre>
<p>Optionally, you may also include a stylesheet at <code>/static/fancy.css</code>,
so that your site doesn't look Hella Nasty for the rest of this tutorial.</p>
<pre><code>@import url(https://fonts.googleapis.com/css?family=Raleway:200);
body {
margin: 100px;
font-family: Raleway; /* Sexy font */
font-weight: 200;
}</code></pre>
<p>Finally, run <code>node app</code> on the command line to start your server!</p>
<p>Check out your crowdfunding site so far at <code>http://localhost:1337</code>.</p>
<p><img src="tutorial/img/homepage1.png" alt="Crowdfunding Homepage 1"></p>
<p>The homepage will display the Campaign Goal you set in the Configuration section of <code>app.js</code>.
The donations page isn't functional yet, so in the following chapters,
I'll show you how to accept and aggregate credit card payments from your wonderful backers.</p>
<h2>2. Getting started with Balanced Payments</h2>
<p><a href="https://www.balancedpayments.com/">Balanced Payments</a> isn't just another payments processor.
They've open sourced their whole site, their chat logs are publicly available, and they even discuss their roadmap in the open.
<a href="http://www.fastcolabs.com/3008944/open-company/why-i-made-my-payments-startup-an-open-company">These people <em>get</em> openness.</a></p>
<p>Best of all, you don't even need to sign up to get started with Balanced!</p>
<p><a href="https://www.balancedpayments.com/marketplaces/start">Just go to this link</a>,
and they'll generate a brand-new Test Marketplace for you,
that you can claim with an account afterwards.
Remember to keep this tab open, or save the URL, so you can come back to your Test Marketplace later.</p>
<p><img src="tutorial/img/balanced1.png" alt="Balanced Test Marketplace"></p>
<p>Click the Settings tab in the sidebar, and note your Marketplace URI and API Key Secret.</p>
<p><img src="tutorial/img/balanced2.png" alt="Balanced Settings"></p>
<p>Copy these variables to the Configuration section of <code>app.js</code> like this:</p>
<pre><code>// Configuration
var BALANCED_MARKETPLACE_URI = "/v1/marketplaces/TEST-YourMarketplaceURI";
var BALANCED_API_KEY = "YourAPIKey";
var CAMPAIGN_GOAL = 1000; // Your fundraising goal, in dollars</code></pre>
<p>Now, let's switch back to <code>fund.html</code> to create our actual payment page.</p>
<p>First, we'll include and initialize <a href="https://docs.balancedpayments.com/current/overview?language=bash#tokenizing-sensitive-information">Balanced.js</a>.
This Javascript library will securely tokenize the user's credit card info, so your server never has to handle the info directly.
Meaning, you will be free from PCI regulations.
Append the following code to <code>fund.html</code>, replacing <code>BALANCED_MARKETPLACE_URI</code> with your actual Marketplace URI:</p>
<pre><code><!-- Remember to replace BALANCED_MARKETPLACE_URI with your actual Marketplace URI! -->
<script src="https://js.balancedpayments.com/v1/balanced.js"></script>
<script>
var BALANCED_MARKETPLACE_URI = "/v1/marketplaces/TEST-YourMarketplaceURI";
balanced.init(BALANCED_MARKETPLACE_URI);
</script></code></pre>
<p>Next, create the form itself, asking for the user's Name, the Amount they want to donate, and other credit card info.
We will also add a hidden input, for the credit card token that Balanced.js will give us.
The form below comes with default values for a test Visa credit card.
Append this to <code>fund.html</code>:</p>
<pre><code><form id="payment_form" action="/pay/balanced" method="POST">
Name: <input name="name" value="Pinkie Pie"/> <br>
Amount: <input name="amount" value="12.34"/> <br>
Card Number: <input name="card_number" value="4111 1111 1111 1111"/> <br>
Expiration Month: <input name="expiration_month" value="4"/> <br>
Expiration Year: <input name="expiration_year" value="2050"/> <br>
Security Code: <input name="security_code" value="123"/> <br>
<!-- Hidden inputs -->
<input type="hidden" name="card_uri"/>
</form>
<button onclick="charge();">
Pay with Credit Card
</button></code></pre>
<p>Notice the Pay button does not submit the form directly,
but calls a <code>charge()</code> function instead, which we are going to implement next.
The <code>charge()</code> function will get the credit card token from Balanced.js,
add it as a hidden input, and submit the form.
Append this to <code>fund.html</code>:</p>
<pre><code><script>
// Get card data from form.
function getCardData(){
// Actual form data
var form = document.getElementById("payment_form");
return {
"name": form.name.value,
"card_number": form.card_number.value,
"expiration_month": form.expiration_month.value,
"expiration_year": form.expiration_year.value,
"security_code": form.security_code.value
};
}
// Charge credit card
function charge(){
// Securely tokenize card data using Balanced
var cardData = getCardData();
balanced.card.create(cardData, function(response) {
// Handle Errors (Anything that's not Success Code 201)
if(response.status!=201){
alert(response.error.description);
return;
}
// Submit form with Card URI
var form = document.getElementById("payment_form");
form.card_uri.value = response.data.uri;
form.submit();
});
};
</script></code></pre>
<p>This form will send a POST request to <code>/pay/balanced</code>, which we will handle in <code>app.js</code>.
For now, we just want to display the card token URI.
Paste the following code at the end of <code>app.js</code>:</p>
<pre><code>// Pay via Balanced
app.post("/pay/balanced",function(request,response){
// Payment Data
var card_uri = request.body.card_uri;
var amount = request.body.amount;
var name = request.body.name;
// Placeholder
response.send("Your card URI is: "+request.body.card_uri);
});</code></pre>
<p>Restart your app, (Ctrl-C to exit, then <code>node app</code> to start again) and go back to <code>http://localhost:1337</code>.</p>
<p>Your payment form should now look like this:</p>
<p><img src="tutorial/img/funding1.png" alt="Funding Form 1"></p>
<p>The default values for the form will already work, so just go ahead and click Pay With Credit Card.
(Make sure you've replaced <code>BALANCED_MARKETPLACE_URI</code> in <code>fund.html</code> with your actual Test Marketplace's URI!)
Your server will happily respond with the generated Card URI Token.</p>
<p><img src="tutorial/img/funding2.png" alt="Funding Form 2"></p>
<p>Next up, we will use this token to actually charge the given credit card!</p>
<h2>3. Charging cards through Balanced Payments</h2>
<p>Before we charge right into this, (haha) let's install two more Node.js modules for convenience.</p>
<p>Run the following in the command line:</p>
<p><code>npm install request</code> A library for simplified HTTP requests.</p>
<p><code>npm install q</code> A <a href="http://howtonode.org/promises">Promises</a> library, to pleasantly handle asynchronous calls and avoid Callback Hell.</p>
<p>Because we'll be making multiple calls to Balanced, let's also create a helper method.
The following function returns a Promise that the Balanced API has responded to whatever HTTP Request we just sent it.
Append this code to <code>app.js</code>:</p>
<pre><code>// Calling the Balanced REST API
var Q = require('q');
var httpRequest = require('request');
function _callBalanced(url,params){
// Promise an HTTP POST Request
var deferred = Q.defer();
httpRequest.post({
url: "https://api.balancedpayments.com"+BALANCED_MARKETPLACE_URI+url,
auth: {
user: BALANCED_API_KEY,
pass: "",
sendImmediately: true
},
json: params
}, function(error,response,body){
// Handle all Bad Requests (Error 4XX) or Internal Server Errors (Error 5XX)
if(body.status_code>=400){
deferred.reject(body.description);
return;
}
// Successful Requests
deferred.resolve(body);
});
return deferred.promise;
}</code></pre>
<p>Now, instead of just showing us the Card Token URI when we submit the donation form, we want to:</p>
<ol>
<li>Create an account with the Card URI</li>
<li>Charge said account for the given amount (note: you'll have to convert to cents for the Balanced API)</li>
<li>Record the transaction in the database (note: we're skipping this for now, and covering it in the next chapter)</li>
<li>Render a personalized message from the transaction</li>
</ol>
<p>Replace the <code>app.post("/pay/balanced", ... );</code> callback from the previous chapter with this:</p>
<pre><code>// Pay via Balanced
app.post("/pay/balanced",function(request,response){
// Payment Data
var card_uri = request.body.card_uri;
var amount = request.body.amount;
var name = request.body.name;
// TODO: Charge card using Balanced API
/*response.send("Your card URI is: "+request.body.card_uri);*/
Q.fcall(function(){
// Create an account with the Card URI
return _callBalanced("/accounts",{
card_uri: card_uri
});
}).then(function(account){
// Charge said account for the given amount
return _callBalanced("/debits",{
account_uri: account.uri,
amount: Math.round(amount*100) // Convert from dollars to cents, as integer
});
}).then(function(transaction){
// Donation data
var donation = {
name: name,
amount: transaction.amount/100, // Convert back from cents to dollars.
transaction: transaction
};
// TODO: Actually record the transaction in the database
return Q.fcall(function(){
return donation;
});
}).then(function(donation){
// Personalized Thank You Page
response.send(
"<link rel='stylesheet' type='text/css' href='/static/fancy.css'>"+
"<h1>Thank you, "+donation.name+"!</h1> <br>"+
"<h2>You donated $"+donation.amount.toFixed(2)+".</h2> <br>"+
"<a href='/'>Return to Campaign Page</a> <br>"+
"<br>"+
"Here's your full Donation Info: <br>"+
"<pre>"+JSON.stringify(donation,null,4)+"</pre>"
);
},function(err){
response.send("Error: "+err);
});
});</code></pre>
<p>Now restart your app, and pay through the Donation Page once again.
(Note: To cover processing fees, you have to pay more than $0.50 USD)
This time, you'll get a full Payment Complete page, with personalized information!</p>
<p><img src="tutorial/img/transaction1.png" alt="Transaction 1"></p>
<p>Furthermore, if you check the transactions tab in your Test Marketplace dashboard,
you should find that money has now been added to your balance.</p>
<p><img src="tutorial/img/transaction2.png" alt="Transaction 2"></p>
<p>We're getting close! Next, let's record donations in a MongoDB database.</p>
<h2>4. Recording donations with MongoDB</h2>
<p><a href="http://mongodb.org">MongoDB</a> is a popular open-source <a href="http://www.10gen.com/nosql">NoSQL</a> database.
NoSQL is especially handy for rapid prototyping, because of its dynamic schemas.
In other words, you can just make stuff up on the fly.
This will be useful if, in the future, you want to record extra details about each donation,
such as the donator's email address, reward levels, favorite color, etc.</p>
<p>Start up a MongoDB database, and get its URI.
You can use a remote database with a service such as <a href="https://www.mongohq.com/home">MongoHQ</a>,
but for this tutorial, let's run MongoDB locally.
<a href="http://docs.mongodb.org/manual/installation/">Here are the instructions for installing and running MongoDB on your computer</a>.</p>
<p>Once you've done that, add the MongoDB URI to your Configuration section at the top of <code>app.js</code>.</p>
<pre><code>// Configuration
var MONGO_URI = "mongodb://localhost:27017/test";
var BALANCED_MARKETPLACE_URI = "/v1/marketplaces/TEST-YourMarketplaceURI";
var BALANCED_API_KEY = "YourAPIKey";
var CAMPAIGN_GOAL = 1000; // Your fundraising goal, in dollars</code></pre>
<p>Now, let's install the native MongoDB driver for Node.js:</p>
<p><code>npm install mongodb</code></p>
<p>Add the following code to the end of <code>app.js</code>.
This will return a Promise that we've recorded a donation in MongoDB.</p>
<pre><code>// Recording a Donation
var mongo = require('mongodb').MongoClient;
function _recordDonation(donation){
// Promise saving to database
var deferred = Q.defer();
mongo.connect(MONGO_URI,function(err,db){
if(err){ return deferred.reject(err); }
// Insert donation
db.collection('donations').insert(donation,function(err){
if(err){ return deferred.reject(err); }
// Promise the donation you just saved
deferred.resolve(donation);
// Close database
db.close();
});
});
return deferred.promise;
}</code></pre>
<p>Previously, we skipped over actually recording a donation to a database.
Go back, and replace that section of code with this:</p>
<pre><code>// TODO: Actually log the donation with MongoDB
/*return Q.fcall(function(){
return donation;
});*/
// Record donation to database
return _recordDonation(donation);</code></pre>
<p>Restart your app, and make another donation.
If you run <code>db.donations.find()</code> on your MongoDB instance, you'll find the donation you just logged!</p>
<p><img src="tutorial/img/transaction3.png" alt="Transaction 3"></p>
<p><em>Just one step left...</em></p>
<p>Finally, we will use these recorded donations to calculate how much money we've raised.</p>
<h2>5. Completing the Donation</h2>
<p>Whether it's showing progress or showing off,
you'll want to tell potential backers how much your campaign's already raised.</p>
<p>To get the total amount donated, simply query for all donation amounts from MongoDB, and add them up.
Here's how you do that with MongoDB, with an asynchronous Promise for it.
Append this code to <code>app.js</code>:</p>
<pre><code>// Get total donation funds
function _getTotalFunds(){
// Promise the result from database
var deferred = Q.defer();
mongo.connect(MONGO_URI,function(err,db){
if(err){ return deferred.reject(err); }
// Get amounts of all donations
db.collection('donations')
.find( {}, {amount:1} ) // Select all, only return "amount" field
.toArray(function(err,donations){
if(err){ return deferred.reject(err); }
// Sum up total amount, and resolve promise.
var total = donations.reduce(function(previousValue,currentValue){
return previousValue + currentValue.amount;
},0);
deferred.resolve(total);
// Close database
db.close();
});
});
return deferred.promise;
}</code></pre>
<p>Now, let's go back to where we were serving a basic homepage.
Let's change that, to <em>actually</em> calculate your total funds, and show the world how far along your campaign has gotten.</p>
<pre><code>// Serve homepage
app.get("/",function(request,response){
// TODO: Actually get fundraising total
/*response.send(
"<link rel='stylesheet' type='text/css' href='/static/fancy.css'>"+
"<h1>Your Crowdfunding Campaign</h1>"+
"<h2>raised ??? out of $"+CAMPAIGN_GOAL.toFixed(2)+"</h2>"+
"<a href='/fund'>Fund This</a>"
);*/
Q.fcall(_getTotalFunds).then(function(total){
response.send(
"<link rel='stylesheet' type='text/css' href='/static/fancy.css'>"+
"<h1>Your Crowdfunding Campaign</h1>"+
"<h2>raised $"+total.toFixed(2)+" out of $"+CAMPAIGN_GOAL.toFixed(2)+"</h2>"+
"<a href='/fund'>Fund This</a>"
);
});
});</code></pre>
<p>Restart the app, and look at your final homepage.</p>
<p><img src="tutorial/img/homepage2.png" alt="Crowdfunding Homepage 2"></p>
<p><em>It's... beautiful.</em></p>
<p>You'll see that your total already includes the donations recorded from the previous chapter.
Make another payment through the Donations Page, and watch your funding total go up.</p>
<p>Congratulations, you just made your very own crowdfunding site!</p>