Avec l’augmentation des transactions numériques, la capacité à intégrer de façon transparente les passerelles de paiement est devenue une compétence essentielle pour les développeurs. Que ce soit pour les places de marché ou les produits SaaS, un processeur de paiement est crucial pour collecter et traiter les paiements des utilisateurs.

Cet article explique comment intégrer Stripe dans un environnement Spring Boot, comment mettre en place des abonnements, proposer des essais gratuits et construire des pages en libre-service pour que tes clients puissent télécharger leurs factures de paiement.

Qu’est-ce que Stripe ?

Stripe est une plateforme de traitement des paiements de renommée mondiale, disponible dans 46 pays. C’est un excellent choix si vous voulez intégrer un système de paiement dans votre application web, en raison de sa grande portée, de son nom réputé et de sa documentation détaillée.

Comprendre les concepts communs de Stripe

Il est utile de comprendre certains concepts communs que Stripe utilise pour coordonner et effectuer des opérations de paiement entre plusieurs parties. Stripe propose deux approches pour mettre en œuvre l’intégration des paiements dans votre application.

Vous pouvez soit intégrer les formulaires de Stripe dans votre application pour une expérience client in-app (intention de paiement), soit rediriger les clients vers une page de paiement hébergée par Stripe, où Stripe gère le processus et informe votre application lorsqu’un paiement a réussi ou échoué (lien de paiement).

Intention de paiement

Lorsque vous gérez des paiements, il est important de recueillir les détails concernant le client et le produit avant de leur demander les informations relatives à la carte et au paiement. Ces détails comprennent la description, le montant total, le moyen de paiement, etc.

Stripe vous demande de collecter ces détails dans votre application et de générer un objet PaymentIntent dans son backend. Cette approche permet à Stripe de formuler une demande de paiement pour cette intention. Une fois le paiement terminé, vous pouvez systématiquement récupérer les détails du paiement, y compris son objet, par le biais de l’objet PaymentIntent.

Liens de paiement

Pour éviter la complexité de l’intégration de Stripe directement dans votre base de code, envisagez d’utiliser Stripe Checkouts pour une solution de paiement hébergée. Comme pour la création d’un PaymentIntent, vous créerez un CheckoutSession avec les détails du paiement et du client. Au lieu d’initier un PaymentIntent in-app, le CheckoutSession génère un lien de paiement vers lequel vous redirigez vos clients. Voici à quoi ressemble une page de paiement hébergée :

La page de paiement hébergée par Stripe.
La page de paiement hébergée par Stripe.

Après le paiement, Stripe redirige vers votre application, ce qui permet d’effectuer des tâches post-paiement telles que les confirmations et les demandes de livraison. Pour plus de fiabilité, configurez un crochet web de backend pour mettre à jour Stripe, assurant ainsi la conservation des données de paiement même si les clients ferment accidentellement la page après le paiement.

Bien qu’efficace, cette méthode manque de flexibilité en matière de personnalisation et de conception. Elle peut également être délicate à configurer correctement pour les applications mobiles, où une intégration native aurait l’air bien plus transparente.

Clés API

Lorsque vous travaillez avec l’API Stripe, vous devez avoir accès aux clés API pour que vos applications client et serveur puissent interagir avec le backend Stripe. Vous pouvez accéder à vos clés API Stripe sur votre tableau de bord de développeur Stripe. Voici à quoi il ressemblerait :

Le tableau de bord Stripe affichant les clés API
Le tableau de bord Stripe affichant les clés API

Comment fonctionnent les paiements dans Stripe ?

Pour comprendre comment fonctionnent les paiements dans Stripe, vous devez comprendre toutes les parties prenantes impliquées. Quatre parties prenantes sont impliquées dans chaque transaction de paiement :

  1. Client : La personne qui a l’intention de payer pour un service/produit.
  2. Marchand : Vous, le ou la propriétaire de l’entreprise, vous êtes responsable de la réception des paiements et de la vente des services/produits.
  3. Acquéreur : Une banque qui traite les paiements en votre nom (le commerçant) et achemine votre demande de paiement aux banques de vos clients. Les acquéreurs peuvent s’associer à un tiers pour aider à traiter les paiements.
  4. Banque émettrice : La banque qui accorde des crédits et émet des cartes et d’autres moyens de paiement aux consommateurs.

Voici un flux de paiement typique entre ces parties prenantes à un niveau très élevé.

Comment fonctionnent les paiements en ligne
Comment fonctionnent les paiements en ligne

Le client fait savoir au commerçant qu’il est prêt à payer. Le commerçant transmet ensuite les informations relatives au paiement à sa banque acquéreur, qui collecte le paiement auprès de la banque émettrice du client et informe le commerçant que le paiement a bien été effectué.

Il s’agit là d’un aperçu de très haut niveau du processus de paiement. En tant que commerçant, vous n’avez  qu’à vous préoccuper de la collecte de l’intention de paiement, de sa transmission au processeur de paiement et du traitement du résultat du paiement. Cependant, comme nous l’avons vu plus haut, il y a deux façons de procéder.

Lors de la création d’une session de paiement gérée par Stripe, où Stripe s’occupe de la collecte des détails du paiement, voici à quoi ressemble le flux typique :

Le flux de paiement de la session de paiement hébergée par Stripe.
Le flux de paiement de la session de paiement hébergée par Stripe. (Source : Stripe Docs)

Avec les flux de paiement personnalisés, tout dépend vraiment de vous. Vous pouvez concevoir l’interaction entre votre client, le serveur, le client et l’API Stripe en fonction des besoins de votre application. Vous pouvez ajouter à ce flux de travail la collecte d’adresses, la génération de factures, l’annulation, les essais gratuits, etc. Selon vos besoins.

Maintenant que vous comprennez le fonctionnement des paiements Stripe, vous êtes prêt à commencer à le construire dans votre application Java.

Intégration de Stripe dans une application Spring Boot

Pour commencer l’intégration de Stripe, crée une application frontend pour interagir avec le backend Java et initier les paiements. Dans ce tutoriel, vous construirez une appli React pour déclencher différents types de paiements et d’abonnements afin de bien comprendre leurs mécanismes.

Remarque : ce tutoriel ne couvrira pas la construction d’un site de commerce électronique complet ; il vise principalement à vous guider dans le processus simple d’intégration de Stripe dans Spring Boot.

Configuration des projets frontend et backend

Créez un nouveau répertoire et échafaudez un projet React à l’aide de Vite en exécutant la commande suivante :

npm create vite@latest

Définissez le nom du projet comme frontend (ou tout autre nom préféré), le framework comme React et la variante comme TypeScript. Naviguez vers le répertoire du projet et installez Chakra UI pour un échafaudage rapide des éléments de l’interface utilisateur en exécutant la commande suivante :

npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion @chakra-ui/icons

Vous installerez également react-router-dom dans votre projet pour le routage côté client en exécutant la commande ci-dessous :

npm i react-router-dom

Maintenant, vous êtes prêt à construire votre application frontend. Voici la page d’accueil que vous allez construire.

La page d'accueil terminée de l'application frontend.
La page d’accueil terminée de l’application frontend.

En cliquant sur n’importe quel bouton de cette page, vous accèderez à des pages de paiement séparées avec des formulaires de paiement. Pour commencer, créez un nouveau dossier nommé routes dans votre répertoire frontend/src. Dans ce dossier, créez un fichier Home.tsx. Ce fichier contiendra le code de la route d’accueil de votre application (/). Collez le code suivant dans le fichier :

import {Button, Center, Heading, VStack} from "@chakra-ui/react";

import { useNavigate } from "react-router-dom";

function Home() {
    const navigate = useNavigate()
    const navigateToIntegratedCheckout = () => {
        navigate("/integrated-checkout")
    }

    const navigateToHostedCheckout = () => {
        navigate("/hosted-checkout")
    }

    const navigateToNewSubscription = () => {
        navigate("/new-subscription")
    }

    const navigateToCancelSubscription = () => {
        navigate("/cancel-subscription")
    }

    const navigateToSubscriptionWithTrial = () => {
        navigate("/subscription-with-trial")
    }

    const navigateToViewInvoices = () => {
        navigate("/view-invoices")
    }

    return (
        <>
            <Center h={'100vh'} color='black'>
                <VStack spacing='24px'>
                    <Heading>Stripe Payments With React & Java</Heading>
                    <Button
                        colorScheme={'teal'}
                        onClick={navigateToIntegratedCheckout}>
                        Integrated Checkout
                    </Button>
                    <Button
                        colorScheme={'blue'}
                        onClick={navigateToHostedCheckout}>
                        Hosted Checkout
                    </Button>
                    <Button
                        colorScheme={'yellow'}
                        onClick={navigateToNewSubscription}>
                        New Subscription
                    </Button>
                    <Button
                        colorScheme={'purple'}
                        onClick={navigateToCancelSubscription}>
                        Cancel Subscription
                    </Button>
                    <Button
                        colorScheme={'facebook'}
                        onClick={navigateToSubscriptionWithTrial}>
                        Subscription With Trial
                    </Button>
                    <Button
                        colorScheme={'pink'}
                        onClick={navigateToViewInvoices}>
                        View Invoices
                    </Button>
                </VStack>
            </Center>
        </>
    )
}

export default Home

Pour permettre la navigation dans votre application, mettez à jour votre fichier App.tsx pour configurer la classe RouteProvider à partir de react-router-dom.

import Home from "./routes/Home.tsx";
import {
    createBrowserRouter,
    RouterProvider,
} from "react-router-dom";

function App() {

    const router = createBrowserRouter([
        {
            path: "/",
            element: (
                <Home/>
            ),
        },
    ]);

  return (
    <RouterProvider router={router}/>
  )
}

export default App

Exécutez la commande npm run dev pour prévisualiser votre application sur https://localhost:5173.

Ceci termine la configuration initiale nécessaire pour l’appli frontend. Ensuite, créez une application backend à l’aide de Spring Boot. Pour initialiser l’application, vous pouvez utiliser le site web spring initializr (si votre IDE prend en charge la création d’applications Spring, vous n’avez pas besoin d’utiliser le site web).

IntelliJ IDEA prend en charge la création d’applications Spring Boot. Commencez par choisir l’option New project sur IntelliJ IDEA. Ensuite, choisissez Spring Initializr dans le panneau de gauche. Saisissez les détails de votre projet backend : nom (backend), emplacement (stripe-payments-java), langage (Java) et type (Maven). Pour les noms de groupe et d’artefact, utilisez respectivement com.kinsta.stripe-java et backend.

La boîte de dialogue du nouveau projet IDEA.
La boîte de dialogue du nouveau projet IDEA.

Cliquez sur le bouton Next. Ajoutez ensuite des dépendances à votre projet en choisissant Spring Web dans le menu déroulant Web du volet des dépendances et cliquez sur le bouton Create.

Choix des dépendances.
Choix des dépendances.

Cela va créer le projet Java et l’ouvrir dans votre IDE. Vous pouvez maintenant procéder à la création des différents flux de paiement à l’aide de Stripe.

Accepter les paiements en ligne pour les achats de produits

