Credit Card Guide (Simple)
This guide walks through building a very simple credit card payment form. It uses Stripe.js v2 API to create a token which can be used to create a charge. It also performs simple validation on the payment form values.
In this guide, you will learn how to:
- Set up a basic CanJS application.
- Collect form data and post it to a service endpoint when the form is submitted.
- Do basic validation.
The final widget looks like:
To use the widget:
- Enter a Card Number, Expiration Date, and CVC.
- Click on the form so those inputs lose focus. The Pay button should become enabled.
- Click the Pay button to get a token from Stripe, which could be used to create a credit card payment.
- Change the inputs to invalid values. An error message should appear, the invalid inputs should be highlighted red, and the Pay button should become disabled.
START THIS TUTORIAL BY CLONING THE FOLLOWING JS BIN:
This JS Bin has initial prototype HTML and CSS which is useful for getting the application to look right.
The following sections are broken down into:
- The problem — A description of what the section is trying to accomplish.
- What you need to know — Information about CanJS that is useful for solving the problem.
- The solution — The solution to the problem.
Setup
The problem
Let’s create a payment-view
template and render it with
a ViewModel called PaymentVM
, which will have
an amount
property that defaults to 9.99
. When complete, we
should be able update the displayed “pay amount” by writing the
following in the console:
viewModel.amount = 1000;
What you need to know
To use Stripe, you must call Stripe.setPublishableKey.
A basic CanJS setup uses instances of a ViewModel to manage the behavior of a View. A ViewModel type is defined, an instance of it is created and passed to a View as follows:
// Define the ViewModel type var MyViewModel = can.DefineMap.extend("MyViewModel",{ ... }) // Create an instance of the ViewModel var viewModel = new MyViewModel(); // Get a View var view = can.stache.from("my-view"); // Render the View with the ViewModel instance var frag = view(viewModel); document.body.appendChild(frag);
CanJS uses can-stache to render data in a template and keep it live. Templates can be authored in
<script>
tags like:<script type="text/stache" id="app-view"> TEMPLATE CONTENT </script>
A can-stache template uses {{key}} magic tags to insert data into the HTML output like:
<script type="text/stache" id="app-view"> {{something.name}} </script>
Load a template from a
<script>
tag with can.stache.from like:var template = can.stache.from(SCRIPT_ID);
Render the template with data into a documentFragment like:
var frag = template({ something: {name: "Derek Brunson"} });
Insert a fragment into the page with:
document.body.appendChild(frag);
DefineMap.extend allows you to define a property with a default value like:
ProductVM = can.DefineMap.extend("ProductVM",{ age: {value: 34} })
This lets you create instances of that type, get & set those properties, and listen to changes like:
var productVM = new ProductVM({}); productVM.age //-> 34 productVM.on("age", function(ev, newAge){ console.log("person age changed to ", newAge); }); productVM.age = 35 //-> logs "person age changed to 35"
The solution
Update the HTML tab to:
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Credit Card Form">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type='text/stache' id="payment-view">
<form>
<input type='text' name='number' placeholder='Card Number'/>
<input type='text' name='expiry' placeholder='MM-YY'/>
<input type='text' name='cvc' placeholder='CVC'/>
<button>Pay ${{amount}}</button>
</form>
</script>
<script src="https://unpkg.com/can@3/dist/global/can.js"></script>
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
</body>
</html>
Update the JavaScript tab to:
Stripe.setPublishableKey('pk_test_zCC2JrO3KSMeh7BB5x9OUe2U');
var PaymentVM = can.DefineMap.extend({
amount: {value: 9.99}
});
var viewModel = new PaymentVM();
var paymentView = can.stache.from("payment-view");
var frag = paymentView( viewModel );
document.body.appendChild( frag );
Read form values
The problem
Let’s send the form values to the ViewModel so we can process and validate them. In this step, we’ll send the form values to the ViewModel and print out the values to make sure the ViewModel has them correctly.
Print out the exported values like:
<p>{{userCardNumber}}, {{userExpiry}}, {{userCVC}}</p>
What you need to know
Use value:bind to set up a two-way binding in
can-stache
. For example, the following keepsemail
on the ViewModel and the input’svalue
in sync:<input value:bind="email"/>
DefineMap.extend allows you to define a property by defining its type like so:
Person = can.DefineMap.extend("Person",{ name: "string", age: "number" })
The solution
Update the HTML tab to:
<script type='text/stache' id="payment-view">
<form>
<input type='text' name='number' placeholder='Card Number'
value:bind="userCardNumber"/>
<input type='text' name='expiry' placeholder='MM-YY'
value:bind="userExpiry"/>
<input type='text' name='cvc' placeholder='CVC'
value:bind="userCVC"/>
<button>Pay ${{amount}}</button>
<p>{{userCardNumber}}, {{userExpiry}}, {{userCVC}}</p>
</form>
</script>
Update the JavaScript tab to:
Stripe.setPublishableKey('pk_test_zCC2JrO3KSMeh7BB5x9OUe2U');
var PaymentVM = can.DefineMap.extend({
amount: {value: 9.99},
userCardNumber: "string",
userExpiry: "string",
userCVC: "string"
});
var viewModel = new PaymentVM();
var paymentView = can.stache.from("payment-view");
var frag = paymentView( viewModel );
document.body.appendChild( frag );
Format form values
The problem
Our data needs to be cleaned up before we pass it to the server. We need to create the following properties, with associated behaviors:
cardNumber
- The user’s card number as a string without hyphens (-
).expiryMonth
- A number for the month entered.expiryYear
- A number for the year entered.cvc
- A number for the cvc entered.
So that we can print out the values like:
<p>{{cardNumber}}, {{expiryMonth}}-{{expiryYear}}, {{cvc}}</p>
What you need to know
ES5 Getter Syntax can be used to define a
DefineMap
property that changes when another property changes. For example, the following defines afirstName
property that always has the first word of thefullName
property:DefineMap.extend({ fullName: "string", get firstName(){ return this.fullName.split(" ")[0]; } });
The solution
Update the HTML tab to:
<script type='text/stache' id="payment-view">
<form>
<input type='text' name='number' placeholder='Card Number'
value:bind="userCardNumber"/>
<input type='text' name='expiry' placeholder='MM-YY'
value:bind="userExpiry"/>
<input type='text' name='cvc' placeholder='CVC'
value:bind="userCVC"/>
<button>Pay ${{amount}}</button>
<p>{{userCardNumber}}, {{userExpiry}}, {{userCVC}}</p>
<p>{{cardNumber}}, {{expiryMonth}}-{{expiryYear}}, {{cvc}}</p>
</form>
</script>
Update the JavaScript tab to:
Stripe.setPublishableKey('pk_test_zCC2JrO3KSMeh7BB5x9OUe2U');
var PaymentVM = can.DefineMap.extend({
amount: {value: 9.99},
userCardNumber: "string",
get cardNumber(){
return this.userCardNumber ? this.userCardNumber.replace(/-/g,""): null;
},
userExpiry: "string",
get expiryParts() {
if(this.userExpiry) {
return this.userExpiry.split("-").map(function(p){
return parseInt(p,10);
});
}
},
get expiryMonth() {
return this.expiryParts && this.expiryParts[0];
},
get expiryYear() {
return this.expiryParts && this.expiryParts[1];
},
userCVC: "string",
get cvc(){
return this.userCVC ?
parseInt(this.userCVC,10) : null;
}
});
var viewModel = new PaymentVM();
var paymentView = can.stache.from("payment-view");
var frag = paymentView( viewModel );
document.body.appendChild( frag );
Validate individual form values
The problem
We need to add class='is-error'
when a form value has a value that
is not valid according to Stripe’s validators. To do that, we need to
create the following properties that will return an error message for
their respective form property:
cardError
- “Invalid card number (ex: 4242-4242-4242).”expiryError
- “Invalid expiration date (ex: 01-22).”cvcError
- “Invalid CVC (ex: 123).”
What you need to know
Stripe has validation methods:
Stripe.card.validateCardNumber(number)
Stripe.card.validateExpiry(month, year)
Stripe.card.validateCVC(cvc)
Use {{#if(value)}} to do
if/else
branching incan-stache
.{{#if(error)}}class='is-error'{{/if}}
The solution
Update the HTML tab to:
<script type='text/stache' id="payment-view">
<form>
<input type='text' name='number' placeholder='Card Number'
{{#if(cardError)}}class='is-error'{{/if}}
value:bind="userCardNumber"/>
<input type='text' name='expiry' placeholder='MM-YY'
{{#if(expiryError)}}class='is-error'{{/if}}
value:bind="userExpiry"/>
<input type='text' name='cvc' placeholder='CVC'
{{#if(cvcError)}}class='is-error'{{/if}}
value:bind="userCVC"/>
<button>Pay ${{amount}}</button>
<p>{{userCardNumber}}, {{userExpiry}}, {{userCVC}}</p>
<p>{{cardNumber}}, {{expiryMonth}}-{{expiryYear}}, {{cvc}}</p>
</form>
</script>
Update the JavaScript tab to:
Stripe.setPublishableKey('pk_test_zCC2JrO3KSMeh7BB5x9OUe2U');
var PaymentVM = can.DefineMap.extend({
amount: {value: 9.99},
userCardNumber: "string",
get cardNumber(){
return this.userCardNumber ? this.userCardNumber.replace(/-/g,""): null;
},
get cardError() {
if( this.cardNumber && !Stripe.card.validateCardNumber(this.cardNumber) ) {
return "Invalid card number (ex: 4242-4242-4242).";
}
},
userExpiry: "string",
get expiryParts() {
if(this.userExpiry) {
return this.userExpiry.split("-").map(function(p){
return parseInt(p,10);
});
}
},
get expiryMonth() {
return this.expiryParts && this.expiryParts[0];
},
get expiryYear() {
return this.expiryParts && this.expiryParts[1];
},
get expiryError() {
if( (this.expiryMonth || this.expiryYear) &&
!Stripe.card.validateExpiry(this.expiryMonth, this.expiryYear) ) {
return "Invalid expiration date (ex: 01-22).";
}
},
userCVC: "string",
get cvc(){
return this.userCVC ?
parseInt(this.userCVC,10) : null;
},
get cvcError() {
if(this.cvc && !Stripe.card.validateCVC(this.cvc)) {
return "Invalid CVC (ex: 123).";
}
}
});
var viewModel = new PaymentVM();
var paymentView = can.stache.from("payment-view");
var frag = paymentView( viewModel );
document.body.appendChild( frag );
Get payment token from Stripe
The problem
When the user submits the form, we need to call Stripe to get a token that we may use to charge the credit card. When we get a token, we will simply alert it to the user like:
alert("Token: "+response.id);
After submitting the form, you should see an alert like:
What you need to know
Use on:event to listen to an event on an element and call a method in
can-stache
. For example, the following callsdoSomething()
when the<div>
is clicked:<div on:click="doSomething(scope.event)"> ... </div>
Notice that it also passed the event object with scope.event.
To prevent a form from submitting, call event.preventDefault().
Stripe.card.createToken can be used to get a token that can be used to charge a card:
Stripe.card.createToken({ number: this.cardNumber, cvc: this.cvc, exp_month: this.expiryMonth, exp_year: this.expiryYear }, stripeResponseHandler(status, response) )
stripeResponseHandler
gets called back with either:- success: a status of
200
and a response with anid
that is the token. - failure: a status other than
200
and a response with anerror.message
value detailing what went wrong.
- success: a status of
The solution
Update the HTML tab to:
<script type='text/stache' id="payment-view">
<form on:submit="pay(scope.event)">
<input type='text' name='number' placeholder='Card Number'
{{#if(cardError)}}class='is-error'{{/if}}
value:bind="userCardNumber"/>
<input type='text' name='expiry' placeholder='MM-YY'
{{#if(expiryError)}}class='is-error'{{/if}}
value:bind="userExpiry"/>
<input type='text' name='cvc' placeholder='CVC'
{{#if(cvcError)}}class='is-error'{{/if}}
value:bind="userCVC"/>
<button>Pay ${{amount}}</button>
<p>{{userCardNumber}}, {{userExpiry}}, {{userCVC}}</p>
<p>{{cardNumber}}, {{expiryMonth}}-{{expiryYear}}, {{cvc}}</p>
</form>
</script>
Update the JavaScript tab to:
Stripe.setPublishableKey('pk_test_zCC2JrO3KSMeh7BB5x9OUe2U');
var PaymentVM = can.DefineMap.extend({
amount: {value: 9.99},
userCardNumber: "string",
get cardNumber(){
return this.userCardNumber ? this.userCardNumber.replace(/-/g,""): null;
},
get cardError() {
if( this.cardNumber && !Stripe.card.validateCardNumber(this.cardNumber) ) {
return "Invalid card number (ex: 4242-4242-4242).";
}
},
userExpiry: "string",
get expiryParts() {
if(this.userExpiry) {
return this.userExpiry.split("-").map(function(p){
return parseInt(p,10);
});
}
},
get expiryMonth() {
return this.expiryParts && this.expiryParts[0];
},
get expiryYear() {
return this.expiryParts && this.expiryParts[1];
},
get expiryError() {
if( (this.expiryMonth || this.expiryYear) &&
!Stripe.card.validateExpiry(this.expiryMonth, this.expiryYear) ) {
return "Invalid expiration date (ex: 01-22).";
}
},
userCVC: "string",
get cvc(){
return this.userCVC ?
parseInt(this.userCVC,10) : null;
},
get cvcError() {
if(this.cvc && !Stripe.card.validateCVC(this.cvc)) {
return "Invalid CVC (ex: 123).";
}
},
pay: function(event){
event.preventDefault();
Stripe.card.createToken({
number: this.cardNumber,
cvc: this.cvc,
exp_month: this.expiryMonth,
exp_year: this.expiryYear
}, function(status, response){
if(status === 200) {
alert("Token: "+response.id);
// stripe.charges.create({
// amount: this.amount,
// currency: "usd",
// description: "Example charge",
// source: response.id,
// })
} else {
alert("Error: "+response.error.message);
}
});
}
});
var viewModel = new PaymentVM();
var paymentView = can.stache.from("payment-view");
var frag = paymentView( viewModel );
document.body.appendChild( frag );
Validate the form
The problem
We need to show a warning message when information is entered incorrectly and disable the form until they have entered it correctly.
To do that, we’ll add the following properties to the ViewModel:
isCardValid
- returns true if the card is validisCardInvalid
- returns true if the card is invaliderrorMessage
- returns the error for the first form value that has an error.
What you need to know
Use {$disabled} to make an input disabled, like:
<button disabled:from="isCardInvalid">...
The solution
Update the HTML tab to:
<script type='text/stache' id="payment-view">
<form on:submit="pay(scope.event)">
{{#if(errorMessage)}}
<div class="message">{{errorMessage}}</div>
{{/if}}
<input type='text' name='number' placeholder='Card Number'
{{#if(cardError)}}class='is-error'{{/if}}
value:bind="userCardNumber"/>
<input type='text' name='expiry' placeholder='MM-YY'
{{#if(expiryError)}}class='is-error'{{/if}}
value:bind="userExpiry"/>
<input type='text' name='cvc' placeholder='CVC'
{{#if(cvcError)}}class='is-error'{{/if}}
value:bind="userCVC"/>
<button disabled:from="isCardInvalid">Pay ${{amount}}</button>
</form>
</script>
Update the JavaScript tab to:
Stripe.setPublishableKey('pk_test_zCC2JrO3KSMeh7BB5x9OUe2U');
var PaymentVM = can.DefineMap.extend({
amount: {value: 9.99},
userCardNumber: "string",
get cardNumber(){
return this.userCardNumber ? this.userCardNumber.replace(/-/g,""): null;
},
get cardError() {
if( this.cardNumber && !Stripe.card.validateCardNumber(this.cardNumber) ) {
return "Invalid card number (ex: 4242-4242-4242).";
}
},
userExpiry: "string",
get expiryParts() {
if(this.userExpiry) {
return this.userExpiry.split("-").map(function(p){
return parseInt(p,10);
});
}
},
get expiryMonth() {
return this.expiryParts && this.expiryParts[0];
},
get expiryYear() {
return this.expiryParts && this.expiryParts[1];
},
get expiryError() {
if( (this.expiryMonth || this.expiryYear) &&
!Stripe.card.validateExpiry(this.expiryMonth, this.expiryYear) ) {
return "Invalid expiration date (ex: 01-22).";
}
},
userCVC: "string",
get cvc(){
return this.userCVC ?
parseInt(this.userCVC,10) : null;
},
get cvcError() {
if(this.cvc && !Stripe.card.validateCVC(this.cvc)) {
return "Invalid CVC (ex: 123).";
}
},
pay: function(event){
event.preventDefault();
Stripe.card.createToken({
number: this.cardNumber,
cvc: this.cvc,
exp_month: this.expiryMonth,
exp_year: this.expiryYear
}, function(status, response){
if(status === 200) {
alert("Token: "+response.id);
// stripe.charges.create({
// amount: this.amount,
// currency: "usd",
// description: "Example charge",
// source: response.id,
// })
} else {
alert("Error: "+response.error.message);
}
});
},
get isCardValid(){
return Stripe.card.validateCardNumber(this.cardNumber) &&
Stripe.card.validateExpiry(this.expiryMonth, this.expiryYear) &&
Stripe.card.validateCVC(this.cvc);
},
get isCardInvalid(){
return !this.isCardValid;
},
get errorMessage(){
return this.cardError || this.expiryError || this.cvcError;
}
});
var viewModel = new PaymentVM();
var paymentView = can.stache.from("payment-view");
var frag = paymentView( viewModel );
document.body.appendChild( frag );
Result
When complete, you should have a working credit card payment form like the following JS Bin: