If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
Let's talk about something fun: coupon codes. When a user checks out, I want them to be able to add a coupon code. Fortunately Stripe totally supports this, and that makes our job easier.
In fact, in the Stripe dashboard, there's a Coupon section. Create a new coupon: you can choose either a percent off or an amount off. To make things simple, I'm only going to support coupons that are for a specific amount off.
Create one that saves us $50
. Oh, and in case this is used on an order with a
subscription, you can set the duration to once, multi-month or forever.
Then, give the code a creative, unique code: like CHEAP_SHEEP. Oh, and the coupon codes are case sensitive.
Tip
I'm choosing to create my coupons through Stripe's admin interface. If you want an admin section that does this on your site, that's totally possible! You can create new Coupons through Stripe's API.
Back on our site, before we get into any Stripe integration, we need to add a spot for adding coupons during checkout.
Open the template: order/checkout.html.twig
. Below the cart table, add a button,
give it some styling classes and a js-show-code-form
class. Say, "I have a coupon code":
... lines 1 - 19 | |
{% block body %} | |
<div class="nav-space-checkout"> | |
<div class="container"> | |
<div class="row"> | |
... lines 24 - 26 | |
<div class="col-xs-12 col-sm-6"> | |
... lines 28 - 57 | |
<button class="btn btn-xs btn-link pull-right js-show-code-form"> | |
I have a coupon code | |
</button> | |
... lines 61 - 75 | |
</div> | |
... lines 77 - 79 | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Instead of adding this form by hand, open your tutorial/
directory: this is
included in the code download. Open coupon-form.twig
, copy its code, then paste
it below the button:
... lines 1 - 19 | |
{% block body %} | |
<div class="nav-space-checkout"> | |
<div class="container"> | |
<div class="row"> | |
... lines 24 - 26 | |
<div class="col-xs-12 col-sm-6"> | |
... lines 28 - 57 | |
<button class="btn btn-xs btn-link pull-right js-show-code-form"> | |
I have a coupon code | |
</button> | |
<div class="js-code-form" style="display: none;"> | |
<form action="" method="POST" class="form-inline"> | |
<div class="form-group"> | |
<div class="input-group"> | |
<span class="input-group-addon"> | |
<i class="fa fa-terminal"></i> | |
</span> | |
<input type="text" name="code" autocomplete="off" class="form-control" placeholder="Coupon Code"/> | |
</div> | |
<button type="submit" class="btn btn-primary">Add</button> | |
</div> | |
</form> | |
</div> | |
</div> | |
... lines 77 - 79 | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
This new div
is hidden by default and has a js-code-form
class that we'll use soon
via JavaScript. And, it has just one field named code
.
Copy the js-show-code-form
class and scroll up to the javascripts
block. Add
a new document.ready()
function:
... lines 1 - 3 | |
{% block javascripts %} | |
... lines 5 - 8 | |
<script> | |
jQuery(document).ready(function() { | |
... lines 11 - 15 | |
}); | |
</script> | |
{% endblock %} | |
... lines 19 - 84 |
Inside, find the .js-show-code-form
element and on click
, add a callback. Start
with our favorite e.preventDefault()
:
... lines 1 - 3 | |
{% block javascripts %} | |
... lines 5 - 8 | |
<script> | |
jQuery(document).ready(function() { | |
$('.js-show-code-form').on('click', function(e) { | |
e.preventDefault(); | |
... lines 13 - 14 | |
}) | |
}); | |
</script> | |
{% endblock %} | |
... lines 19 - 84 |
Then, scroll down to the form, copy the js-code-form
class, use jQuery to select this,
and... drumroll... show it!
... lines 1 - 3 | |
{% block javascripts %} | |
... lines 5 - 8 | |
<script> | |
jQuery(document).ready(function() { | |
$('.js-show-code-form').on('click', function(e) { | |
e.preventDefault(); | |
$('.js-code-form').show(); | |
}) | |
}); | |
</script> | |
{% endblock %} | |
... lines 19 - 84 |
Cool! Now when you refresh, we have a new link that shows the form.
So let's move to phase two: when we hit "Add", this should submit to a new endpoint that validates the code in Stripe and attaches it to our user's cart.
To create the new endpoint, open OrderController
. Near the bottom add a new public
function addCouponAction()
with @Route("/checkout/coupon")
. Name it order_add_coupon
.
And to be extra-hipster, add @Method("POST")
to guarantee that you can only POST
to this:
... lines 1 - 9 | |
use Symfony\Component\HttpFoundation\Request; | |
class OrderController extends BaseController | |
{ | |
... lines 14 - 79 | |
/** | |
* @Route("/checkout/coupon", name="order_add_coupon") | |
* @Method("POST") | |
*/ | |
public function addCouponAction(Request $request) | |
{ | |
... lines 86 - 97 | |
} | |
... lines 99 - 144 | |
} | |
... lines 146 - 147 |
Cool! Copy the route name, then find the coupon form in the checkout template. Update
the form's action
: add path()
and then paste the route name:
... lines 1 - 19 | |
{% block body %} | |
<div class="nav-space-checkout"> | |
<div class="container"> | |
<div class="row"> | |
... lines 24 - 26 | |
<div class="col-xs-12 col-sm-6"> | |
... lines 28 - 61 | |
<div class="js-code-form" style="display: none;"> | |
<form action="{{ path('order_add_coupon') }}" method="POST" class="form-inline"> | |
... lines 64 - 73 | |
</form> | |
</div> | |
</div> | |
... lines 77 - 79 | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Next, we'll read the submitted code and check with Stripe to make sure it's real, and not just someone trying to guess clever coupon codes. Come on, we've all tried it before.
Hey Blueblazer!
Nice idea, definitely an user will find it helpful
You need two things here, one in your template for sending an AJAX to the server, and a Route in your controller, for handling that request
If you are using Jquery you can use $.ajax() method for seding that request, like this:
$.ajax({
url: "{{ path('order_remove_product', {id: product.id}) }}",
method: "DELETE"
})
.done(function( data ) {
//data variable contains the response from the server
//logic for showing a successful message to the user
});
Yep, you can use twig inside javascript for simplifying some things, and you only have to adjust that path and product id
Controller:
/**
* @Route("/cart/product/{id}", name="order_remove_product")
* @Method("DELETE")
*/
public function removeProductAction(Product $product)
{
$this->get('shopping_cart')
->removeProduct($product);
return new JsonResponse('Product removed!');
}
You can read more about Jquery AJAX here:
http://api.jquery.com/jquery.ajax/
Cheers!
Hey Blueblazer,
Sorry! I forgot about that method :p
Your implementation looks fine to me, but I think this way may be faster
public function removeProduct(Product $product){
$products = $this->getProducts();
if(in_array($product, $products)){
unset($products[$product->getId()]);
$this->updateProducts($products);
}
}
Products are only updated if the item was found, if it wasn't found you might want to throw an exception because it shouldn't happen
Cheers!
but now i get this nasty error: http://imgur.com/a/hU9Kx why does it not recognice the product variable, because it is existing?
Hey Blueblazer172 ,
Please, ensure you're passing "product" variable in all places where you render "order/checkout.html.twig" template - I bet you just missed it somewhere.
Cheers!
yeah my problem is passing the variable to the template, but how can i if the user hasn't selected something.
i tried like this in ordercontroller.php:
/**
* @Route("/checkout", name="order_checkout", schemes={"%secure_channel%"})
* @Security("is_granted('ROLE_USER')")
*/
public function checkoutAction(Request $request, Product $product)
{
if (!$products = $this->get('shopping_cart')->getProducts()) {
$this->addFlash('warning', 'No Products in Cart');
return $this->redirectToRoute('homepage');
}
$error = false;
if ($request->isMethod('POST')) {
$token = $request->request->get('stripeToken');
try {
$this->chargeCustomer($token);
} catch (\Stripe\Error\Card $e) {
$error = 'There was a problem charging your card: '.$e->getMessage();
}
if (!$error) {
$this->em = $this->get('fos_user.user_manager');
$role = $this->getUser()->addRole('ROLE_BUYER_USER');
$this->em->updateUser($role);
$this->get('shopping_cart')->emptyCart();
$this->addFlash('success', 'Yay! Order fully completed!');
return $this->redirectToRoute('homepage');
}
}
return $this->render('order/checkout.html.twig', array(
'products' => $products,
'cart' => $this->get('shopping_cart'),
'stripe_public_key' => $this->getParameter('stripe_public_key'),
'error' => $error,
'product' => $this->get('shopping_cart')->getProductId($product)
));
and the getProductId() funtion in the shoppingcart.php:
public function getProductId(Product $product)
{
return $product->getId();
}
but then i get this error:
Unable to guess how to get a Doctrine instance from the request information for parameter "product".
Hmm, looks like you are mixing some things:
- You shouldn't need to pass the shopping cart to the template, only the products
- Instead of asking for product ID to the shopping cart, you can ask it directly to the product itself
- And if the user hasn't selected anything, then he can't remove anything :)
I hope this makes sense to you :)
Have a nice day!
Hey Blueblazer172
As Victor said, you might have forgotten to pass "product" variable to the template
Have a nice day!
currently my 2 functions in the shoppingcart look like this:
public function addProduct(Product $product)
{
$products = $this->getProducts();
if (!in_array($product, $products)) {
$products[] = $product;
}
$this->updateProducts($products);
}
public function removeProduct(Product $product)
{
$products = $this->getProducts();
$id = $product->getId();
if (($key = array_search($id, $products)) !== false) {
unset($products[$key]);
}
$this->updateProducts($products);
}
// composer.json
{
"require": {
"php": ">=5.5.9, <7.4",
"symfony/symfony": "3.1.*", // v3.1.10
"doctrine/orm": "^2.5", // v2.7.2
"doctrine/doctrine-bundle": "^1.6", // 1.6.8
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.6.2
"symfony/monolog-bundle": "^2.8", // v2.12.1
"symfony/polyfill-apcu": "^1.0", // v1.3.0
"sensio/distribution-bundle": "^5.0", // v5.0.22
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.26
"incenteev/composer-parameter-handler": "^2.0", // v2.1.2
"friendsofsymfony/user-bundle": "~2.0.1", // v2.0.1
"stof/doctrine-extensions-bundle": "^1.2", // v1.2.2
"stripe/stripe-php": "^3.15", // v3.23.0
"doctrine/doctrine-migrations-bundle": "^1.1", // v1.2.1
"phpunit/phpunit": "^5.5", // 5.7.20
"composer/package-versions-deprecated": "^1.11" // 1.11.99
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.1.4
"symfony/phpunit-bridge": "^3.0", // v3.3.0
"hautelook/alice-bundle": "^1.3", // v1.4.1
"doctrine/data-fixtures": "^1.2" // v1.2.2
}
}
Yo :)
It would be really handy if a user could remove a product from the cart :)
my cart looks like this: https://imgur.com/a/H6Y6l
now when someone clicks on the x the product should be removed from the cart and the cart should be updated.
my template looks like this:
https://imgur.com/a/H6Y6l
and i started the javascript like this:
i dont know how to send a request to the server and then remove the product or subscription.
a hint for me?
Thanks for anything :)