La fonctionnalité la plus importante et la plus utilisée de Stripe est l’acceptation de paiements uniques de la part des clients. Dans cette section, vous apprendrez deux façons d’intégrer le traitement des paiements dans votre application avec Stripe.

Commande hébergée

Tout d’abord, Vous construisez une page de commande qui déclenche un flux de travail de paiement hébergé où vous ne déclenchez un paiement qu’à partir de votre application frontend. Stripe se charge ensuite de recueillir les détails de la carte du client et de collecter le paiement, et ne partage le résultat de l’opération de paiement qu’à la fin.

Voici à quoi ressemblerait la page de paiement :

La page de commande hébergée complétée.
La page de commande hébergée complétée.

Cette page comporte trois éléments principaux : CartItem – représente chaque article du panier ; TotalFooter – affiche le montant total ; CustomerDetails – recueille les détails du client. Vous pouvez réutiliser ces composants pour créer des formulaires de commande pour d’autres scénarios de cet article, comme le paiement intégré et les abonnements.

Construire le frontend

Créez un dossier components dans votre répertoire frontend/src. Dans le dossier components, créez un nouveau fichier CartItem.tsx et collez le code suivant :

import {Button, Card, CardBody, CardFooter, Heading, Image, Stack, Text, VStack} from "@chakra-ui/react";

function CartItem(props: CartItemProps) {
    return <Card direction={{base: 'column', sm: 'row'}}
                 overflow='hidden'
                 width={'xl'}
                 variant='outline'>
        <Image
            objectFit='cover'
            maxW={{base: '100%', sm: '200px'}}
            src={props.data.image}
        />
        <Stack mt='6' spacing='3'>
            <CardBody>
                <VStack spacing={'3'} alignItems={"flex-start"}>
                    <Heading size='md'>{props.data.name}</Heading>
                    <VStack spacing={'1'} alignItems={"flex-start"}>
                        <Text>
                            {props.data.description}
                        </Text>
                        {(props.mode === "checkout" ? <Text>
                            {"Quantity: " + props.data.quantity}
                        </Text> : <></>)}
                    </VStack>
                </VStack>
            </CardBody>

            <CardFooter>
                <VStack alignItems={'flex-start'}>
                    <Text color='blue.600' fontSize='2xl'>
                        {"$" + props.data.price}
                    </Text>
                </VStack>
            </CardFooter>
        </Stack>
    </Card>
}

export interface ItemData {
    name: string
    price: number
    quantity: number
    image: string
    description: string
    id: string
}

interface CartItemProps {
    data: ItemData
    mode: "subscription" | "checkout"
    onCancelled?: () => void
}

export default CartItem

Le code ci-dessus définit deux interfaces à utiliser comme types pour les propriétés transmises au composant. Le type ItemData est exporté pour être réutilisé dans d’autres composants.

Le code renvoie la disposition d’un composant d’article de panier. Il utilise les propriétés fournies pour afficher l’article à l’écran.

Ensuite, créez un fichier TotalFooter.tsx dans le répertoire des compoents et collez le code suivant :

import {Divider, HStack, Text} from "@chakra-ui/react";

function TotalFooter(props: TotalFooterProps) {
    return <>
        <Divider />
        <HStack>
            <Text>Total</Text>
            <Text color='blue.600' fontSize='2xl'>
                {"$" + props.total}
            </Text>
        </HStack>
        {props.mode === "subscription" &&
            <Text fontSize={"xs"}>(Monthly, starting today)</Text>
        }
        {props.mode === "trial" &&
            <Text fontSize={"xs"}>(Monthly, starting next month)</Text>
        }
    </>
}

interface TotalFooterProps {
    total: number
    mode: "checkout" | "subscription" | "trial"
}

export default TotalFooter

Le composant TotalFooter affiche la valeur totale du panier et utilise la valeur mode pour rendre un texte spécifique de manière conditionnelle.

Enfin, créez le composant CustomerDetails.tsx et collez le code suivant :

import {ItemData} from "./CartItem.tsx";
import {Button, Input, VStack} from "@chakra-ui/react";
import {useState} from "react";

function CustomerDetails(props: CustomerDetailsProp) {
    const [name, setName] = useState("")
    const [email, setEmail] = useState("")
    const onCustomerNameChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
        setName(ev.target.value)
    }



    const onCustomerEmailChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
        setEmail(ev.target.value)
    }

    const initiatePayment = () => {
        fetch(process.env.VITE_SERVER_BASE_URL + props.endpoint, {
            method: "POST",
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                items: props.data.map(elem => ({name: elem.name, id: elem.id})),
                customerName: name,
                customerEmail: email,
            })
        })
            .then(r => r.text())
            .then(r => {
                window.location.href = r
            })

    }

    return <>
        <VStack spacing={3} width={'xl'}>
            <Input variant='filled' placeholder='Customer Name' onChange={onCustomerNameChange} value={name}/>
            <Input variant='filled' placeholder='Customer Email' onChange={onCustomerEmailChange} value={email}/>
            <Button onClick={initiatePayment} colorScheme={'green'}>Checkout</Button>
        </VStack>
    </>
}

interface CustomerDetailsProp {
    data: ItemData[]
    endpoint: string
}

export default CustomerDetails

Le code ci-dessus affiche un formulaire avec deux champs de saisie – pour recueillir le nom et l’e-mail de l’utilisateur. Lorsque l’utilisateur clique sur le bouton Commander, la méthode initiatePayment est invoquée pour envoyer la demande de paiement au backend.

Elle demande le point de terminaison que vous avez passé au composant et envoie les informations du client et les articles du panier dans le cadre de la demande, puis redirige l’utilisateur vers l’URL reçue du serveur. Cette URL conduira l’utilisateur à une page de commande hébergée sur le serveur de Stripe. Vous apprendrez à construire cette URL dans quelques instants.

Note : Ce composant utilise la variable d’environnement VITE_SERVER_BASE_URL pour l’URL du serveur backend. Définissez-la en créant un fichier .env à la racine de votre projet :

VITE_SERVER_BASE_URL=http://localhost:8080

Tous les composants ont été créés. Passons maintenant à la construction de l’itinéraire de commande hébergé à l’aide des composants. Pour cela, créez un nouveau fichier HostedCheckout.tsx dans votre dossier routes avec le code suivant :

import {Center, Heading, VStack} from "@chakra-ui/react";
import {useState} from "react";
import CartItem, {ItemData} from "../components/CartItem.tsx";
import TotalFooter from "../components/TotalFooter.tsx";
import CustomerDetails from "../components/CustomerDetails.tsx";
import {Products} from '../data.ts'

function HostedCheckout() {
    const [items] = useState<ItemData[]>(Products)
    return <>
        <Center h={'100vh'} color='black'>
            <VStack spacing='24px'>
                <Heading>Hosted Checkout Example</Heading>
                {items.map(elem => {
                    return <CartItem data={elem} mode={'checkout'}/>
                })}
                <TotalFooter total={30} mode={"checkout"}/>
                <CustomerDetails data={items} endpoint={"/checkout/hosted"} mode={"checkout"}/>
            </VStack>
        </Center>
    </>
}

export default HostedCheckout

Cette route utilise les trois composants que vous venez de construire pour assembler un écran de commande. Tous les modes des composants sont configurés comme checkout, et le point de terminaison /checkout/hosted est fourni au composant de formulaire pour initier la demande de commande avec précision.

Le composant utilise un objet Products pour remplir le tableau des articles. Dans le monde réel, ces données proviennent de l’API du panier et contiennent les articles sélectionnés par l’utilisateur. Cependant, pour ce tutoriel, c’est une liste statique provenant d’un script qui remplit le tableau. Définissez le tableau en créant un fichier data.ts à la racine de votre projet frontend et en y stockant le code suivant :

import {ItemData} from "./components/CartItem.tsx";

export const Products: ItemData[] = [
    {
        description: "Premium Shoes",
        image: "https://source.unsplash.com/NUoPWImmjCU",
        name: "Puma Shoes",
        price: 20,
        quantity: 1,
        id: "shoe"
    },
    {
        description: "Comfortable everyday slippers",
        image: "https://source.unsplash.com/K_gIPI791Jo",
        name: "Nike Sliders",
        price: 10,
        quantity: 1,
        id: "slippers"
    },
]

Ce fichier définit deux éléments dans le tableau des produits qui sont affichés dans le panier. N’hésitez pas à modifier les valeurs des produits.

La dernière étape de la construction du frontend consiste à créer deux nouvelles routes pour gérer les succès et les échecs. La page de commande hébergée par Stripe redirigera les utilisateurs dans votre application sur ces deux routes en fonction du résultat de la transaction. Stripe fournira également à vos routes des données utiles liées à la transaction, telles que l’identifiant de la session de commande, que vous pourrez utiliser pour récupérer l’objet de session de paiement correspondant et accéder aux données liées au paiement, telles que le moyen de paiement, les détails de la facture, etc.

Pour cela, créez un fichier Success.tsx dans le répertoire src/routes et enregistrez-y le code suivant :

import {Button, Center, Heading, Text, VStack} from "@chakra-ui/react";
import {useNavigate} from "react-router-dom";

function Success() {
    const queryParams = new URLSearchParams(window.location.search)
    const navigate = useNavigate()
    const onButtonClick = () => {
        navigate("/")
    }
    return <Center h={'100vh'} color='green'>
        <VStack spacing={3}>
            <Heading fontSize={'4xl'}>Success!</Heading>
            <Text color={'black'}>{queryParams.toString().split("&").join("n")}</Text>
            <Button onClick={onButtonClick} colorScheme={'green'}>Go Home</Button>
        </VStack>
    </Center>
}

export default Success

Lors du rendu, ce composant affiche le message « Success ! » et affiche tous les paramètres de requête de l’URL à l’écran. Il comprend également un bouton pour rediriger les utilisateurs vers la page d’accueil de l’application.

Lors de la création d’applications réelles, c’est sur cette page que se déroulent les transactions non critiques qui dépendent de la réussite de la transaction en cours. Par exemple, si vous construisez une page de commande pour une boutique en ligne, vous pouvez utiliser cette page pour montrer une confirmation à l’utilisateur et lui indiquer quand les produits qu’il a achetés seront livrés.

Ensuite, créez un fichier Failure.tsx contenant le code suivant :

import {Button, Center, Heading, Text, VStack} from "@chakra-ui/react";
import {useNavigate} from "react-router-dom";

function Failure() {
    const queryParams = new URLSearchParams(window.location.search)
    const navigate = useNavigate()
    const onButtonClick = () => {
        navigate("/")
    }

    return <Center h={'100vh'} color='red'>
        <VStack spacing={3}>
            <Heading fontSize={'4xl'}>Failure!</Heading>
            <Text color={'black'}>{queryParams.toString().split("&").join("n")}</Text>
            <Button onClick={onButtonClick} colorScheme={'red'}>Try Again</Button>
        </VStack>
    </Center>
}

export default Failure

Ce composant est similaire à celui de Success.tsx et affiche le message « Failure ! » lorsqu’il est rendu.

.

Pour les tâches essentielles telles que la livraison des produits, l’envoi d’e-mails ou toute partie critique de votre flux d’achat, utilisez des crochets web. Les crochets web sont des routes API sur votre serveur que Stripe peut invoquer lorsqu’une transaction se produit.

