Shopping : le paiement avec Stripe
Dans notre boutique on en est arrivés au stade de la confirmation de la commande. En cas de paiement par virement, chèque ou mandat administratif les choses en reste là en attendant le paiement effectif. Il n’en est pas de même dans le cas d’un paiement par carte bancaire parce que celui-ci va s’effectuer normalement immédiatement. J’ai opté pour Stripe comme fournisseur parce qu’il propose une API bien conçue et un package pratique.
Vous pouvez télécharger un ZIP du projet ici.
Stripe
Stripe est une solution de paiement en ligne. C’est une entreprise gigantesque qui a son siège social au États-Unis mais qui possède des bureaux dans la plupart des pays. Faut-il s’en méfier ? Lorsqu’on lit certains forums on pourrait le penser. Je n’en ai personnellement aucune expérience pour le moment et je lis autant d’avis positifs que négatifs. Je vous propose dans cet article de l’utiliser pour notre boutique mais c’est évidemment adaptable avec une autre solution de paiement, mais avec peut-être plus de difficultés pour utiliser l’API.
Vous pouvez ouvrir un compte gratuit pour essayer la solution et ainsi récupérer vos clés API (une secrète et une publique). J’ai trouvé un tutoriel en français pour l’intégration de la solution avec Laravel. Il est globalement plutôt bien fait mais curieusement il crée une redondance du 3D Secure, parce que Stripe s’en charge déjà avec une fenêtre modale et dans son code il relance la page en cas de paiement pas encore abouti. je n’ai pas très bien compris la démarche et j’ai simplifié largement le codage.
La première chose à faire est d’ajouter le package à la boutique :
composer require stripe/stripe-php
La documentation de l’API est très complète. Mais elle est tellement fournie qu’on a parfois du mal à trouver l’information qu’on cherche !
Un paiement passe par plusieurs étapes :
- création d’un PaymentIntent, c’est un objet créé sur le compte Stripe qui mémorise le montant à payer pour un client et les étapes de paiement. On obtient ainsi une clé secrète pour ce paiement. Il est judicieux de mémoriser cette clé en session pour éviter de créer plusieurs PaymentIntent pour un même paiement
- Stripe propose des éléments d’interface pour la collecte des informations de la carte bancaire, il y a création d’un iframe et envoie en mode sécurisé des informations à Stripe
- s’il y a besoin du 3D Secure une page modale est automatiquement ouverte pour collecter les informations complémentaires
- on peut récupérer des événements à l’issue du paiement, en particulier s’il est réussi
On va mémoriser les clés dans la configuration de la boutique. Déjà dans .env :
STRIPE_PUBLISHABLE_KEY=pk_test_*** STRIPE_SECRET_KEY=sk_test_***
Et on crée un fichier de configuration :
Dans lequel on récupère les clés :
<?php return [ 'publishable_key' => env('STRIPE_PUBLISHABLE_KEY'), 'secret_key' => env('STRIPE_SECRET_KEY'), ];
Pourquoi ne pas se contenter du fichier .env ? Si on génère un cache pour la configuration le fichier .env ne sera plus lu !
Contrôleur
Dans le contrôleur OrdersController on va ajouter une méthode pour le paiement :
/** * Stripe * * @param \Illuminate\Http\Request $request * @param \App\Models\Order $order * @return array */ protected function stripe($data, $request, $order) { if($request->session()->has($order->reference)) { $data['secret'] = $request->session()->get($order->reference); } else { \Stripe\Stripe::setApiKey(config('stripe.secret_key')); $intent = \Stripe\PaymentIntent::create([ 'amount' => (integer) ($order->totalOrder * 100), 'currency' => 'EUR', 'metadata' => [ 'reference' => $order->reference, ], ]); $request->session()->put($order->reference, $intent->client_secret); $data['secret'] = $intent->client_secret; }; return $data; }
Et l’appel de cette méthode dans la méthode data :
protected function data($request, $order) { ... if($order->state->slug === 'carte' || $order->state->slug === 'erreur') { $data = $this->stripe($data, $request, $order); } return $data; }
On commence par mémoriser la clé secrète :
\Stripe\Stripe::setApiKey(config('stripe.secret_key'));
Ensuite on crée l’objet PaymentIntent :
$intent = \Stripe\PaymentIntent::create([ 'amount' => (integer) ($order->totalOrder * 100), 'currency' => 'EUR', 'metadata' => [ 'reference' => $order->reference, ], ]);
On a la somme en centimes, la monnaie et on ajoute comme information la référence de la commande, ce qui facilitera la lecture des données dans l’interface de Stripe.
On mémorise ensuite la clé secrète du ce paiement (on utilise la référence de la commande comme clé) en session :
$request->session()->put($order->reference, $intent->client_secret);
Évidemment si la clé a déjà été générée et mémorisée en session (et donc on dispose déjà d’un objet PaymentIntent) on se contente de récupérer cette clé :
if($request->session()->has($order->reference)) { $data['secret'] = $request->session()->get($order->reference);
Les vues
Vue partielle du formulaire de paiement
On crée une vue partielle pour afficher le formulaire de paiement parce qu’on réutilisera cette vue si jamais le client n’a pas pu payer et veut le faire plus tard à partir de son compte :
Avec ce code :
<br> <div id="payment-pending" class="card"> <div class="card-content center-align"> <span class="card-title">Vous avez choisi de payer par carte bancaire. Veuillez compléter le présent formulaire pour procéder à ce règlement</span> <p class="orange-text">Nous ne conservons aucune de ces informations sur notre site, elles sont directement transmises à notre prestataire de paiement <a href="https://stripe.com/fr">Stripe</a>.</p> <p class="orange-text">La transmission de ces informations est entièrement sécurisée.</p> <br> <div class="row"> <form id="payment-form"> <div class="card blue-grey darken-1"> <div class="card-content white-text"> <div id="card-element"></div> </div> </div> <div id="card-errors" class="helper-text red-text"></div> <br> <div id="wait" class="hide"> <div class="loader"></div> <br> <p><span class="red-text card-title center-align">Processus de paiement en cours, ne fermez pas cette fenêtre avant la fin du traitement !</span></p> </div> <button style="float: right;" class="btn waves-effect waves-light" id='submit' type="submit" name="action">Payer <i class="material-icons right">payment</i> </button> </form> </div> </div> </div>
La partie qui concerne la génération des éléments du formulaire est ici :
<form id="payment-form"> ... <div id="card-element"></div> ... <div id="card-errors" class="helper-text red-text"></div> ... <button id='submit' type="submit" name="action">Payer</button> </form>
Vue partielle pour le Javascript
On va aussi créer une vue partielle pour l’intégration du Javascript :
@if($order->state->slug === 'carte' || $order->state->slug === 'erreur') <script src="https://js.stripe.com/v3/"></script> <script> const stripe = Stripe('{{ config('stripe.publishable_key') }}'); const elements = stripe.elements(); const style = { base: { color: '#32325d', fontFamily: '"Helvetica Neue", Helvetica, sans-serif', fontSmoothing: 'antialiased', fontSize: '16px', '::placeholder': { color: '#aab7c4' } }, invalid: { color: '#fa755a', iconColor: '#fa755a' } }; const card = elements.create("card", { style: style }); card.mount("#card-element"); const displayError = document.getElementById('card-errors'); card.addEventListener('change', ({error}) => { displayError.textContent = error ? error.message : ''; }); document.getElementById('payment-form').addEventListener('submit', ev => { ev.preventDefault(); displayError.textContent = ''; document.getElementById('submit').classList.add('hide'); document.getElementById('wait').classList.remove('hide'); stripe.confirmCardPayment('{{ $secret }}', { payment_method: { card: card } }).then(result => { document.getElementById('wait').classList.add('hide'); if (result.error) { document.getElementById('submit').classList.remove('hide'); displayError.textContent = result.error.message; } else { if (result.paymentIntent.status === 'succeeded') { document.getElementById('payment-pending').classList.add('hide'); document.getElementById('payment-ok').classList.remove('hide'); } } let info = result.error ? 'error' : result.paymentIntent.id; fetch('{{ route('commandes.payment', $order->id) }}', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }, body: JSON.stringify({ payment_intent_id: info }) }); }); }); </script> @endif
La partie intéressant dans ce code est la création de l’élément Card :
const card = elements.create("card", { style: style });
On aura ainsi un input global qui va collecter toutes les informations.
On prévoit le code pour l’affichage des erreurs probables :
card.addEventListener('change', ({error}) => { const displayError = document.getElementById('card-errors'); displayError.textContent = error ? error.message : ''; });
A la soumission on envoie les informations à Stripe :
stripe.confirmCardPayment('{{ $secret }}', { payment_method: { card: card } }).then(result => {
Là on agit en fonction du succès ou de l’échec.
Pour le moment je n’ai pas précisé la route pour l’envoi des informations après le paiement parce qu’on ne l’a pas encore créée.
La vue de confirmation
On va compléter maintenant la vue de confirmation (views/command/confirmation.blade.php) qu’on a créée lors du précédent article.
On va ajouter un peu de style :
@extends('layouts.app') @section('css') <style> .StripeElement { box-sizing: border-box; height: 40px; padding: 10px 12px; border: 1px solid transparent; border-radius: 4px; background-color: white; box-shadow: 0 1px 3px 0 #e6ebf1; -webkit-transition: box-shadow 150ms ease; transition: box-shadow 150ms ease; } .StripeElement--focus { box-shadow: 0 1px 3px 0 #cfd7df; } .StripeElement--invalid { border-color: #fa755a; } .StripeElement--webkit-autofill { background-color: #fefde5 !important; } </style> @endsection
Et intégrer les vues partielles :
@elseif($order->state->slug === 'carte' || $order->state->slug === 'erreur') @include('command.partials.stripe') @endif ... @endsection @section('javascript') @include('command.partials.stripejs') @endsection
Maintenant quand on affiche la page de confirmation en cas de paiement par carte on a le formulaire qui apparaît :
On peut vérifier les messages d’erreur :
On peut aussi tester le 3D Secure :
Vous pouvez trouver une liste de numéros de cartes de test pour les différentes situations ici.
Sur le tableau de bors de Stripe on retrouve la tentative (si ça n’a pas abouti) de paiement :
On dispose de tout le détail, par exemple la référence de la commande qu’on a envoyée :
Et de tout plein d’autres informations que je vous laisse découvrir…
Mise à jour du paiement
Maintenant qu’on peut payer par carte il faut pouvoir récupérer l’information de réussite ou d’achec de ce paiment dans la boutique.
On va créer un nouveau contrôleur :
php artisan make:controller PaymentController
On aura une seule méthode :
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Models\{ Order, State }; class PaymentController extends Controller { /** * Manage payment * * @param \App\Models\Order $order * @return void */ public function __invoke(Request $request, Order $order) { $this->authorize('manage', $order); $state = null; if($request->payment_intent_id === 'error') { $state = 'erreur'; } else { \Stripe\Stripe::setApiKey(config('stripe.secret_key')); $intent = \Stripe\PaymentIntent::retrieve($request->payment_intent_id); if ($intent->status === 'succeeded') { $request->session()->forget($order->reference); $order->payment_infos()->create(['payment_id' => $intent->id]); $state = 'paiement_ok'; // Création de la facture à prévoir } else { $state = 'erreur'; } } $order->state_id = State::whereSlug($state)->first()->id; $order->save(); } }
Ici on va mettre à jour l’état de la commande. S’il y a une erreur de paiement c’est tout simple :
if($request->payment_intent_id === 'error') { $state = 'erreur';
Sinon on retrouve les informations de ce paiement chez Stripe (on ne va pas faire confiance à ce qui vient du client) :
\Stripe\Stripe::setApiKey(config('stripe.secret_key')); $intent = \Stripe\PaymentIntent::retrieve($request->payment_intent_id);
Et là si le paiement a abouti :
if ($intent->status === 'succeeded') { $request->session()->forget($order->reference); $order->payment_infos()->create(['payment_id' => $intent->id]); $state = 'paiement_ok';
On vide la session, on renseigne la table payments avec l’identifiant du paiement, et on change l’état.
On prévoit la route pour aboutir à cette méthode :
// Utilisateur authentifié Route::middleware('auth')->group(function () { // Commandes Route::prefix('commandes')->group(function () { ... Route::name('commandes.payment')->post('paiement/{order}', 'PaymentController'); });
Et on ajoute cette route dans le Javascript de la vue partielle stripejs :
fetch('{{ route('commandes.payment', $order->id) }}', {
Lorsque le paiement a réussi on affiche une information claire au client :
On vérifie que l’état a bien changé dans la table orders :
Et que la table payments a bien été renseignée :
On peut vérifier la réussite dans le tableau de bord de Stripe :
Conclusion
On a réussi notre intégration d’une solution de paiment dans notre boutique ! Maintenant les clients peuvent payer immédiatement leurs achats. Mais nous avons encore beaucoup de travail en vue ! Rendez-vous au prochain article !
12 commentaires
jondelweb
Salut BestMomo,
Juste pour savoir, ce serait pas mieux d’intégrer Stripe avec leur solution checkout?
https://stripe.com/docs/payments/checkout
Je l’ai fait sur une autre appli avec laravel en back et Angular en front et franchement, c’est plutôt bien fait comme truc…
bestmomo
Salut,
J’avais regardé cette solution également mais je ne l’avais pas retenue mais je ne me rappelle plus de la raison… De toute façon j’ai réalisé cette partie paiement avec l’utilisation la plus classique de Stripe pour l’exemple. Mais franchement je ne conseille pas vraiment d’utiliser cette solution de paiement et de plutôt s’orienter vers un prestataire français comme Paygreen. Évidemment ça oblige à plus de travail au niveau du codage parce qu’il faut bien étudier la documentation de leurs API, et ce n’est pas toujours assez complet.
Mody
Merci davantage! je resterai connecter pour la suite
Mody
merci davantage et désolé pour mes questions.
Je continuer tout au long
Mody
Bonjour !
après validation de la commande par carte j’ai eu cette erreur :Aucune clé API fournie. (CONSEIL: définissez votre clé API à l’aide de « Stripe :: setApiKey () » or j’ai crée un compte sur stripe.com puis copié les clés de publishable et secret sur le fichier .env
bestmomo
Salut,
Est-ce que tu as bien créé et codé le fichier de configuration config.stripe ?
Mody
oui mais dans les paramettre j’ai pas remis le publishable_key et le secret
bestmomo
C’est sûr que sans clé ça ne peut pas marcher.
Mody
merci davantage !!
mon compte stripe n’est pas activer car il n’est pas disponible ici au Sénégal. De ce fait est-il possible d’avoir un numéro de carte pour le test car la zone de texte du carte ne s’affiche pas chez moi
bestmomo
Si Stripe n’est pas disponible alors ça ne pourra pas fonctionner. Il faut suivre le tutoriel en oubliant la partie paiement par carte.
golli
bonsoir bestmomo
il ya un probleme de securité au niveau actualisation de la page de paiement stripe qui refait un payment de transport il quand on retour a la page d’avant « confirmation commande » on as seulement le prix du transport sans le prox du produit
il ne faut pas autorisé l’actualisation de la page de paiement stripe et le retour en arriéré
veuillez svp nous communiquez comment faire !!??
mercii
bestmomo
Salut,
Effectivement un retour sur la page et une actualisation n’affiche que le port puisque le panier a été vidé à la confirmation. Une façon de faire est d’ajouter ce code au début de la méthode create de OrderController :
if(Cart::isEmpty()) abort(404);
A voir s’il y a une meilleure option…