Paiement par carte avec tipsi-stripe depuis une application React Native

Contexte

En ce moment, une plateforme de paiement est assez en vogue. Il s'agit de Stripe et j'ai eu l'occasion de l'utiliser pour implémenter la partie paiement par carte sur une application React Native. C'est le sujet de cet article et nous pouvons voir sans plus attendre les différentes parties qui nous intéressent sur le schéma suivant.

L'application est en React Native et le back a été créé avec JHipster. Les seules parties que vous pourriez ne pas connaître sont celles concernant Stripe, un gestionnaire de paiement par carte bancaire. Tipsi-stripe est une bibliothèque utilisable depuis une application React Native. Elle permet la récupération et le transfert de données sensibles, telles que les coordonnées bancaires de l'utilisateur, directement depuis l'application vers Stripe. Elle répondait parfaitement à l'un de nos critères qui était d'éviter la gestion des coordonnées bancaires côté Back.

Nous avions également deux autres critères principaux :

  • La transaction doit être validée côté back pour pouvoir enregistrer les informations de cette dernière une fois celle-ci terminée.
  • Un paiement nécessitant la validation 3-D Secure doit être également possible.

Ces prérequis ont été respectés grâce au workflow que l'on a mis en place et qui est présenté avec les deux diagrammes suivants.

Diagrammes de séquence

Dans un premier temps, prenons le cas simple d'un paiement ne nécessitant pas la validation 3-D Secure. Grâce à tipsi-stripe, l'utilisateur renseigne ses coordonnées bancaires et Stripe retourne en échange un "paymentMethodId" qui permet au Back de compléter les informations de paiement avec notamment le montant et la devise pour ensuite avoir, de Stripe, la confirmation du paiement. Enfin, il peut enregistrer les changements en base de données, d'une part l'incrémentation du nombre de tickets de l'utilisateur mais surtout l'enregistrement de la transaction effectuée. Les tickets sont le motif de l'achat, ce sont des crédits virtuels utilisés au sein de l'application.

Le back ne communique pas directement  avec Stripe, toute la transaction se passe autour d'un "PaymentIntent", qui possède un statut définissant l'étape du paiement. C'est ce statut qui permet au back de savoir si le paiement nécessite une validation 3-D Secure avec la valeur "requires_action".

Quand cette validation doit être effectuée, le back renvoie un "clientSecret" à l'application qui, avec cet id et via tipsi-stripe, propose à l'utilisateur de fournir le code permettant de valider le paiement. Le "paiementIntentId" issu de cette communication est ensuite envoyé au Back pour récupérer le "paymentIntent" et obtenir, si tout s'est bien passé, une confirmation du paiement grâce au statut "succeeded".

Utilisation de tipsi-stripe

Tous les échanges entre l'application et Stripe sont effectués grâce à tipsi-stripe et sont extrêmement simples. Voilà par exemple la partie du code permettant à l'utilisateur de renseigner ses coordonnées bancaires.

stripe.paymentRequestWithCardForm({})
      .then((token: any) => {
	      // call backend with token.id which is the paymentMethodId
      })
      .catch(handleError);

De même, voici les trois lignes de code permettant à l'utilisateur de renseigner son code de validation 3-D Secure :

stripe.authenticatePaymentIntent({
    	clientSecret: clientSecret,
		returnUrl: 'my.custom.scheme://callback',
	  })
	  .then((paymentIntent: any) => {
		// call backend with paymentIntent.paymentIntentId
	  })
	  .catch(handleAuthenticationError);

Tester son application sans payer

En ce qui concerne les tests unitaires, les deux méthodes vues ci-dessus peuvent être facilement mockées, par exemple avec Jest :

paymentRequestWithCardFormSpy = jest
	.spyOn(stripe, 'paymentRequestWithCardForm')
	.mockReturnValue(Promise.resolve({ id: 'tokenId' }));

authenticatePaymentIntentSpy = jest
	.spyOn(stripe, 'authenticatePaymentIntent')
	.mockReturnValue(Promise.resolve({ paymentIntentId: 'paymentIntentId' }));

Mais il peut être pratique et souhaitable de tester le workflow en entier sans être obligé de dépenser de l'argent avec sa carte bleue. Rassurez-vous, Stripe propose des numéros de cartes et des ids de tests permettant de tester non seulement le workflow complet mais également les statuts possibles à chaque étape ainsi que différentes erreurs. Vous pouvez trouver leur description détaillée sur cette page mais attention, pour utiliser ces données, vous devez utiliser une API key de test. C'est aussi un point positif, l'API key est le seul point à changer pour revenir au code de production.

La gestion des erreurs

Ne laissons pas ce sujet qui nous tient à cœur tomber aux oubliettes ; d'autant plus que Stripe s'occupe presque de tout.

Côté Front, les erreurs sont présentées de manière générique à l'utilisateur mais c'est un choix que nous avons pris. Il est possible d'avoir des traces complètes et/ou de donner plus d'information à l'utilisateur car celles issues de l'utilisation de tipsi-stripe présentent des codes d'erreur assez explicites que vous pouvez trouver ici.

De même, côté Back, des appels à l'API de Stripe se cachent derrière l'utilisation du "PaymentIntent", ainsi, en cas d'imprévu dans le flow de paiement, une exception Stripe est lancée avec un code d'erreur également très précis.

Conclusion

La partie la plus difficile a été de trouver le bon workflow, de découvrir les différentes étapes avec les ids à communiquer du Front au Back et vice versa. Pour ceux qui se poseraient la question ou qui ont été voir la description des statuts du "PaymentIntent", un workflow est proposé mais certaines étapes du paiement sont interchangeables et c'est ce que nous avons fait en commençant par la récupération des coordonnées bancaires de l'utilisateur. Cela permettait d'avoir un aller retour Front/Back en moins, de même que la confirmation automatique du paiement. Hormis ces quelques complications, tipsi-stripe et de manière plus large, Stripe, est assez simple d'utilisation. Le setup enfantin et les erreurs détaillées sont deux avantages non négligeables.