Le crochet web reçoit tous les détails de la transaction (via l’objet CheckoutSession ), ce qui vous permet de l’enregistrer dans la base de données de votre application et de déclencher les flux de travail de réussite ou d’échec correspondants. Comme votre serveur est toujours accessible à Stripe, aucune transaction n’est manquée, ce qui garantit la fonctionnalité constante de votre boutique en ligne.

Enfin, mettez à jour le fichier App.tsx pour qu’il ressemble à ceci :

import Home from "./routes/Home.tsx";
import {createBrowserRouter, RouterProvider,} from "react-router-dom";
import HostedCheckout from "./routes/HostedCheckout.tsx";
import Success from "./routes/Success.tsx";
import Failure from "./routes/Failure.tsx";

function App() {

    const router = createBrowserRouter([
        {
            path: "/",
            element: (
                <Home/>
            ),
        },
        {
            path: "/hosted-checkout",
            element: (
                <HostedCheckout/>
            )
        },
        {
            path: '/success',
            element: (
                <Success/>
            )
        },
        {
            path: '/failure',
            element: (
                <Failure/>
            )
        },
    ]);

    return (
        <RouterProvider router={router}/>
    )
}

export default App

Cela permettra de s’assurer que les composants Success et Failure sont rendus sur les routes /success et /failure, respectivement.

La configuration du frontend est terminée. Ensuite, il faut configurer le backend pour créer le point de terminaison /checkout/hosted.

Construction du backend

Ouvrez le projet backend et installe le SDK Stripe en ajoutant les lignes suivantes dans le tableau des dépendances de votre fichier pom.xml:

        <dependency>
            <groupId>com.stripe</groupId>
            <artifactId>stripe-java</artifactId>
            <version>22.29.0</version>
        </dependency>

Ensuite, chargez les modifications Maven dans votre projet pour installer les dépendances. Si votre IDE ne prend pas en charge cette opération via l’interface utilisateur, exécutez la commande maven dependency:resolve ou maven install. Si vous n’avez pas le CLI maven, utilisez le wrapper mvnw de Spring initializr lorsque vous créez le projet.

Une fois les dépendances installées, créez un nouveau contrôleur REST pour gérer les requêtesHTTP entrantes pour votre application backend. Pour cela, créez un fichier PaymentController.java dans le répertoire src/main/java/com/kinsta/stripe-java/backend et ajoutez le code suivant :

package com.kinsta.stripejava.backend;

import com.stripe.Stripe;
import com.stripe.exception.StripeException;
import com.stripe.model.Customer;
import com.stripe.model.Product;
import com.stripe.model.checkout.Session;
import com.stripe.param.checkout.SessionCreateParams;
import com.stripe.param.checkout.SessionCreateParams.LineItem.PriceData;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@CrossOrigin
public class PaymentController {

    String STRIPE_API_KEY = System.getenv().get("STRIPE_API_KEY");

    @PostMapping("/checkout/hosted")
    String hostedCheckout(@RequestBody RequestDTO requestDTO) throws StripeException {
      return "Hello World!";
    }

}

Le code ci-dessus importe les dépendances Stripe essentielles et établit la classe PaymentController. Cette classe porte deux annotations : @RestController et @CrossOrigin. L’annotation @RestController indique à Spring Boot de traiter cette classe comme un contrôleur, et ses méthodes peuvent maintenant utiliser les annotations @Mapping pour traiter les requêtes HTTP entrantes.

L’annotation @CrossOrigin marque tous les points de terminaison définis dans cette classe comme ouverts à toutes les origines en vertu des règles CORS. Cependant, cette pratique est déconseillée en production en raison des failles de sécurité potentielles provenant de divers domaines Internet.

Pour des résultats optimaux, il est conseillé d’héberger les serveurs frontend et backend sur le même domaine afin de contourner les problèmes CORS. Sinon, si ce n’est pas possible, vous pouvez spécifier le domaine de votre client frontend (qui envoie des requêtes au serveur backend) en utilisant l’annotation @CrossOrigin, comme ceci :

@CrossOrigin(origins = "http://frontend.com")

La classe PaymentController extrait la clé API Stripe des variables d’environnement pour la fournir ultérieurement au SDK Stripe. Lors de l’exécution de l’application, vous devez fournir votre clé API Stripe à l’application par le biais des variables d’environnement.

Localement, vous pouvez créer une nouvelle variable d’environnement dans votre système, soit de façon temporaire (en ajoutant une phrase KEY=VALUE avant la commande utilisée pour démarrer votre serveur de développement), soit de façon permanente (en mettant à jour les fichiers de configuration de votre terminal ou en définissant une variable d’environnement dans le panneau de configuration de Windows).

Dans les environnements de production, votre fournisseur de déploiement (tel que Kinsta) vous fournira une option séparée pour remplir les variables d’environnement utilisées par votre application.

Si vous utilisez IntelliJ IDEA (ou un IDE similaire), cliquez sur Exécuter les configurations en haut à droite de l’IDE et cliquez sur l’option Modifier les configurations… dans la liste déroulante qui s’ouvre pour mettre à jour votre commande d’exécution et définir la variable d’environnement.

Ouverture de la boîte de dialogue des configurations d'exécution/débogage.
Ouverture de la boîte de dialogue des configurations d’exécution/débogage.

Une boîte de dialogue s’ouvre alors, dans laquelle vous pouvez indiquer les variables d’environnement de votre application à l’aide du champ Variables d’environnement. Saisissez la variable d’environnement STRIPE_API_KEY au format VAR1=VALUE. Vous pouvez trouver votre clé API sur le site web des développeurs de Stripe. Vous devez fournir la valeur de la clé secrète à partir de cette page.

Le tableau de bord Stripe affichant les clés API.
Le tableau de bord Stripe affichant les clés API.

Si vous ne l’avez pas encore fait, créez un nouveau compte Stripe pour avoir accès aux clés API.

Une fois que vous avez configuré la clé API, passez à la construction du point de terminaison. Ce point de terminaison recueillera les données du client (nom et e-mail), créera un profil client pour eux dans Stripe s’il n’en existe pas déjà un, et créera une session de paiement pour permettre aux utilisateurs de payer les articles du panier.

Voici à quoi ressemble le code de la méthode hostedCheckout:

    @PostMapping("/checkout/hosted")
    String hostedCheckout(@RequestBody RequestDTO requestDTO) throws StripeException {

        Stripe.apiKey = STRIPE_API_KEY;
        String clientBaseURL = System.getenv().get("CLIENT_BASE_URL");

        // Start by finding an existing customer record from Stripe or creating a new one if needed
        Customer customer = CustomerUtil.findOrCreateCustomer(requestDTO.getCustomerEmail(), requestDTO.getCustomerName());

        // Next, create a checkout session by adding the details of the checkout
        SessionCreateParams.Builder paramsBuilder =
                SessionCreateParams.builder()
                        .setMode(SessionCreateParams.Mode.PAYMENT)
                        .setCustomer(customer.getId())
                        .setSuccessUrl(clientBaseURL + "/success?session_id={CHECKOUT_SESSION_ID}")
                        .setCancelUrl(clientBaseURL + "/failure");

        for (Product product : requestDTO.getItems()) {
            paramsBuilder.addLineItem(
                    SessionCreateParams.LineItem.builder()
                            .setQuantity(1L)
                            .setPriceData(
                                    PriceData.builder()
                                            .setProductData(
                                                    PriceData.ProductData.builder()
                                                            .putMetadata("app_id", product.getId())
                                                            .setName(product.getName())
                                                            .build()
                                            )
                                            .setCurrency(ProductDAO.getProduct(product.getId()).getDefaultPriceObject().getCurrency())
                                            .setUnitAmountDecimal(ProductDAO.getProduct(product.getId()).getDefaultPriceObject().getUnitAmountDecimal())
                                            .build())
                            .build());
        }

        }

        Session session = Session.create(paramsBuilder.build());

        return session.getUrl();
    }

Lors de la création de la session de paiement, le code utilise le nom du produit reçu du client mais n’utilise pas les détails du prix provenant de la demande. Cette approche permet d’éviter les manipulations de prix potentielles côté client, où des acteurs malveillants pourraient envoyer des prix réduits dans la demande de paiement afin de payer moins cher les produits et les services.

Pour éviter cela, la méthode hostedCheckout interroge votre base de données de produits (via ProductDAO) pour récupérer le prix correct de l’article.

En outre, Stripe propose diverses classes Builder qui suivent le modèle de conception builder. Ces classes permettent de créer des objets paramètres pour les requêtes Stripe. L’extrait de code fourni fait également référence aux variables d’environnement pour récupérer l’URL de l’application cliente. Cette URL est nécessaire pour que l’objet de session de paiement puisse être redirigé de manière appropriée après un paiement réussi ou échoué.

Pour exécuter ce code, définissez l’URL de l’application cliente via les variables d’environnement, de la même manière que la clé API Stripe a été fournie. Comme l’application client est exécutée par Vite, l’URL de l’application locale doit être http://localhost:5173. Incluez ceci dans vos variables d’environnement par l’intermédiaire de votre IDE, de votre terminal ou du panneau de contrôle du système.

CLIENT_BASE_URL=http://localhost:5173

Fournissez également à l’application une adresse ProductDAO à partir de laquelle elle pourra consulter les prix des produits. L’objet d’accès aux données (Data Access Object ou DAO) interagit avec les sources de données (telles que les bases de données) pour accéder aux données liées à l’application. Bien que la mise en place d’une base de données de produits sorte du cadre de ce tutoriel, une mise en œuvre simple consiste à ajouter un nouveau fichier ProductDAO.java dans le même répertoire que PaymentController.java et à y coller le code suivant :

package com.kinsta.stripejava.backend;

import com.stripe.model.Price;
import com.stripe.model.Product;

import java.math.BigDecimal;

public class ProductDAO {

    static Product[] products;

    static {
        products = new Product[4];

        Product sampleProduct = new Product();
        Price samplePrice = new Price();

        sampleProduct.setName("Puma Shoes");
        sampleProduct.setId("shoe");
        samplePrice.setCurrency("usd");
        samplePrice.setUnitAmountDecimal(BigDecimal.valueOf(2000));
        sampleProduct.setDefaultPriceObject(samplePrice);
        products[0] = sampleProduct;

        sampleProduct = new Product();
        samplePrice = new Price();

        sampleProduct.setName("Nike Sliders");
        sampleProduct.setId("slippers");
        samplePrice.setCurrency("usd");
        samplePrice.setUnitAmountDecimal(BigDecimal.valueOf(1000));
        sampleProduct.setDefaultPriceObject(samplePrice);
        products[1] = sampleProduct;

        sampleProduct = new Product();
        samplePrice = new Price();

        sampleProduct.setName("Apple Music+");
        sampleProduct.setId("music");
        samplePrice.setCurrency("usd");
        samplePrice.setUnitAmountDecimal(BigDecimal.valueOf(499));
        sampleProduct.setDefaultPriceObject(samplePrice);
        products[2] = sampleProduct;

    }

    public static Product getProduct(String id) {

        if ("shoe".equals(id)) {
            return products[0];
        } else if ("slippers".equals(id)) {
            return products[1];
        } else if ("music".equals(id)) {
            return products[2];
        } else return new Product();

    }
}

Cela initialisera un tableau de produits et te permettra d’interroger les données du produit à l’aide de son identifiant (ID). Vous devrez également créer un DTO (Data Transfer Object) pour permettre à Spring Boot de sérialiser automatiquement la charge utile entrante du client et de vous présenter un objet simple pour accéder aux données. Pour cela, créez un nouveau fichier RequestDTO.java et collez le code suivant :

package com.kinsta.stripejava.backend;

import com.stripe.model.Product;

public class RequestDTO {
    Product[] items;
    String customerName;
    String customerEmail;

    public Product[] getItems() {
        return items;
    }

    public String getCustomerName() {
        return customerName;
    }

    public String getCustomerEmail() {
        return customerEmail;
    }

}

Ce fichier définit un POJO qui porte le nom du client, son e-mail et la liste des articles avec lesquels il passe la commande.

Enfin, implémentez la méthode CustomerUtil.findOrCreateCustomer() pour créer l’objet Client dans Stripe s’il n’existe pas déjà. Pour cela, créez un fichier portant le nom CustomerUtil et ajoutez-y le code suivant :

package com.kinsta.stripejava.backend;

import com.stripe.exception.StripeException;
import com.stripe.model.Customer;
import com.stripe.model.CustomerSearchResult;
import com.stripe.param.CustomerCreateParams;
import com.stripe.param.CustomerSearchParams;

public class CustomerUtil {

    public static Customer findCustomerByEmail(String email) throws StripeException {
        CustomerSearchParams params =
                CustomerSearchParams
                        .builder()
                        .setQuery("email:'" + email + "'")
                        .build();

        CustomerSearchResult result = Customer.search(params);

        return result.getData().size() > 0 ? result.getData().get(0) : null;
    }

    public static Customer findOrCreateCustomer(String email, String name) throws StripeException {
        CustomerSearchParams params =
                CustomerSearchParams
                        .builder()
                        .setQuery("email:'" + email + "'")
                        .build();

        CustomerSearchResult result = Customer.search(params);

        Customer customer;

        // If no existing customer was found, create a new record
        if (result.getData().size() == 0) {

            CustomerCreateParams customerCreateParams = CustomerCreateParams.builder()
                    .setName(name)
                    .setEmail(email)
                    .build();

            customer = Customer.create(customerCreateParams);
        } else {
            customer = result.getData().get(0);
        }

        return customer;
    }
}

Cette classe contient également une autre méthode findCustomerByEmail qui vous permet de rechercher des clients dans Stripe à l’aide de leur adresse e-mail. L’API de recherche de clients est utilisée pour rechercher les enregistrements de clients dans la base de données Stripe et l’API de création de clients est utilisée pour créer les enregistrements de clients selon les besoins.

Ceci complète la configuration du backend nécessaire pour le flux de paiement hébergé. vous pouvez maintenant tester l’application en exécutant l’application frontend et l’application backend dans leur IDE ou dans des terminaux séparés. Voici à quoi ressemblerait le flux de réussite :

Un flux de commande hébergée réussi.
Un flux de commande hébergée réussi.

Lorsque vous testez les intégrations Stripe, vous pouvez toujours utiliser les détails de carte suivants pour simuler les transactions par carte :

Numéro de carte : 4111 1111 1111 1111
Mois et année d’expiration : 12 / 25
CVC : N’importe quel nombre à trois chiffres
Nom sur la carte : N’importe quel nom

Si vous choisissez d’annuler la transaction au lieu de payer, voici à quoi ressemblerait le flux d’échec :

Un flux de paiement hébergé échoué.
Un flux de paiement hébergé échoué.

Ceci complète la configuration d’une expérience de paiement hébergée par Stripe intégrée à votre application. Vous ouvez consulter la documentation de Stripe pour en savoir plus sur la façon de personnaliser votre page de commande, de recueillir plus de détails auprès du client, et plus encore.

Commande intégrée

Une expérience de commande intégrée fait référence à la construction d’un flux de paiement qui ne redirige pas tes utilisateurs en dehors de votre application (comme c’était le cas dans le flux de commande hébergé) et qui rend le formulaire de paiement dans votre application elle-même.

Construire une expérience de paiement intégrée signifie gérer les détails de paiement des clients, ce qui implique des informations sensibles telles que les numéros de carte bancaire, l’identifiant Google Pay, etc. Toutes les applications ne sont pas conçues pour gérer ces données en toute sécurité.

Pour vous décharger du fardeau de répondre à des normes comme PCI-DSS, Stripe fournit des éléments que vous pouvez utiliser dans l’application pour collecter les détails de paiement tout en laissant Stripe gérer la sécurité et traiter les paiements en toute sécurité de leur côté.

Construire le frontend

Pour commencer, installez Stripe React SDK dans votre application frontent pour accéder aux éléments Stripe en exécutant la commande suivante dans votre répertoire frontend :

npm i @stripe/react-stripe-js @stripe/stripe-js

Ensuite, créez un nouveau fichier appelé IntegratedCheckout.tsx dans votre répertoire frontend/src/routes et enregistrez le code suivant dedans :

import {Button, Center, Heading, Input, VStack} from "@chakra-ui/react";
import {useEffect, useState} from "react";
import CartItem, {ItemData} from "../components/CartItem.tsx";
import TotalFooter from "../components/TotalFooter.tsx";
import {Products} from '../data.ts'
import {Elements, PaymentElement, useElements, useStripe} from '@stripe/react-stripe-js';
import {loadStripe, Stripe} from '@stripe/stripe-js';

function IntegratedCheckout() {

    const [items] = useState<ItemData[]>(Products)
    const [transactionClientSecret, setTransactionClientSecret] = useState("")
    const [stripePromise, setStripePromise] = useState<Promise<Stripe | null> | null>(null)
    const [name, setName] = useState("")
    const [email, setEmail] = useState("")
    const onCustomerNameChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
        setName(ev.target.value)
    }

    const onCustomerEmailChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
        setEmail(ev.target.value)
    }

    useEffect(() => {
        // Make sure to call `loadStripe` outside of a component’s render to avoid
        // recreating the `Stripe` object on every render.
        setStripePromise(loadStripe(process.env.VITE_STRIPE_API_KEY || ""));

    }, [])

    const createTransactionSecret = () => {
        fetch(process.env.VITE_SERVER_BASE_URL + "/checkout/integrated", {
            method: "POST",
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                items: items.map(elem => ({name: elem.name, id: elem.id})),
                customerName: name,
                customerEmail: email,
            })
        })
            .then(r => r.text())
            .then(r => {
                setTransactionClientSecret(r)
            })
    }

    return <>
        <Center h={'100vh'} color='black'>
            <VStack spacing='24px'>
                <Heading>Integrated Checkout Example</Heading>
                {items.map(elem => {
                    return <CartItem data={elem} mode={'checkout'}/>
                })}
                <TotalFooter total={30} mode={"checkout"}/>

                <Input variant='filled' placeholder='Customer Name' onChange={onCustomerNameChange}
                       value={name}/>
                <Input variant='filled' placeholder='Customer Email' onChange={onCustomerEmailChange}
                       value={email}/>
                <Button onClick={createTransactionSecret} colorScheme={'green'}>Initiate Payment</Button>

                {(transactionClientSecret === "" ?
                    <></>
                    : <Elements stripe={stripePromise} options={{clientSecret: transactionClientSecret}}>
                        <CheckoutForm/>
                    </Elements>)}
            </VStack>
        </Center>
    </>
}

const CheckoutForm = () => {

    const stripe = useStripe();
    const elements = useElements();
    const handleSubmit = async (event: React.MouseEvent<HTMLButtonElement>) => {
        event.preventDefault();

        if (!stripe || !elements) {
            return;
        }

        const result = await stripe.confirmPayment({
            elements,
            confirmParams: {
                return_url: process.env.VITE_CLIENT_BASE_URL + "/success",
            },
        });

        if (result.error) {
            console.log(result.error.message);
        }
    };

    return <>
        <VStack>
            <PaymentElement/>
            <Button colorScheme={'green'} disabled={!stripe} onClick={handleSubmit}>Pay</Button>
        </VStack>
    </>
}

export default IntegratedCheckout

Ce fichier définit deux composants, IntegratedCheckout et CheckoutForm. CheckoutForm définit un formulaire simple avec un PaymentElement de Stripe qui recueille les détails de paiement des clients et un bouton Payer qui déclenche une demande d’encaissement du paiement.

Ce composant appelle également le crochet useStripe() et useElements() pour créer une instance du SDK Stripe que vous pouvez utiliser pour créer des demandes de paiement. Une fois que vous avez cliqué sur le bouton Payer, la méthode stripe.confirmPayment() du SDK Stripe est appelée pour collecter les données de paiement de l’utilisateur à partir de l’instance d’éléments et les envoyer au backend de Stripe avec une URL de réussite vers laquelle rediriger si la transaction est réussie.

Le formulaire de paiement a été séparé du reste de la page parce que les crochets useStripe() et useElements() doivent être appelés dans le contexte d’un fournisseur Elements, ce qui a été fait dans la déclaration de retour de IntegratedCheckout. Si vous déplacez les appels au crochet Stripe vers le composant IntegratedCheckout directement, ils seront en dehors de la portée du fournisseur Elements et ne fonctionneront donc pas.

Le composant IntegratedCheckout réutilise les composants CartItem et TotalFooter pour afficher les articles du panier et le montant total. Il affiche également deux champs de saisie pour recueillir les informations du client et un bouton Initier le paiement qui envoie une demande au serveur Java pour créer la clé secrète du client à l’aide des détails du client et du panier. Une fois la clé secrète du client reçue, le site CheckoutForm est rendu, ce qui permet de collecter les informations de paiement du client.

En outre, useEffect est utilisé pour appeler la méthode loadStripe. Cet effet n’est exécuté qu’une seule fois lors du rendu du composant afin que le SDK Stripe ne soit pas chargé plusieurs fois lorsque les états internes du composant sont mis à jour.

Pour exécuter le code ci-dessus, vous devrez également ajouter deux nouvelles variables d’environnement à votre projet frontend : VITE_STRIPE_API_KEY et VITE_CLIENT_BASE_URL. La variable Stripe API key contiendra la clé API publiable du tableau de bord Stripe, et la variable client base URL contiendra le lien vers l’application client (qui est l’application frontend elle-même) afin qu’elle puisse être transmise au Stripe SDK pour gérer les redirections de succès et d’échec.

Pour cela, ajoutez le code suivant à votre fichier .env dans le répertoire frontend :

VITE_STRIPE_API_KEY=pk_test_xxxxxxxxxx # Your key here
VITE_CLIENT_BASE_URL=http://localhost:5173

Enfin, metez à jour le fichier App.tsx pour inclure le composant IntegratedCheckout à la route /integrated-checkout de l’application frontend. Ajoutez le code suivant dans le tableau passé à l’appel createBrowserRouter dans le composant App:

       {
            path: '/integrated-checkout',
            element: (
                <IntegratedCheckout/>
            )
        },

Ceci complète la configuration nécessaire sur le frontend. Ensuite, créez une nouvelle route sur votre serveur backend qui crée la clé secrète du client nécessaire pour gérer les sessions de paiement intégrées sur votre application frontend.

Construction du backend

Pour s’assurer que l’intégration frontend n’est pas utilisée de manière abusive par les attaquants (puisque le code du frontend est plus facile à craquer que celui du backend), Stripe vous demande de générer un secret client unique sur votre serveur backend et de vérifier chaque demande de paiement intégré avec le secret client généré sur le backend pour s’assurer que c’est bien votre application qui essaie de collecter les paiements. Pour cela, vous devez mettre en place une autre route dans le backend qui crée des secrets clients basés sur les informations relatives au client et au panier.

Pour créer la clé du secret client sur votre serveur, créez une nouvelle méthode dans ta classe PaymentController avec le nom integratedCheckout et enregistrez le code suivant dans cette méthode :

@PostMapping("/checkout/integrated")
    String integratedCheckout(@RequestBody RequestDTO requestDTO) throws StripeException {

        Stripe.apiKey = STRIPE_API_KEY;

        // Start by finding existing customer or creating a new one if needed
        Customer customer = CustomerUtil.findOrCreateCustomer(requestDTO.getCustomerEmail(), requestDTO.getCustomerName());

        // Create a PaymentIntent and send it's client secret to the client
        PaymentIntentCreateParams params =
                PaymentIntentCreateParams.builder()
                       .setAmount(Long.parseLong(calculateOrderAmount(requestDTO.getItems())))
                        .setCurrency("usd")
                        .setCustomer(customer.getId())
                        .setAutomaticPaymentMethods(
                                PaymentIntentCreateParams.AutomaticPaymentMethods
                                        .builder()
                                        .setEnabled(true)
                                        .build()
                        )
                        .build();

        PaymentIntent paymentIntent = PaymentIntent.create(params);

        // Send the client secret from the payment intent to the client
        return paymentIntent.getClientSecret();
    }

De la même manière que la session de commande a été construite à l’aide d’une classe de construction qui accepte la configuration de la demande de paiement, le flux de paiement intégré exige que vous construisiez une session de paiement avec le montant, la devise et les moyens de paiement. Contrairement à la session de paiement, vous ne pouvez pas associer d’éléments de ligne à une session de paiement à moins de créer une facture, ce que vous apprendrez dans une section ultérieure du tutoriel.

Comme vous ne transmettez pas les articles au constructeur de la session de paiement, vous devez calculer manuellement le montant total des articles du panier et l’envoyer au backend de Stripe. Utilisez ProductDAO pour trouver et ajouter les prix de chaque produit du panier.

Pour cela, définissez une nouvelle méthode calculateOrderAmount et ajoutez-y le code suivant :

     static String calculateOrderAmount(Product[] items) {
        long total = 0L;

        for (Product item: items) {
            // Look up the application database to find the prices for the products in the given list
            total += ProductDAO.getProduct(item.getId()).getDefaultPriceObject().getUnitAmountDecimal().floatValue();
        }
        return String.valueOf(total);
    }

Cela devrait suffire pour mettre en place le flux de paiement intégré à la fois sur le frontend et le backend. Vous pouvez redémarrer les serveurs de développement pour le serveur et le client et essayer le nouveau flux de paiement intégré dans l’application frontend. Voici à quoi ressemblera le flux intégré :

Un flux de commande intégré.
Un flux de commande intégré.

Ceci complète un flux de paiement intégré de base dans votre application. Vous pouvez maintenant explorer davantage la documentation Stripe pour personnaliser les moyens de paiement ou intégrer d’autres composants pour vous aider dans d’autres opérations telles que la collecte d’adresses, les demandes de paiement, l’intégration de liens, et bien plus encore !

Configurer des abonnements pour des services récurrents

De nos jours, les boutiques en ligne proposent souvent des abonnements. Que vous construisiez une place de marché pour des services ou que vous proposiez un produit numérique de façon périodique, un abonnement est la solution parfaite pour donner à vos clients un accès périodique à votre service pour une somme modique par rapport à un achat unique.

Stripe peut vous aider à mettre en place et à annuler des abonnements facilement. Vous pouvez également proposer des essais gratuits dans le cadre de votre abonnement afin que les utilisateurs puissent essayer votre offre avant de s’engager.

Configurer un nouvel abonnement

La mise en place d’un nouvel abonnement est simple en utilisant le flux de paiement hébergé. Il vous suffira de modifier quelques paramètres lors de la construction de la demande de paiement et de créer une nouvelle page (en réutilisant les composants existants) pour afficher une page de paiement pour un nouvel abonnement. Pour commencer, créez un fichier NewSubscription.tsx dans le dossier components du frontend. Collez le code suivant dans ce fichier :

import {Center, Heading, VStack} from "@chakra-ui/react";
import {useState} from "react";
import CartItem, {ItemData} from "../components/CartItem.tsx";
import TotalFooter from "../components/TotalFooter.tsx";
import CustomerDetails from "../components/CustomerDetails.tsx";
import {Subscriptions} from "../data.ts";

function NewSubscription() {
    const [items] = useState<ItemData[]>(Subscriptions)
    return <>
        <Center h={'100vh'} color='black'>
            <VStack spacing='24px'>
                <Heading>New Subscription Example</Heading>
                {items.map(elem => {
                    return <CartItem data={elem} mode={'subscription'}/>
                })}
                <TotalFooter total={4.99} mode={"subscription"}/>
                <CustomerDetails data={items} endpoint={"/subscriptions/new"} />
            </VStack>
        </Center>
    </>
}

export default NewSubscription

Dans le code ci-dessus, les données du panier proviennent du fichier data.ts, et il ne contient qu’un seul article pour simplifier le processus. Dans les scénarios réels, vous pouvez avoir plusieurs articles dans le cadre d’une commande d’abonnement.

Pour effectuer le rendu de ce composant sur la bonne route, ajoutez le code suivant dans le tableau passé à l’appel createBrowserRouter dans le composant App.tsx:

       {
            path: '/new-subscription',
            element: (
                <NewSubscription/>
            )
        },

Ceci complète la configuration nécessaire sur le frontend. Sur le backend, créez une nouvelle route /subscription/new pour créer une nouvelle session de paiement hébergée pour un produit d’abonnement. Créez une méthode newSubscription dans le répertoire backend/src/main/java/com/kinsta/stripejava/backend et enregistrez le code suivant dedans :

@PostMapping("/subscriptions/new")
    String newSubscription(@RequestBody RequestDTO requestDTO) throws StripeException {

        Stripe.apiKey = STRIPE_API_KEY;

        String clientBaseURL = System.getenv().get("CLIENT_BASE_URL");

        // Start by finding existing customer record from Stripe or creating a new one if needed
        Customer customer = CustomerUtil.findOrCreateCustomer(requestDTO.getCustomerEmail(), requestDTO.getCustomerName());

        // Next, create a checkout session by adding the details of the checkout
        SessionCreateParams.Builder paramsBuilder =
                SessionCreateParams.builder()
                        // For subscriptions, you need to set the mode as subscription
                        .setMode(SessionCreateParams.Mode.SUBSCRIPTION)
                        .setCustomer(customer.getId())
                        .setSuccessUrl(clientBaseURL + "/success?session_id={CHECKOUT_SESSION_ID}")
                        .setCancelUrl(clientBaseURL + "/failure");

        for (Product product : requestDTO.getItems()) {
            paramsBuilder.addLineItem(
                    SessionCreateParams.LineItem.builder()
                            .setQuantity(1L)
                            .setPriceData(
                                    PriceData.builder()
                                            .setProductData(
                                                    PriceData.ProductData.builder()
                                                            .putMetadata("app_id", product.getId())
                                                            .setName(product.getName())
                                                            .build()
                                            )
                                            .setCurrency(ProductDAO.getProduct(product.getId()).getDefaultPriceObject().getCurrency())
                                            .setUnitAmountDecimal(ProductDAO.getProduct(product.getId()).getDefaultPriceObject().getUnitAmountDecimal())
                                            // For subscriptions, you need to provide the details on how often they would recur
                                            .setRecurring(PriceData.Recurring.builder().setInterval(PriceData.Recurring.Interval.MONTH).build())
                                            .build())
                            .build());
        }

        Session session = Session.create(paramsBuilder.build());

        return session.getUrl();
    }

Le code de cette méthode est assez similaire au code de la méthode hostedCheckout, sauf que le mode défini pour la création de la session est l’abonnement au lieu du produit et qu’avant de créer la session, une valeur est définie pour l’intervalle de récurrence de l’abonnement.

Cela indique à Stripe de traiter cette commande comme une commande d’abonnement au lieu d’un paiement unique. Comme la méthode hostedCheckout, cette méthode renvoie également l’URL de la page de paiement hébergée en tant que réponse HTTP au client. Le client est configuré pour rediriger vers l’URL reçue, ce qui permet au client de terminer le paiement.

Vous pouvez redémarrer les serveurs de développement pour le client et le serveur et voir la nouvelle page d’abonnement en action. Voici à quoi elle ressemble :

Flux de commande d'un abonnement hébergé.
Flux de commande d’un abonnement hébergé.

Annulation d’un abonnement existant

Maintenant que vous savez comment créer de nouveaux abonnements, apprenons à permettre à vos clients d’annuler des abonnements existants. Puisque l’application de démonstration construite dans ce tutoriel ne contient aucune configuration d’authentification, utilisez un formulaire pour permettre au client de saisir son e-mail afin de consulter ses abonnements, puis dotez chaque élément d’abonnement d’un bouton d’annulation pour permettre à l’utilisateur de l’annuler.

Pour cela, vous devrez procéder comme ci-dessous :

  1. Mettre à jour le composant CartItem pour afficher un bouton d’annulation sur la page d’annulation des abonnements.
  2. Créer un composant CancelSubscription qui affiche d’abord un champ de saisie et un bouton permettant au client de rechercher des abonnements à l’aide de son adresse e-mail, puis qui affiche une liste d’abonnements à l’aide du composant CartItem mis à jour.
  3. Créer une nouvelle méthode dans le serveur backend qui peut rechercher des abonnements à partir du backend Stripe en utilisant l’adresse e-mail du client.
  4. Créer une nouvelle méthode dans le serveur backend qui peut annuler un abonnement en fonction de l’identifiant de l’abonnement qui lui a été transmis.

Commencez par mettre à jour le composant CartItem pour qu’il ressemble à ceci :

// Existing imports here

function CartItem(props: CartItemProps) {

    // Add this hook call and the cancelSubscription method to cancel the selected subscription
    const toast = useToast()
    const cancelSubscription = () => {

        fetch(process.env.VITE_SERVER_BASE_URL + "/subscriptions/cancel", {
            method: "POST",
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                subscriptionId: props.data.stripeSubscriptionData?.subscriptionId
            })
        })
            .then(r => r.text())
            .then(() => {
                toast({
                    title: 'Subscription cancelled.',
                    description: "We've cancelled your subscription for you.",
                    status: 'success',
                    duration: 9000,
                    isClosable: true,
                })

                if (props.onCancelled)
                    props.onCancelled()
            })
    }

    return <Card direction={{base: 'column', sm: 'row'}}
                 overflow='hidden'
                 width={'xl'}
                 variant='outline'>
        <Image
            objectFit='cover'
            maxW={{base: '100%', sm: '200px'}}
            src={props.data.image}
        />
        <Stack mt='6' spacing='3'>
            <CardBody>
                <VStack spacing={'3'} alignItems={"flex-start"}>
                    <Heading size='md'>{props.data.name}</Heading>
                    <VStack spacing={'1'} alignItems={"flex-start"}>
                        <Text>
                            {props.data.description}
                        </Text>
                        {(props.mode === "checkout" ? <Text>
                            {"Quantity: " + props.data.quantity}
                        </Text> : <></>)}
                    </VStack>

                    {/* <----------------------- Add this block ----------------------> */}
                    {(props.mode === "subscription" && props.data.stripeSubscriptionData ?
                        <VStack spacing={'1'} alignItems={"flex-start"}>
                            <Text>
                                {"Next Payment Date: " + props.data.stripeSubscriptionData.nextPaymentDate}
                            </Text>
                            <Text>
                                {"Subscribed On: " + props.data.stripeSubscriptionData.subscribedOn}
                            </Text>
                            {(props.data.stripeSubscriptionData.trialEndsOn ? <Text>
                                {"Free Trial Running Until: " + props.data.stripeSubscriptionData.trialEndsOn}
                            </Text> : <></>)}
                        </VStack> : <></>)}
                </VStack>

            </CardBody>

            <CardFooter>
                <VStack alignItems={'flex-start'}>
                    <Text color='blue.600' fontSize='2xl'>
                        {"$" + props.data.price}
                    </Text>
                    {/* <----------------------- Add this block ----------------------> */}
                    {(props.data.stripeSubscriptionData ?
                        <Button colorScheme={'red'} onClick={cancelSubscription}>Cancel Subscription</Button>
                        : <></>)}
                </VStack>
            </CardFooter>
        </Stack>
    </Card>
}

// Existing types here

export default CartItem

Ensuite, créez un composant CancelSubscription.tsx dans le répertoire routes de votre page frontend et enregistrez-y le code suivant :

import {Button, Center, Heading, Input, VStack} from "@chakra-ui/react";
import {useState} from "react";
import CartItem, {ItemData, ServerSubscriptionsResponseType} from "../components/CartItem.tsx";
import {Subscriptions} from "../data.ts";

function CancelSubscription() {
    const [email, setEmail] = useState("")
    const [subscriptions, setSubscriptions] = useState<ItemData[]>([])

    const onCustomerEmailChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
        setEmail(ev.target.value)
    }

    const listSubscriptions = () => {

        fetch(process.env.VITE_SERVER_BASE_URL + "/subscriptions/list", {
            method: "POST",
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                customerEmail: email,
            })
        })
            .then(r => r.json())
            .then((r: ServerSubscriptionsResponseType[]) => {

                const subscriptionsList: ItemData[] = []

                r.forEach(subscriptionItem => {

                    let subscriptionDetails = Subscriptions.find(elem => elem.id === subscriptionItem.appProductId) || undefined

                    if (subscriptionDetails) {

                        subscriptionDetails = {
                            ...subscriptionDetails,
                            price: Number.parseInt(subscriptionItem.price) / 100,
                            stripeSubscriptionData: subscriptionItem,
                        }

                        subscriptionsList.push(subscriptionDetails)
                    } else {
                        console.log("Item not found!")
                    }
                })

                setSubscriptions(subscriptionsList)
            })

    }

    const removeSubscription = (id: string | undefined) => {
        const newSubscriptionsList = subscriptions.filter(elem => (elem.stripeSubscriptionData?.subscriptionId !== id))
        setSubscriptions(newSubscriptionsList)
    }

    return <>
        <Center h={'100vh'} color='black'>
            <VStack spacing={3} width={'xl'}>
                <Heading>Cancel Subscription Example</Heading>
                {(subscriptions.length === 0 ? <>
                    <Input variant='filled' placeholder='Customer Email' onChange={onCustomerEmailChange}
                           value={email}/>
                    <Button onClick={listSubscriptions} colorScheme={'green'}>List Subscriptions</Button>
                </> : <></>)}
                {subscriptions.map(elem => {
                    return <CartItem data={elem} mode={'subscription'} onCancelled={() => removeSubscription(elem.stripeSubscriptionData?.subscriptionId)}/>
                })}
            </VStack>
        </Center>
    </>
}

export default CancelSubscription

Ce composant rend un champ de saisie et un bouton pour que les clients puissent saisir leur e-mail et commencer à chercher des abonnements. Si des abonnements sont trouvés, le champ de saisie et le bouton sont masqués et une liste d’abonnements s’affiche à l’écran. Pour chaque élément d’abonnement, le composant transmet une méthode removeSubscription qui demande au serveur backend Java d’annuler l’abonnement sur le backend Stripe.

Pour l’attacher à la route /cancel-subscription de votre application frontend, ajoutez le code suivant dans le tableau passé à l’appel createBrowserRouter dans le composant App:

       {
            path: '/cancel-subscription',
            element: (
                <CancelSubscription/>
            )
        },

Pour rechercher des abonnements sur le serveur backend, ajoutez une méthode viewSubscriptions dans la classe PaymentController de votre projet backend avec le contenu suivant :

@PostMapping("/subscriptions/list")
    List<Map<String, String>> viewSubscriptions(@RequestBody RequestDTO requestDTO) throws StripeException {

        Stripe.apiKey = STRIPE_API_KEY;

        // Start by finding existing customer record from Stripe
        Customer customer = CustomerUtil.findCustomerByEmail(requestDTO.getCustomerEmail());

        // If no customer record was found, no subscriptions exist either, so return an empty list
        if (customer == null) {
            return new ArrayList<>();
        }

        // Search for subscriptions for the current customer
        SubscriptionCollection subscriptions = Subscription.list(
                SubscriptionListParams.builder()
                        .setCustomer(customer.getId())
                        .build());

        List<Map<String, String>> response = new ArrayList<>();

        // For each subscription record, query its item records and collect in a list of objects to send to the client
        for (Subscription subscription : subscriptions.getData()) {
            SubscriptionItemCollection currSubscriptionItems =
                    SubscriptionItem.list(SubscriptionItemListParams.builder()
                            .setSubscription(subscription.getId())
                            .addExpand("data.price.product")
                            .build());

            for (SubscriptionItem item : currSubscriptionItems.getData()) {
                HashMap<String, String> subscriptionData = new HashMap<>();
                subscriptionData.put("appProductId", item.getPrice().getProductObject().getMetadata().get("app_id"));
                subscriptionData.put("subscriptionId", subscription.getId());
                subscriptionData.put("subscribedOn", new SimpleDateFormat("dd/MM/yyyy").format(new Date(subscription.getStartDate() * 1000)));
                subscriptionData.put("nextPaymentDate", new SimpleDateFormat("dd/MM/yyyy").format(new Date(subscription.getCurrentPeriodEnd() * 1000)));
                subscriptionData.put("price", item.getPrice().getUnitAmountDecimal().toString());

                if (subscription.getTrialEnd() != null && new Date(subscription.getTrialEnd() * 1000).after(new Date()))
                    subscriptionData.put("trialEndsOn", new SimpleDateFormat("dd/MM/yyyy").format(new Date(subscription.getTrialEnd() * 1000)));
                response.add(subscriptionData);
            }

        }

        return response;
    }

La méthode ci-dessus trouve d’abord l’objet client pour l’utilisateur donné dans Stripe. Ensuite, elle recherche les abonnements actifs du client. Une fois la liste des abonnements reçue, elle en extrait les articles et trouve les produits correspondants dans la base de données des produits de l’application pour les envoyer au frontend. Ceci est important car l’identifiant avec lequel le frontend identifie chaque produit dans la base de données de l’appli peut ou non être le même que l’identifiant du produit stocké dans Stripe.

Enfin, crée un cancelSubscription</code method in the PaymentController class and paste the code below to delete a subscription based on the subscription ID passed.

@PostMapping("/subscriptions/cancel")
    String cancelSubscription(@RequestBody RequestDTO requestDTO) throws StripeException {
        Stripe.apiKey = STRIPE_API_KEY;

        Subscription subscription =
                Subscription.retrieve(
                        requestDTO.getSubscriptionId()
                );

        Subscription deletedSubscription =
                subscription.cancel();

        return deletedSubscription.getStatus();
    }

Cette méthode récupère l’objet d’abonnement de Stripe, appelle la méthode cancel dessus, puis renvoie l’état de l’abonnement au client. Cependant, pour pouvoir exécuter cette méthode, vous devez mettre à jour votre objet DTO pour ajouter le champ subscriptionId. Pour cela, ajoutez le champ et la méthode suivants dans la classe RequestDTO:

package com.kinsta.stripejava.backend;

import com.stripe.model.Product;

public class RequestDTO {
    // … other fields …

    // Add this
    String subscriptionId;

    // … other getters …

    // Add this
    public String getSubscriptionId() {
        return subscriptionId;
    }

}

Une fois que vous abvez ajouté cela, vous pouvez maintenant réexécuter le serveur de développement pour l’application backend et frontend et voir le flux d’annulation en action :

A user flow showing how a successful subscription cancellation using the Stripe hosted page looks like.
Un flux d’annulation d’abonnement.

Mise en place d’essais gratuits pour les abonnements avec des transactions à valeur nulle

Une caractéristique commune à la plupart des abonnements modernes est d’offrir une courte période d’essai gratuite avant de facturer l’utilisateur. Cela permet aux utilisateurs d’explorer le produit ou le service sans y investir. Cependant, il est préférable de stocker les données de paiement du client lors de son inscription à l’essai gratuit afin de pouvoir facilement le facturer dès la fin de l’essai.

Stripe simplifie grandement la création de tels abonnements. Pour commencer, génèrez un nouveau composant dans le répertoire frontend/routes nommé SubscriptionWithTrial.tsx, et collez le code suivant :

import {Center, Heading, VStack} from "@chakra-ui/react";
import {useState} from "react";
import CartItem, {ItemData} from "../components/CartItem.tsx";
import TotalFooter from "../components/TotalFooter.tsx";
import CustomerDetails from "../components/CustomerDetails.tsx";
import {Subscriptions} from "../data.ts";

function SubscriptionWithTrial() {
    const [items] = useState<ItemData[]>(Subscriptions)
    return <>
        <Center h={'100vh'} color='black'>
            <VStack spacing='24px'>
                <Heading>New Subscription With Trial Example</Heading>
                {items.map(elem => {
                    return <CartItem data={elem} mode={'subscription'}/>
                })}
                <TotalFooter total={4.99} mode={"trial"}/>
                <CustomerDetails data={items} endpoint={"/subscriptions/trial"}/>
            </VStack>
        </Center>
    </>
}

export default SubscriptionWithTrial

Ce composant réutilise les composants créés précédemment. La principale différence entre ce composant et le composant NewSubscription est qu’il transmet le mode pour TotalFooter en tant qu’essai au lieu d’abonnement. Ainsi, le composant TotalFooter affiche un texte indiquant que le client peut commencer l’essai gratuit maintenant mais qu’il sera facturé après un mois.

Pour attacher ce composant à la route /subscription-with-trial de votre application frontend, ajoutez le code suivant dans le tableau passé à l’appel createBrowserRouter dans le composant App:

       {
            path: '/subscription-with-trial',
            element: (
                <SubscriptionWithTrial/>
            )
        },

Pour construire le flux de paiement pour les abonnements avec essai sur le backend, créez une nouvelle méthode nommée newSubscriptionWithTrial dans la classe PaymentController et ajoutez le code suivant :

    @PostMapping("/subscriptions/trial")
    String newSubscriptionWithTrial(@RequestBody RequestDTO requestDTO) throws StripeException {

        Stripe.apiKey = STRIPE_API_KEY;

        String clientBaseURL = System.getenv().get("CLIENT_BASE_URL");

        // Start by finding existing customer record from Stripe or creating a new one if needed
        Customer customer = CustomerUtil.findOrCreateCustomer(requestDTO.getCustomerEmail(), requestDTO.getCustomerName());

        // Next, create a checkout session by adding the details of the checkout
        SessionCreateParams.Builder paramsBuilder =
                SessionCreateParams.builder()
                        .setMode(SessionCreateParams.Mode.SUBSCRIPTION)
                        .setCustomer(customer.getId())
                        .setSuccessUrl(clientBaseURL + "/success?session_id={CHECKOUT_SESSION_ID}")
                        .setCancelUrl(clientBaseURL + "/failure")
                        // For trials, you need to set the trial period in the session creation request
                        .setSubscriptionData(SessionCreateParams.SubscriptionData.builder().setTrialPeriodDays(30L).build());

        for (Product product : requestDTO.getItems()) {
            paramsBuilder.addLineItem(
                    SessionCreateParams.LineItem.builder()
                            .setQuantity(1L)
                            .setPriceData(
                                    PriceData.builder()
                                            .setProductData(
                                                    PriceData.ProductData.builder()
                                                            .putMetadata("app_id", product.getId())
                                                            .setName(product.getName())
                                                            .build()
                                            )
                                            .setCurrency(ProductDAO.getProduct(product.getId()).getDefaultPriceObject().getCurrency())
                                            .setUnitAmountDecimal(ProductDAO.getProduct(product.getId()).getDefaultPriceObject().getUnitAmountDecimal())
                                            .setRecurring(PriceData.Recurring.builder().setInterval(PriceData.Recurring.Interval.MONTH).build())
                                            .build())
                            .build());
        }

        Session session = Session.create(paramsBuilder.build());

        return session.getUrl();
    }

Ce code est assez similaire à celui de la méthode newSubscription. La seule différence (et la plus importante) est qu’une période d’essai est transmise à l’objet des paramètres de création de session avec la valeur 30, indiquant une période d’essai gratuite de 30 jours.

Vous pouvez maintenant enregistrer les modifications et réexécuter le serveur de développement pour le backend et le frontend afin de voir le flux de travail de l’abonnement avec essai gratuit en action :

Un flux d'abonnement avec essai gratuit.
Un flux d’abonnement avec essai gratuit.

Générer des factures pour les paiements

Pour les abonnements, Stripe génère automatiquement des factures pour chaque paiement, même s’il s’agit d’une transaction de valeur nulle pour l’inscription à un essai. Pour les paiements ponctuels, vous pouvez choisir de créer des factures si nécessaire.

Pour commencer à associer tous les paiements à des factures, mettez à jour le corps du payload envoyé dans la fonction initiatePayment du composant CustomerDetails dans l’application frontend pour qu’il contienne la propriété suivante :

invoiceNeeded: true

Vous devrez également ajouter cette propriété dans le corps de la charge utile envoyée au serveur dans la fonction createTransactionSecret du composant IntegratedCheckout.

Ensuite, mettez à jour les routes du backend pour vérifier la présence de cette nouvelle propriété et mets à jour les appels Stripe SDK en conséquence.

Pour le moyen de paiement hébergé, afin d’ajouter la fonctionnalité de facturation, mettez à jour la méthode hostedCheckout en ajoutant les lignes de code suivantes :

@PostMapping("/checkout/hosted")
    String hostedCheckout(@RequestBody RequestDTO requestDTO) throws StripeException {

        // … other operations being done after creating the SessionCreateParams builder instance       

        // Add the following block of code just before the SessionCreateParams are built from the builder instance
        if (requestDTO.isInvoiceNeeded()) {
            paramsBuilder.setInvoiceCreation(SessionCreateParams.InvoiceCreation.builder().setEnabled(true).build());
        }

        Session session = Session.create(paramsBuilder.build());

        return session.getUrl();
    }

Ceci vérifiera la présence du champ invoiceNeeded et définira les paramètres de création en conséquence.

L’ajout d’une facture à un paiement intégré est légèrement délicat. Vous ne pouvez pas simplement définir un paramètre pour demander à Stripe de créer automatiquement une facture avec le paiement. Vous devez créer manuellement la facture, puis une intention de paiement liée.

Si l’intention de paiement est payée avec succès et complétée, la facture est marquée comme payée ; dans le cas contraire, la facture reste impayée. Bien que cela soit logique, cela peut être un peu complexe à mettre en œuvre (surtout lorsqu’il n’y a pas d’exemples clairs ou de références à suivre).

Pour mettre cela en œuvre, mettez à jour la méthode integratedCheckout pour qu’elle ressemble à ceci :

String integratedCheckout(@RequestBody RequestDTO requestDTO) throws StripeException {

        Stripe.apiKey = STRIPE_API_KEY;

        // Start by finding an existing customer or creating a new one if needed
        Customer customer = CustomerUtil.findOrCreateCustomer(requestDTO.getCustomerEmail(), requestDTO.getCustomerName());

        PaymentIntent paymentIntent;

        if (!requestDTO.isInvoiceNeeded()) {
            // If the invoice is not needed, create a PaymentIntent directly and send it to the client
            PaymentIntentCreateParams params =
                    PaymentIntentCreateParams.builder()
                            .setAmount(Long.parseLong(calculateOrderAmount(requestDTO.getItems())))
                            .setCurrency("usd")
                            .setCustomer(customer.getId())
                            .setAutomaticPaymentMethods(
                                    PaymentIntentCreateParams.AutomaticPaymentMethods
                                            .builder()
                                            .setEnabled(true)
                                            .build()
                            )
                            .build();

            paymentIntent = PaymentIntent.create(params);
        } else {
            // If invoice is needed, create the invoice object, add line items to it, and finalize it to create the PaymentIntent automatically
            InvoiceCreateParams invoiceCreateParams = new InvoiceCreateParams.Builder()
                    .setCustomer(customer.getId())
                    .build();

            Invoice invoice = Invoice.create(invoiceCreateParams);

            // Add each item to the invoice one by one
            for (Product product : requestDTO.getItems()) {

                // Look for existing Product in Stripe before creating a new one
                Product stripeProduct;

                ProductSearchResult results = Product.search(ProductSearchParams.builder()
                        .setQuery("metadata['app_id']:'" + product.getId() + "'")
                        .build());

                if (results.getData().size() != 0)
                    stripeProduct = results.getData().get(0);
                else {

                    // If a product is not found in Stripe database, create it
                    ProductCreateParams productCreateParams = new ProductCreateParams.Builder()
                            .setName(product.getName())
                            .putMetadata("app_id", product.getId())
                            .build();

                    stripeProduct = Product.create(productCreateParams);
                }

                // Create an invoice line item using the product object for the line item
                InvoiceItemCreateParams invoiceItemCreateParams = new InvoiceItemCreateParams.Builder()
                        .setInvoice(invoice.getId())
                        .setQuantity(1L)
                        .setCustomer(customer.getId())
                        .setPriceData(
                                InvoiceItemCreateParams.PriceData.builder()
                                        .setProduct(stripeProduct.getId())
                                        .setCurrency(ProductDAO.getProduct(product.getId()).getDefaultPriceObject().getCurrency())
                                        .setUnitAmountDecimal(ProductDAO.getProduct(product.getId()).getDefaultPriceObject().getUnitAmountDecimal())
                                        .build())
                        .build();

                InvoiceItem.create(invoiceItemCreateParams);
            }

            // Mark the invoice as final so that a PaymentIntent is created for it
            invoice = invoice.finalizeInvoice();

            // Retrieve the payment intent object from the invoice
            paymentIntent = PaymentIntent.retrieve(invoice.getPaymentIntent());
        }

        // Send the client secret from the payment intent to the client
        return paymentIntent.getClientSecret();
    }

L’ancien code de cette méthode est déplacé dans le bloc if qui vérifie si le champ invoiceNeeded est false. S’il est vrai, la méthode crée maintenant une facture avec les éléments de la facture et la marque comme finalisée pour qu’elle puisse être payée.

Elle récupère ensuite l’intention de paiement créée automatiquement lorsque la facture a été finalisée et envoie au client le secret de cette intention de paiement. Une fois que le client a terminé le processus de paiement intégré, le paiement est collecté et la facture est marquée comme payée.

Ceci complète la configuration nécessaire pour commencer à générer des factures à partir de ton application. Vous pouvez vous rendre dans la section des factures de votre tableau de bord Stripe pour voir les factures que votre application génère à chaque paiement d’achat et d’abonnement.

Cependant, Stripe vous permet également d’accéder aux factures via son API afin d’offrir aux clients une expérience en libre-service leur permettant de télécharger les factures quand ils le souhaitent.

Pour cela, créez un nouveau composant dans le répertoire frontend/routes nommé ViewInvoices.tsx. Collez le code suivant dans ce composant :

import {Button, Card, Center, Heading, HStack, IconButton, Input, Text, VStack} from "@chakra-ui/react";
import {useState} from "react";
import {DownloadIcon} from "@chakra-ui/icons";

function ViewInvoices() {
    const [email, setEmail] = useState("")
    const [invoices, setInvoices] = useState<InvoiceData[]>([])

    const onCustomerEmailChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
        setEmail(ev.target.value)
    }

    const listInvoices = () => {

        fetch(process.env.VITE_SERVER_BASE_URL + "/invoices/list", {
            method: "POST",
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                customerEmail: email,
            })
        })
            .then(r => r.json())
            .then((r: InvoiceData[]) => {
                setInvoices(r)
            })

    }

    return <>
        <Center h={'100vh'} color='black'>
            <VStack spacing={3} width={'xl'}>
                <Heading>View Invoices for Customer</Heading>
                {(invoices.length === 0 ? <>
                    <Input variant='filled' placeholder='Customer Email' onChange={onCustomerEmailChange}
                           value={email}/>
                    <Button onClick={listInvoices} colorScheme={'green'}>Look up Invoices</Button>
                </> : <></>)}
                {invoices.map(elem => {
                    return <Card direction={{base: 'column', sm: 'row'}}
                                 overflow='hidden'
                                 alignItems={'center'}
                                 justifyContent={'space-between'}
                                 padding={'8px'}
                                 width={500}
                                 variant='outline'>
                        <Text>
                            {elem.number}
                        </Text>
                        <HStack spacing={"3"}>
                            <Text color='blue.600' fontSize='2xl'>
                                {"$" + elem.amount}
                            </Text>
                            <IconButton onClick={() => {
                                window.location.href = elem.url
                            }} icon={<DownloadIcon/>} aria-label={'Download invoice'}/>
                        </HStack>
                    </Card>
                })}
            </VStack>
        </Center>
    </>
}

interface InvoiceData {
    number: string,
    amount: string,
    url: string
}

export default ViewInvoices

Semblable au composant CancelSubscription, ce composant affiche un champ de saisie pour que le client saisisse son e-mail et un bouton pour rechercher des factures. Une fois les factures trouvées, le champ de saisie et le bouton sont masqués, et une liste de factures avec le numéro de la facture, le montant total et un bouton pour télécharger le PDF de la facture est affichée au client.

Pour implémenter la méthode du backend qui recherche les factures du client donné et renvoie les informations pertinentes (numéro de facture, montant et URL PDF), ajoutez la méthode suivante dans votre classe PaymentController sur le backend ;

@PostMapping("/invoices/list")
    List<Map<String, String>> listInvoices(@RequestBody RequestDTO requestDTO) throws StripeException {

        Stripe.apiKey = STRIPE_API_KEY;

        // Start by finding existing customer record from Stripe
        Customer customer = CustomerUtil.findCustomerByEmail(requestDTO.getCustomerEmail());

        // If no customer record was found, no subscriptions exist either, so return an empty list
        if (customer == null) {
            return new ArrayList<>();
        }

        // Search for invoices for the current customer
        Map<String, Object> invoiceSearchParams = new HashMap<>();
        invoiceSearchParams.put("customer", customer.getId());
        InvoiceCollection invoices =
                Invoice.list(invoiceSearchParams);

        List<Map<String, String>> response = new ArrayList<>();

        // For each invoice, extract its number, amount, and PDF URL to send to the client
        for (Invoice invoice : invoices.getData()) {
            HashMap<String, String> map = new HashMap<>();

            map.put("number", invoice.getNumber());
            map.put("amount", String.valueOf((invoice.getTotal() / 100f)));
            map.put("url", invoice.getInvoicePdf());

            response.add(map);
        }

        return response;
    }

La méthode recherche d’abord le client par l’adresse e-mail qui lui a été fournie. Ensuite, elle recherche les factures de ce client qui sont marquées comme payées. Une fois la liste des factures trouvée, elle extrait le numéro de la facture, le montant et l’URL du PDF et renvoie une liste de ces informations à l’application cliente.

Voici à quoi ressemble le flux des factures :

Visualisation des factures
Visualisation des factures

Ceci termine le développement de notre application Java de démonstration (frontend et backend). Dans la section suivante, vous apprendrez à déployer cette application sur Kinsta afin que vous puissiez y accéder en ligne.

Déploiement de votre application sur Kinsta

Une fois que votre application est prête, vous pouvez la déployer sur Kinsta. Kinsta prend en charge les déploiements à partir de votre fournisseur Git préféré (Bitbucket, GitHub ou GitLab). Connectez les dépôts de code source de votre application à Kinsta, il déploie automatiquement votre application dès qu’il y a une modification dans le code.

Préparez vos projets

Pour déployer vos applications en production, identifiez les commandes de construction et de déploiement que Kinsta utilisera. Pour le frontend, assurez-vous que votre fichier package.json contient les scripts suivants :

"scripts": {
    "dev": "vite",
    "build": "NODE_ENV=production tsc && vite build",
    "start": "serve ./dist",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },

Vous devrez également installer le paquet serve npm qui vous permet de servir des sites web statiques. Ce paquet sera utilisé pour servir la version de production de votre application depuis l’environnement de déploiement de Kinsta. Vous pouvez l’installer en exécutant la commande suivante :

npm i serve

Une fois que vous aurez construit votre application à l’aide de vite, l’application entière sera empaquetée dans un seul fichier, index.html, car la configuration de React que vous utilisez dans ce tutoriel est destinée à créer des applications à page unique. Bien que cela ne fasse pas une énorme différence pour vos utilisateurs, vous devez mettre en place une configuration supplémentaire pour gérer le routage et la navigation du navigateur dans ce type d’applications.

Avec la configuration actuelle, vous ne pouvez accéder à votre application qu’à l’URL de base de ton déploiement. Si l’URL de base du déploiement est example.com, toutes les requêtes adressées à example.com/some-route conduiront à des erreurs HTTP 404.

En effet, votre serveur n’a qu’un seul fichier à servir, le fichier index.html. Une requête envoyée à example.com/some-route commencera à chercher le fichier some-route/index.html, qui n’existe pas ; elle recevra donc une réponse 404 Non Trouvé.

Pour résoudre ce problème, créez un fichier nommé serve.json dans votre dossier frontend/public et enregistrez le code suivant dans ce fichier :

{
  "rewrites": [
    { "source": "*", "destination": "/index.html" }
  ]
}

Ce fichier demandera à serve de réécrire toutes les requêtes entrantes pour les acheminer vers le fichier index.html tout en affichant le chemin vers lequel la requête originale a été envoyée dans la réponse. Cela vous permettra de servir correctement les pages de succès et d’échec de votre application lorsque Stripe redirigera vos clients vers votre application.

Pour le backend, créez un Dockerfile afin de mettre en place exactement le bon environnement pour votre application Java. L’utilisation d’un fichier Docker garantit que l’environnement fourni à votre application Java est le même sur tous les hôtes (qu’il s’agisse de votre hôte de développement local ou de l’hôte de déploiement Kinsta) et vous pouvez vous assurer que votre application s’exécute comme prévu.

Pour cela, créez un fichier nommé Dockerfile dans le dossier backend et enregistrez-y le contenu suivant :

FROM openjdk:22-oraclelinux8

LABEL maintainer="krharsh17"

WORKDIR /app

COPY . /app

RUN ./mvnw clean package

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "/app/target/backend.jar"]

Ce fichier indique au runtime d’utiliser l’image Java OpenJDK comme base du conteneur de déploiement, d’exécuter la commande ./mvnw clean package pour construire le fichier JAR de votre application et d’utiliser la commande java -jar <jar-file> pour l’exécuter. Ceci termine la préparation du code source pour le déploiement sur Kinsta.

Configurer les dépôts GitHub

Pour commencer à déployer les applications, créez deux dépôts GitHub pour héberger le code source de vos applications. Si vous utilisez le CLI de GitHub, vous pouvez le faire via le terminal en exécutant les commandes suivantes :

# Run these in the backend folder
gh repo create stripe-payments-java-react-backend --public --source=. --remote=origin
git init
git add .
git commit -m "Initial commit"
git push origin main

# Run these in the frontend folder
gh repo create stripe-payments-java-react-frontend --public --source=. --remote=origin
git init
git add .
git commit -m "Initial commit"
git push origin main

Cela devrait créer de nouveaux dépôts GitHub dans votre compte et y pousser le code de vos applications. Vous devriez pouvoir accéder aux dépôts frontend et backend. Ensuite, déployez cesdépôts sur Kinsta en suivant ces étapes :

  1. Connectez-vous ou créez votre compte Kinsta sur le tableau de bord MyKinsta.
  2. Dans la colonne latérale de gauche, cliquez sur Applications, puis sur Ajouter une application.
  3. Dans la modale qui s’affiche, choisissez le dépôt que vous voulez déployer. Si vous avez plusieurs branches, vous pouvez sélectionner la branche souhaitée et donner un nom à votre application.
  4. Sélectionnez l’un des emplacements de centre de données disponibles dans la liste des 25 options. Kinsta détecte automatiquement la commande de démarrage de votre application.

N’oubliez pas que vous devez fournir à vos applications frontend et backend certaines variables d’environnement pour qu’elles fonctionnent correctement. L’application frontend a besoin des variables d’environnement suivantes :

  • VITE_STRIPE_API_KEY
  • VITE_SERVER_BASE_URL
  • VITE_CLIENT_BASE_URL

Pour déployer l’application backend, faîtes exactement ce que nous avons fait pour le frontend, mais pour l’étape Construire l’environnement, sélectionnez le bouton radio Utiliser Dockerfile pour configurer l’image du conteneur et saisissez Dockerfile comme chemin d’accès à Dockerfile pour votre application backend.

Définition des détails de l'environnement de construction
Définition des détails de l’environnement de construction

N’oubliez pas d’ajouter les variables d’environnement de l’application backend :

  • CLIENT_BASE_URL
  • STRIPE_API_KEY

Une fois le déploiement terminé, rendez-vous sur la page de détails de vos applications et accèdez à l’URL du déploiement à partir de là.

L'URL hébergée des applications déployées sur Kinsta.
L’URL hébergée des applications déployées sur Kinsta.

Extrayez les URL des deux applications déployées. Dirigez-vous vers le tableau de bord Stripe pour obtenir vos clés d’API secrètes et publiables.

Assurez-vous de fournir la clé publiable Stripe à votre application frontend (et non la clé secrète). Assurez-vous également que vos URL de base n’ont pas de barre oblique avant (/) à la fin. Les itinéraires ont déjà des barres obliques avant, donc le fait d’avoir une barre oblique avant à la fin des URL de base entraînera l’ajout de deux barres obliques aux URL finales.

Pour votre application backend, ajoutez la clé secrète du tableau de bord Stripe (pas la clé publiable). Assurez-vous également que l’URL de votre client n’a pas de barre oblique avant (/) à la fin.

Une fois les variables ajoutées, allez dans l’onglet Déploiements de l’application et cliquez sur le bouton Redéployer pour votre application backend. Ceci complète la configuration unique dont vous avez besoin pour fournir à vos déploiements Kinsta des informations d’identification via les variables d’environnement.

Vous pouvez maintenant valider les modifications dans votre système de contrôle de version. Kinsta redéploiera automatiquement votre application si vous avez coché l’option lors du déploiement ; sinon, vous devez déclencher le redéploiement manuellement.

Résumé

Dans cet article, vous avez découvert le fonctionnement de Stripe et les flux de paiement qu’il propose. Vous avez également appris à travers un exemple détaillé comment intégrer Stripe dans votre application Java pour accepter des paiements ponctuels, mettre en place des abonnements, proposer des essais gratuits et générer des factures de paiement.

En utilisant Stripe et Java ensemble, vous pouvez offrir à vos clients une solution de paiement robuste qui peut évoluer et s’intégrer de façon transparente à votre écosystème d’applications et d’outils existants.

Vous utilisez Stripe dans votre application pour collecter des paiements ? Si oui, lequel des deux flux préfèrez-vous – hébergé, personnalisé ou dans l’application ? Faîtes-le nous savoir dans les commentaires ci-dessous !

Jeremy Holcombe Kinsta

Content & Marketing Editor at Kinsta, WordPress Web Developer, and Content Writer. Outside of all things WordPress, I enjoy the beach, golf, and movies. I also have tall people problems ;).