Con l’aumento delle transazioni digitali, la capacità di integrare perfettamente i gateway di pagamento è diventata un’abilità fondamentale per chi sviluppa. Che si tratti di marketplace o di prodotti SaaS, un processore di pagamenti è fondamentale per raccogliere ed elaborare i pagamenti degli utenti.

Questo articolo spiega come integrare Stripe in un ambiente Spring Boot, come impostare abbonamenti, offrire prove gratuite e creare pagine self-service per i vostri clienti per scaricare le fatture di pagamento.

Cos’è Stripe?

Stripe è una piattaforma di elaborazione dei pagamenti famosa in tutto il mondo e disponibile in 46 paesi. È un’ottima scelta se volete creare un’integrazione per pagamenti nella vostra applicazione web grazie alla sua ampia portata, alla sua fama e alla sua documentazione dettagliata.

Capire i concetti comuni di Stripe

È utile comprendere alcuni concetti comuni che Stripe usa per coordinare ed eseguire le operazioni di pagamento tra più parti. Stripe offre due approcci per implementare l’integrazione dei pagamenti nella vostra app.

Potete incorporare i moduli di Stripe all’interno della vostra app per un’esperienza in-app del cliente (Payment Intent) o reindirizzare i clienti a una pagina di pagamento ospitata da Stripe, dove Stripe gestisce il processo e fa sapere alla vostra app quando il pagamento va a buon fine o fallisce (Payment Link).

Payment Intent

Quando si gestiscono i pagamenti, è importante raccogliere i dettagli del cliente e del prodotto prima di richiedere i dati della carta e del pagamento. Questi dettagli comprendono la descrizione, l’importo totale, la modalità di pagamento e altro ancora.

Stripe vi chiede di raccogliere questi dati all’interno della vostra applicazione e di generare un oggetto PaymentIntent nel suo backend. Questo approccio consente a Stripe di formulare una richiesta di pagamento per quell’intento. Una volta concluso il pagamento, potete recuperare i dettagli del pagamento, compreso il suo scopo, attraverso l’oggetto PaymentIntent.

Payment Link

Per evitare le complessità dell’integrazione di Stripe direttamente nella vostra base di codice, considerate l’utilizzo di Stripe Checkouts come soluzione di pagamento in hosting. Come per la creazione di PaymentIntent, creerete un oggetto CheckoutSession con i dettagli del pagamento e del cliente. Invece di avviare un PaymentIntent in-app, il CheckoutSession genera un link di pagamento dove reindirizzare i clienti. Ecco come appare una pagina di pagamento ospitata:

Schermata della pagina di checkout ospitata da Stripe che mostra i dettagli della fattura a sinistra e il modulo di raccolta dei dati di pagamento a destra.
La pagina di pagamento ospitata da Stripe.

Dopo il pagamento, Stripe reindirizza alla vostra app, consentendo di svolgere attività successive al pagamento come conferme e richieste di consegna. Per garantire l’affidabilità, configura un webhook di backend per aggiornare Stripe, assicurando la conservazione dei dati di pagamento anche se i clienti chiudono accidentalmente la pagina dopo il pagamento.

Pur essendo efficace, questo metodo manca di flessibilità nella personalizzazione e nel design. Inoltre, può essere difficile da configurare correttamente per le applicazioni mobili, dove un’integrazione nativa sarebbe molto più semplice.

Chiavi API

Quando lavorate con l’API di Stripe, dovete avere accesso alle chiavi API per le vostre app client e server per interagire con il backend di Stripe. Potete accedere alle chiavi API di Stripe nella vostra bacheca per sviluppatori di Stripe. Ecco come si presenta:

Schermata della sezione sviluppatori della bacheca di Stripe che mostra la scheda API keys.
La bacheca di Stripe che mostra le chiavi API

Come funzionano i pagamenti con Stripe?

Per capire come funzionano i pagamenti in Stripe, dovete comprendere tutti gli attori coinvolti. In ogni transazione di pagamento sono coinvolti quattro soggetti:

  1. Customer: cliente, la persona che intende pagare un servizio/prodotto.
  2. Merchant: il titolare dell’attività, responsabile della ricezione dei pagamenti e della vendita di servizi/prodotti.
  3. Acquirer: una banca che elabora i pagamenti per conto vostro (merchant) e inoltra la richiesta di pagamento alle banche dei vostri clienti. Gli acquirer possono collaborare con una terza parte per elaborare i pagamenti.
  4. Issuing bank: la banca emittente che estende il credito ed emette carte e altri metodi di pagamento per i consumatori.

Ecco un tipico flusso di pagamenti tra questi soggetti a un livello molto alto.

Illustrazione di un flusso di lavoro di base che mostra come vengono gestiti i pagamenti online da parte del cliente, del merchant, dell'acquirer e della banca emittente.
Come funzionano i pagamenti online

Il cliente comunica all’esercente la sua intenzione di pagare. L’esercente inoltra i dettagli del pagamento alla propria banca acquirente, che raccoglie il pagamento dalla banca emittente del cliente e comunica all’esercente che il pagamento è andato a buon fine.

Questa è una panoramica di alto livello del processo di pagamento. In qualità di commercianti, dovete solo preoccuparvi di raccogliere l’intenzione di pagamento, trasmetterla al processore di pagamento e gestire il risultato del pagamento. Tuttavia, come già detto, potete procedere in due modi.

Quando create una sessione di checkout gestita da Stripe in cui Stripe si occupa della raccolta dei dati di pagamento, ecco come si presenta il flusso tipico:

Diagramma con il flusso di lavoro del pagamento del checkout ospitato da Stripe: mostra come il pagamento viene gestito tra il client, il server, l'API di Stripe e la pagina del checkout ospitato da Stripe.
Il flusso di pagamento del checkout gestito da Stripe. (Fonte: Documenti di Stripe)

Con i flussi di pagamento personalizzati, dipende tutto da voi. Potete progettare l’interazione tra il vostro client, il server, il cliente e l’API di Stripe in base alle esigenze della vostra applicazione. Potete aggiungere a questo flusso di lavoro la raccolta di indirizzi, la generazione di fatture, la cancellazione, le prove gratuite e così via, a seconda delle vostre esigenze.

Ora che avete capito come funzionano i pagamenti con Stripe, tutto è pronto per iniziare a integrarlo nella vostra applicazione Java.

Integrazione di Stripe nell’applicazione Spring Boot

Per iniziare l’integrazione di Stripe, create un’applicazione frontend per interagire con il backend Java e avviare i pagamenti. In questo tutorial, costruiremo un’applicazione React per attivare vari tipi di pagamento e abbonamenti in modo da acquisire una chiara comprensione dei loro meccanismi.

Nota: questo tutorial non si occuperà della costruzione di un sito di ecommerce completo, ma ha lo scopo principale di guidarvi attraverso il semplice processo di integrazione di Stripe in Spring Boot.

Impostazione dei progetti Frontend e Backend

Create una nuova cartella e un progetto React con Vite eseguendo il seguente comando:

npm create vite@latest

Impostate il nome del progetto come frontend (o qualsiasi altro nome preferiate), il framework come React e la variante come TypeScript. Navigate nella directory del progetto e installate Chakra UI per creare rapidamente l’impalcatura degli elementi dell’interfaccia utente eseguendo il seguente comando:

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

Installate anche react-router-dom nel vostro progetto per il routing lato client eseguendo il comando qui sotto:

npm i react-router-dom

Ora siamo pronti per iniziare a costruire l’applicazione frontend. Ecco la homepage che andremo a costruire.

La home page completata per l'applicazione frontend mostra un'intestazione e i pulsanti per accedere a tutte le pagine dell'applicazione.
La pagina iniziale completata per l’applicazione frontend.

Facendo clic su un pulsante qualsiasi di questa pagina, si accede a pagine di pagamento separate con moduli di pagamento. Per iniziare, create una nuova cartella denominata routes nella cartella frontend/src. All’interno di questa cartella, create un file Home.tsx. Questo file conterrà il codice del percorso home della vostra applicazione (/). Incollate il seguente codice nel file:

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

Per abilitare la navigazione nella vostra applicazione, aggiornate il file App.tsx per configurare la classe RouteProvider di 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

Eseguite il comando npm run dev per visualizzare l’anteprima della vostra applicazione su https://localhost:5173.

Questo completa la configurazione iniziale necessaria per l’applicazione frontend. Successivamente, create un’applicazione backend con Spring Boot. Per inizializzare l’applicazione, potete utilizzare il sito web di spring initializr (se il vostro IDE supporta la creazione di applicazioni Spring, non è necessario usare il sito web).

IntelliJ IDEA supporta la creazione di app Spring Boot. Iniziate scegliendo l’opzione New project su IntelliJ IDEA. Poi, scegliete Spring Initializr dal pannello di sinistra. Inserite i dettagli del progetto backend: nome (backend), posizione (directory stripe-payments-java), linguaggio (Java) e tipo (Maven). Per i nomi dei gruppi e degli artefatti, usate rispettivamente com.kinsta.stripe-java e backend.

La finestra di dialogo del nuovo progetto di IntelliJ IDEA mostra i dettagli compilati per il nuovo progetto.
La finestra di dialogo del nuovo progetto IDEA.

Fate clic sul pulsante Next. Quindi, aggiungete le dipendenze al vostro progetto scegliendo Spring Web dal menu a tendina Web nel riquadro delle dipendenze e fate clic sul pulsante Create.

La procedura guidata per il nuovo progetto di IntelliJ IDEA mostra le dipendenze che l'utente ha scelto di aggiungere alla sua nuova applicazione.
La scelta delle dipendenze.

Questo creerà il progetto Java e lo aprirà nel vostro IDE. Ora potete procedere con la creazione dei vari flussi di pagamento utilizzando Stripe.

Accettare pagamenti online per l’acquisto di prodotti

La funzionalità più importante e più utilizzata di Stripe è l’accettazione di pagamenti una tantum da parte dei clienti. In questa sezione scoprirete due modi per integrare l’elaborazione dei pagamenti nella vostra app con Stripe.

Checkout ospitato

Costruite una pagina di checkout che attiva un flusso di lavoro di checkout ospitato in cui il pagamento viene attivato solo dalla vostra applicazione frontend. Stripe si occupa di raccogliere i dati della carta del cliente e di incassare il pagamento, condividendo solo alla fine il risultato dell’operazione di pagamento.

Ecco come appare la pagina di pagamento:

La pagina di checkout in hosting completata.
La pagina di checkout in hosting completata.

Questa pagina ha tre componenti principali: CartItem, che rappresenta ogni articolo del carrello; TotalFooter, che visualizza l’importo totale; CustomerDetails, che raccoglie i dati del cliente. Potete riutilizzare questi componenti per creare moduli di pagamento per altri scenari illustrati in questo articolo, come il pagamento integrato e gli abbonamenti.

Costruire il frontend

Create una cartella components nella cartella frontend/src. Nella cartella components, create un nuovo file CartItem.tsx e incollate il seguente codice:

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

Il codice precedente definisce due interfacce da usare come tipi per le proprietà passate al componente. Il tipo ItemData viene esportato per essere riutilizzato in altri componenti.

Il codice restituisce il layout di un componente articolo del carrello. Utilizza i props forniti per rendere l’articolo sullo schermo.

Quindi, create un file TotalFooter.tsx nella cartella components e incollate il seguente codice:

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

Il componente TotalFooter visualizza il valore totale del carrello e utilizza il valore mode per rendere condizionatamente un testo specifico.

Infine, create il componente CustomerDetails.tsx e incollate il seguente codice:

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

Il codice precedente visualizza un modulo con due campi di input per raccogliere il nome e l’email dell’utente. Quando si fa clic sul pulsante Checkout, viene invocato il metodo initiatePayment per inviare la richiesta di checkout al backend.

Richiede l’endpoint che avete passato al componente e invia le informazioni del cliente e gli articoli del carrello come parte della richiesta, quindi reindirizza l’utente all’URL ricevuto dal server. Questo URL condurrà l’utente a una pagina di pagamento ospitata sul server di Stripe. La creazione di questo URL avverrà nel giro di poco.

Nota: questo componente usa la variabile d’ambiente VITE_SERVER_BASE_URL per l’URL del server backend. Impostatela creando un file .env nella root del vostro progetto:

VITE_SERVER_BASE_URL=http://localhost:8080

Tutti i componenti sono stati creati. Ora procediamo a costruire il percorso di checkout hosted utilizzando i componenti. Per farlo, create un nuovo file HostedCheckout.tsx nella cartella routes con il seguente codice:

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

Questo percorso usa i tre componenti che avete appena costruito per assemblare una schermata di checkout. Tutte le modalità dei componenti sono configurate come checkout e l’endpoint /checkout/hosted è fornito al componente del modulo per avviare con precisione la richiesta di checkout.

Il componente usa un oggetto Products per riempire l’array degli articoli. Negli scenari reali, questi dati provengono dall’API del carrello e contengono gli articoli selezionati dall’utente. Tuttavia, per questo tutorial, un elenco statico proveniente da uno script popola l’array. Definite l’array creando un file data.ts nella root del vostro progetto frontend e inserendovi il seguente codice:

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"
	},
]

Questo file definisce due elementi dell’array di prodotti che vengono visualizzati nel carrello. Potete modificare liberamente i valori dei prodotti.

Come ultimo passo della creazione del frontend, create due nuovi percorsi per gestire il successo e il fallimento. La pagina di pagamento ospitata da Stripe reindirizzerà gli utenti verso la vostra applicazione attraverso questi due percorsi in base al risultato della transazione. Stripe fornirà alle vostre rotte anche il payload relativo alla transazione, come l’ID della sessione di checkout, che potrete usare per recuperare l’oggetto della sessione di checkout corrispondente e accedere ai dati relativi al checkout come il metodo di pagamento, i dettagli della fattura, ecc.

Per farlo, create un file Success.tsx nella cartella src/routes e inserite il seguente codice:

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

Al momento del rendering, questo componente mostra il messaggio “Success!” e stampa sullo schermo i parametri della query URL. Include anche un pulsante per reindirizzare gli utenti alla homepage dell’applicazione.

Quando si realizzano applicazioni reali, questa pagina è il luogo in cui si gestiscono le transazioni non critiche sul lato dell’applicazione che dipendono dal successo della transazione in questione. Per esempio, se state creando una pagina di checkout per un negozio online, potreste usare questa pagina per mostrare una conferma all’utente e un’ora di consegna dei prodotti acquistati.

Quindi, create un file Failure.tsx con il seguente codice:

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

Questo componente è simile a quello di Success.tsx e visualizza il messaggio “Failure!” quando viene reso.

Per le operazioni essenziali come la consegna dei prodotti, l’invio di email o qualsiasi altra parte critica del flusso di acquisto, usate i webhook. I webhook sono percorsi API sul vostro server che Stripe può invocare quando si verifica una transazione.

Il webhook riceve tutti i dettagli della transazione (tramite l’oggetto CheckoutSession ), consentendovi di registrarla nel database della vostra applicazione e di attivare i relativi flussi di lavoro di successo o fallimento. Poiché il vostro server è sempre accessibile a Stripe, non viene persa nessuna transazione, assicurando una funzionalità costante del vostro negozio online.

Infine, aggiornate il file App.tsx in modo che assomigli a questo:

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

In questo modo i componenti Success e Failure saranno resi rispettivamente sui percorsi /success e /failure.

Questo completa la configurazione del frontend. Successivamente, configurate il backend per creare l’endpoint /checkout/hosted.

Creare il backend

Aprite il progetto del backend e installate l’SDK di Stripe aggiungendo le seguenti righe nell’array delle dipendenze del file pom.xml:

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

Successivamente, caricate le modifiche di Maven nel progetto per installare le dipendenze. Se il vostro IDE non supporta questa operazione tramite l’interfaccia utente, eseguite il comando maven dependency:resolve o maven install. Se non disponete della CLI maven, usate il wrapper mvnw di Spring initializr quando create il progetto.

Una volta installate le dipendenze, create un nuovo controller REST per gestire le richieste HTTP in arrivo per la vostra applicazione backend. A tal fine, create un file PaymentController.java nella cartella src/main/java/com/kinsta/stripe-java/backend e aggiungete il seguente codice:

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!";
	}

}

Il codice sopra riportato importa le dipendenze essenziali di Stripe e crea la classe PaymentController. Questa classe contiene due annotazioni: @RestController e @CrossOrigin. L’annotazione @RestController indica a Spring Boot di trattare questa classe come un controller e i suoi metodi possono ora usare le annotazioni @Mapping per gestire le richieste HTTP in arrivo.

L’annotazione @CrossOrigin contrassegna tutti gli endpoint definiti in questa classe come aperti a tutte le origini secondo le regole CORS. Tuttavia, questa pratica è sconsigliata in produzione a causa delle potenziali vulnerabilità di sicurezza dei vari domini internet.

Per ottenere risultati ottimali, è consigliabile ospitare i server backend e frontend sullo stesso dominio per aggirare i problemi CORS. In alternativa, se questo non è possibile, potete specificare il dominio del vostro client frontend (che invia le richieste al server backend) usando l’annotazione @CrossOrigin, in questo modo:

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

La classe PaymentController estrarrà la chiave API di Stripe dalle variabili d’ambiente per fornirla successivamente all’SDK di Stripe. Quando eseguite l’applicazione, dovete fornire la vostra chiave API di Stripe all’applicazione attraverso le variabili d’ambiente.

A livello locale, potete creare una nuova variabile d’ambiente nel vostro sistema in modo temporaneo (aggiungendo una frase KEY=VALUE prima del comando utilizzato per avviare il vostro server di sviluppo) o permanente (aggiornando i file di configurazione del vostro terminale o impostando una variabile d’ambiente nel pannello di controllo di Windows).

Negli ambienti di produzione, il vostro provider di distribuzione (come Kinsta) vi fornirà un’opzione separata per inserire le variabili d’ambiente utilizzate dalla vostra applicazione.

Se usate IntelliJ IDEA (o un IDE simile), fate clic su Run Configurations in alto a destra dell’IDE e su Edit Configurations… dall’elenco a discesa che si apre per aggiornare il comando di esecuzione e impostare la variabile d’ambiente.

La finestra di IntelliJ IDEA mostra da dove accedere all'impostazione delle configurazioni di run/debug.
Apertura della finestra di dialogo delle configurazioni di run/debug.

Si aprirà una finestra di dialogo in cui potrete inserire le variabili d’ambiente per la vostra applicazione tramite il campo Environment variables. Inserite la variabile d’ambiente STRIPE_API_KEY nel formato VAR1=VALUE. Potete trovare la vostra chiave API sul sito web di Stripe Developers. Dovete fornire il valore della chiave segreta da questa pagina.

 La bacheca di Stripe con una freccia che indica dove cercare le chiavi API. Le chiavi sono oscurate per nascondere le informazioni sensibili.
La bacheca di Stripe che mostra le chiavi API.

Se non l’avete ancora fatto, create un nuovo account Stripe per avere accesso alle chiavi API.

Una volta impostata la chiave API, procedete alla creazione dell’endpoint. Questo endpoint raccoglierà i dati del cliente (nome ed email), creerà un profilo del cliente in Stripe se non esiste già e creerà una sessione di checkout per consentire agli utenti di pagare gli articoli del carrello.

Ecco come si presenta il codice del metodo 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();
	}

Quando costruisce la sessione di pagamento, il codice utilizza il nome del prodotto ricevuto dal cliente, ma non utilizza i dettagli del prezzo della richiesta. Questo approccio evita una potenziale manipolazione dei prezzi da parte del cliente, in cui attori malintenzionati potrebbero inviare prezzi ridotti nella richiesta di acquisto per pagare meno prodotti e servizi.

Per evitare ciò, il metodo hostedCheckout interroga il database dei prodotti (tramite ProductDAO) per recuperare il prezzo corretto dell’articolo.

Inoltre, Stripe offre diverse classi Builder che seguono il modello di progettazione del builder. Queste classi aiutano a creare oggetti parametro per le richieste di Stripe. Il frammento di codice fornito fa riferimento anche alle variabili d’ambiente per recuperare l’URL dell’applicazione client. Questo URL è necessario affinché l’oggetto della sessione di checkout venga reindirizzato in modo appropriato dopo un pagamento riuscito o fallito.

Per eseguire questo codice, impostate l’URL dell’applicazione client tramite le variabili d’ambiente, in modo simile a come è stata fornita la chiave API di Stripe. Poiché l’applicazione client viene eseguita tramite Vite, l’URL dell’applicazione locale deve essere http://localhost:5173. Includetelo nelle variabili d’ambiente attraverso l’IDE, il terminale o il pannello di controllo del sistema.

CLIENT_BASE_URL=http://localhost:5173

Fornite poi all’app un indirizzo ProductDAO da cui consultare i prezzi dei prodotti. Il Data Access Object (DAO) interagisce con le fonti di dati (come i database) per accedere ai dati relativi all’applicazione. Sebbene la creazione di un database di prodotti esuli dallo scopo di questo tutorial, una semplice implementazione che potete fare è aggiungere un nuovo file ProductDAO.java nella stessa directory di PaymentController.java e incollare il seguente codice:

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();

	}
}

Questo inizializzerà un array di prodotti e vi permetterà di interrogare i dati dei prodotti usando il loro identificatore (ID). Dovrete anche creare un DTO (Data Transfer Object) per permettere a Spring Boot di serializzare automaticamente il payload in arrivo dal client e presentarvi un semplice oggetto per accedere ai dati. Per farlo, create un nuovo file RequestDTO.java e incollate il seguente codice:

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;
	}

}

Questo file definisce un POJO che contiene il nome del cliente, l’email e l’elenco degli articoli che sta acquistando.

Infine, implementate il metodo CustomerUtil.findOrCreateCustomer() per creare l’oggetto Cliente in Stripe se non esiste già. Per farlo, create un file con il nome CustomerUtil e aggiungeteci il seguente codice:

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;
	}
}

Questa classe contiene anche un altro metodo findCustomerByEmail che vi permette di cercare i clienti in Stripe usando i loro indirizzi email. L’API Customer Search viene utilizzata per cercare i record dei clienti nel database di Stripe e l’API Customer Create viene utilizzata per creare i record dei clienti quando necessario.

Questo completa la configurazione del backend necessaria per il flusso di pagamento in hosting. Ora potete testare l’applicazione eseguendo le applicazioni frontend e backend nei rispettivi IDE o terminali separati. Ecco come si presenterebbe il flusso di successo:

Un flusso utente che mostra l'aspetto di un checkout riuscito con successo utilizzando la pagina di Stripe hosted.
Un flusso di checkout hosted terminato con successo.

Per testare le integrazioni con Stripe, potete sempre usare i seguenti dati della carta per simulare le transazioni con la carta:

Numero di carta: 4111 1111 1111 1111
Mese e anno di scadenza: 12 / 25
CVV: qualsiasi numero di tre cifre
Nome sulla carta: Qualsiasi nome

Se scegliete di annullare la transazione invece di pagare, ecco come si presenta il flusso di errore:

Un flusso utente che mostra come si presenta un checkout fallito utilizzando la pagina di Stripe hosted.
Un flusso di pagamento hosted fallito.

Questo completa la configurazione di un’esperienza di checkout ospitata da Stripe e integrata nella vostra app. Potete consultare la documentazione di Stripe per saperne di più su come personalizzare la pagina di checkout, raccogliere ulteriori dettagli dal cliente e altro ancora.

Checkout integrato

Un’esperienza di checkout integrato si riferisce alla creazione di un flusso di pagamento che non reindirizza gli utenti all’esterno della vostra applicazione (come nel caso del flusso di pagamento ospitato) e rende il modulo di pagamento all’interno della vostra app.

Costruire un’esperienza di pagamento integrata significa gestire i dati di pagamento dei clienti, che comportano informazioni sensibili come i numeri di carta di credito, l’ID di Google Pay, ecc. Non tutte le app sono progettate per gestire questi dati in modo sicuro.

Per eliminare l’onere di rispettare standard come il PCI-DSS, Stripe fornisce elementi che potete usare all’interno dell’app per raccogliere i dati di pagamento, lasciando che Stripe gestisca la sicurezza ed elabori i pagamenti in modo sicuro.

Creare il frontend

Per iniziare, installate lo SDK Stripe React nella vostra applicazione frontend per accedere agli elementi Stripe eseguendo il seguente comando nella directory del frontend:

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

Successivamente, create un nuovo file chiamato IntegratedCheckout.tsx nella cartella frontend/src/routes e salvate il seguente codice:

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

Questo file definisce due componenti, IntegratedCheckout e CheckoutForm. CheckoutForm definisce un semplice modulo con un PaymentElement di Stripe che raccoglie i dati di pagamento dei clienti e un pulsante Pay che attiva una richiesta di incasso.

Questo componente richiama anche gli hook useStripe() e useElements() per creare un’istanza dell’SDK di Stripe che potete utilizzare per creare richieste di pagamento. Una volta fatto clic sul pulsante Pay, viene richiamato il metodo stripe.confirmPayment() dell’SDK di Stripe che raccoglie i dati di pagamento dell’utente dall’istanza degli elementi e li invia al backend di Stripe con un URL di successo a cui reindirizzare se la transazione è andata a buon fine.

Il modulo di checkout è stato separato dal resto della pagina perché gli hook useStripe() e useElements() devono essere richiamati dal contesto di un provider Elements, cosa che è stata fatta nella dichiarazione di ritorno di IntegratedCheckout. Se spostassimo le chiamate all’hook di Stripe direttamente nel componente IntegratedCheckout, queste sarebbero al di fuori dell’ambito del provider Elements e quindi non funzionerebbero.

Il componente IntegratedCheckout riusa i componenti CartItem e TotalFooter per visualizzare gli articoli del carrello e l’importo totale. Inoltre, visualizza due campi di input per raccogliere le informazioni del cliente e un pulsante Initiate payment che invia una richiesta al server backend Java per creare la chiave segreta del cliente tramite i dati del cliente e del carrello. Una volta ricevuta la chiave segreta del cliente, viene visualizzato il sito CheckoutForm, che gestisce la raccolta dei dati di pagamento del cliente.

Inoltre, useEffect viene utilizzato per chiamare il metodo loadStripe. Questo effetto viene eseguito solo una volta durante il rendering del componente, in modo che l’SDK di Stripe non venga caricato più volte quando gli stati interni del componente vengono aggiornati.

Per eseguire il codice di cui sopra, dovrete anche aggiungere due nuove variabili d’ambiente al vostro progetto frontend: VITE_STRIPE_API_KEY e VITE_CLIENT_BASE_URL. La variabile Stripe API key conterrà la chiave API pubblicabile dalla bacheca di Stripe, mentre la variabile client base URL conterrà il link all’applicazione client (che è l’applicazione frontend stessa) in modo da poterlo passare all’SDK di Stripe per gestire i reindirizzamenti di successo e fallimento.

Per farlo, aggiungete il seguente codice al vostro file .env nella directory di frontend:

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

Infine, aggiornate il file App.tsx per includere il componente IntegratedCheckout nel percorso /integrated-checkout dell’applicazione frontend. Aggiungete il seguente codice nell’array passato alla chiamata createBrowserRouter nel componente App:

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

Questo completa la configurazione necessaria sul frontend. Successivamente, create una nuova route sul vostro server backend che crei la chiave segreta del cliente necessaria per gestire le sessioni di checkout integrate nell’applicazione frontend.

Creare il backend

Per garantire che l’integrazione del frontend non venga sfruttata da malintenzionati (dato che il codice del frontend è più facile da decifrare rispetto a quello del backend), Stripe vi chiede di generare un client secret univoco sul vostro server backend e di verificare ogni richiesta di pagamento integrata con il client secret generato sul backend per assicurarsi che sia effettivamente la vostra app a cercare di raccogliere i pagamenti. Per fare ciò, dovete impostare un altro percorso nel backend che crei i segreti del cliente in base alle informazioni del cliente e del carrello.

Per creare la chiave segreta del cliente sul vostro server, create un nuovo metodo nella vostra classe PaymentController con il nome integratedCheckout e inserite il seguente codice:

@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();
	}

Analogamente a come la sessione di pagamento è stata costruita utilizzando una classe costruttore che accetta la configurazione per la richiesta di pagamento, il flusso di pagamento integrato richiede la creazione di una sessione di pagamento con l’importo, la valuta e i metodi di pagamento. A differenza della sessione di checkout, non è possibile associare voci di spesa a una sessione di pagamento a meno che non si crei una fattura, cosa che imparerete in una sezione successiva del tutorial.

Dato che non state passando le righe al costruttore della sessione di pagamento, dovete calcolare manualmente l’importo totale degli articoli del carrello e inviarlo al backend di Stripe. Usate il vostro ProductDAO per trovare e aggiungere i prezzi di ogni prodotto nel carrello.

Per farlo, definite un nuovo metodo calculateOrderAmount e aggiungeteci il seguente codice:

 	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);
	}

Questo dovrebbe essere sufficiente per configurare il flusso di pagamento integrato sia sul frontend che sul backend. Potete riavviare i server di sviluppo per il server e il client e provare il nuovo flusso di pagamento integrato nell’applicazione frontend. Ecco come apparirà il flusso integrato:

Un flusso utente che mostra come si svolge un checkout integrato di successo utilizzando l'integrazione con Stripe.
Un flusso di pagamento integrato.

Questo completa un flusso di pagamento integrato di base nella vostra applicazione. Ora potete esplorare ulteriormente la documentazione di Stripe per personalizzare i metodi di pagamento o integrare altri componenti per aiutarvi con altre operazioni come la raccolta degli indirizzi, le richieste di pagamento, l’integrazione dei link e altro ancora!

Impostare gli abbonamenti per i servizi ricorrenti

Un’offerta molto diffusa nei negozi online è l’abbonamento. Sia che stiate creando un mercato di servizi o che stiate offrendo periodicamente un prodotto digitale, l’abbonamento è la soluzione perfetta per dare ai vostri clienti l’accesso periodico al vostro servizio a un prezzo ridotto rispetto all’acquisto una tantum.

Stripe può aiutarvi a impostare e cancellare facilmente gli abbonamenti. Potete anche offrire delle prove gratuite come parte del vostro abbonamento, in modo che gli utenti possano provare la vostra offerta prima di impegnarsi.

Impostazione di un nuovo abbonamento

L’impostazione di un nuovo abbonamento è semplice grazie al flusso di pagamento ospitato. Dovrete modificare solo alcuni parametri nella creazione della richiesta di pagamento e creare una nuova pagina (riutilizzando i componenti esistenti) per mostrare una pagina di pagamento per un nuovo abbonamento. Per iniziare, create un file NewSubscription.tsx nella cartella dei componenti del frontend. Incollate il seguente codice:

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

Nel codice qui sopra, i dati del carrello sono presi dal file data.ts e contengono solo un articolo per semplificare il processo. Negli scenari reali, potete avere più articoli come parte di un ordine di abbonamento.

Per rendere questo componente sul percorso giusto, aggiungete il seguente codice nell’array passato alla chiamata createBrowserRouter nel componente App.tsx:

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

Questo completa la configurazione necessaria sul frontend. Nel backend, create una nuova route /subscription/new per creare una nuova sessione di pagamento in hosting per un prodotto in abbonamento. Create un metodo newSubscription nella cartella backend/src/main/java/com/kinsta/stripejava/backend e salvate il seguente codice:

@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();
	}

Il codice di questo metodo è molto simile a quello del metodo hostedCheckout, tranne per il fatto che la modalità impostata per la creazione della sessione è l’abbonamento anziché il prodotto e che prima di creare la sessione viene impostato un valore per l’intervallo di ricorrenza dell’abbonamento.

Ciò indica a Stripe di trattare questo checkout come un checkout di un abbonamento invece che di un pagamento unico. Come il metodo hostedCheckout, anche questo metodo restituisce l’URL della pagina di checkout ospitata come risposta HTTP al cliente. Il cliente deve reindirizzarsi all’URL ricevuto, consentendo al cliente di completare il pagamento.

Potete riavviare i server di sviluppo sia per il client che per il server e vedere la nuova pagina di sottoscrizione in azione. Ecco come appare:

Un flusso utente che mostra l'aspetto di un checkout di abbonamento riuscito utilizzando la pagina ospitata da Stripe.
Flusso di pagamento hosted di un abbonamento.

Annullare un abbonamento esistente

Ora che sapete come creare nuovi abbonamenti, scopriamo come consentire ai vostri clienti di annullare gli abbonamenti esistenti. Dal momento che l’applicazione dimostrativa realizzata in questo tutorial non contiene alcuna configurazione di autenticazione, usate un modulo per consentire al cliente di inserire la propria email per cercare i propri abbonamenti e poi fornite a ogni voce di abbonamento un pulsante di cancellazione per consentire all’utente di annullarlo.

Per fare ciò, dovrete procedere come segue:

  1. Aggiornate il componente CartItem per mostrare un pulsante di annullamento nella pagina di annullamento degli abbonamenti.
  2. Create un componente CancelSubscription che mostri prima un campo di input e un pulsante per consentire al cliente di cercare gli abbonamenti utilizzando il suo indirizzo email e poi renda un elenco di abbonamenti utilizzando il componente CartItem aggiornato.
  3. Create un nuovo metodo nel server backend in grado di cercare le sottoscrizioni dal backend di Stripe utilizzando l’indirizzo email del cliente.
  4. Create un nuovo metodo nel server backend che possa cancellare un abbonamento in base all’ID dell’abbonamento che gli è stato passato.

Iniziate aggiornando il componente CartItem in modo che abbia questo aspetto:

// 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

Quindi, create un componente CancelSubscription.tsx nella cartella routes del vostro frontend e salvate il seguente codice al suo interno:

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

Questo componente visualizza un campo di input e un pulsante per consentire ai clienti di inserire la propria email e iniziare a cercare gli abbonamenti. Se gli abbonamenti vengono trovati, il campo di input e il pulsante vengono nascosti e sullo schermo viene visualizzato un elenco di abbonamenti. Per ogni abbonamento, il componente passa un metodo removeSubscription che richiede al server backend Java di annullare l’abbonamento sul backend di Stripe.

Per collegarlo alla route /cancel-subscription della vostra applicazione frontend, aggiungete il seguente codice nell’array passato alla chiamata createBrowserRouter nel componente App:

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

Per cercare gli abbonamenti sul server backend, aggiungete un metodo viewSubscriptions nella classe PaymentController del vostro progetto backend con il seguente contenuto:

@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;
	}

Il metodo qui sopra trova innanzitutto l’oggetto cliente per l’utente dato in Stripe. Poi cerca gli abbonamenti attivi del cliente. Una volta ricevuto l’elenco degli abbonamenti, ne estrae gli articoli e trova i prodotti corrispondenti nel database dei prodotti dell’app da inviare al frontend. Questo è importante perché l’ID con cui il frontend identifica ogni prodotto nel database dell’app può coincidere o meno con l’ID del prodotto memorizzato in Stripe.

Infine, create un metodo cancelSubscription< nella classe PaymentController e incollare il codice sottostante per eliminare un abbonamento in base all’ID dell’abbonamento passato.

@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();
	}

Questo metodo recupera l’oggetto abbonamento da Stripe, richiama il metodo cancel e restituisce lo stato dell’abbonamento al cliente. Tuttavia, per poterlo eseguire, dovete aggiornare il tuo oggetto DTO per aggiungere il campo subscriptionId. Per farlo, aggiungete il seguente campo e metodo nella 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;
	}

}

Una volta aggiunti, potete eseguire nuovamente il server di sviluppo sia per l’applicazione backend che per quella frontend e vedere il flusso di annullamento in azione:

Un flusso utente che mostra come si presenta una cancellazione dell'abbonamento effettuata con successo utilizzando la pagina ospitata da Stripe.
Un flusso di cancellazione di un abbonamento.

Impostazione di prove gratuite per abbonamenti con transazioni a valore zero

Una caratteristica comune alla maggior parte degli abbonamenti moderni è quella di offrire un breve periodo di prova gratuito prima di addebitare il costo all’utente. In questo modo gli utenti possono esplorare il prodotto o il servizio senza investire in esso. Tuttavia, è meglio memorizzare i dati di pagamento del cliente durante l’iscrizione alla prova gratuita, in modo da poterli addebitare facilmente non appena la prova termina.

Stripe semplifica notevolmente la creazione di questi abbonamenti. Per iniziare, create un nuovo componente nella cartella frontend/routes chiamato SubscriptionWithTrial.tsx e incollate il seguente codice:

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

Questo componente riutilizza i componenti creati in precedenza. La differenza principale tra questo e il componente NewSubscription è che passa la modalità di TotalFooter come trial invece che come subscription. Questo fa sì che il componente TotalFooter visualizzi un testo in cui si dice che il cliente può iniziare subito la prova gratuita ma che gli verrà addebitato il costo dopo un mese.

Per collegare questo componente alla route /subscription-with-trial nella vostra applicazione frontend, aggiungete il seguente codice nell’array passato alla chiamata createBrowserRouter nel componente App:

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

Per creare il flusso di pagamento per gli abbonamenti con trial nel backend, create un nuovo metodo chiamato newSubscriptionWithTrial nella classe PaymentController e aggiungete il seguente codice:

	@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();
	}

Questo codice è molto simile a quello del metodo newSubscription. L’unica (e più importante) differenza è che un periodo di prova viene passato all’oggetto di creazione dei parametri della sessione con il valore 30, che indica un periodo di prova gratuito di 30 giorni.

Ora potete salvare le modifiche e rieseguire il server di sviluppo per il backend e il frontend per vedere il flusso di lavoro dell’abbonamento con prova gratuita in azione:

Un flusso utente che mostra l'esito positivo del checkout di un abbonamento con aggiunta di una prova gratuita utilizzando la pagina ospitata da Stripe.
Un flusso di abbonamento con prova gratuita.

Generare fatture per i pagamenti

Per gli abbonamenti, Stripe genera automaticamente le fatture per ogni pagamento, anche se si tratta di una transazione a valore zero per l’iscrizione alla prova. Per i pagamenti una tantum, potete scegliere di creare le fatture se necessario.

Per iniziare ad associare tutti i pagamenti alle fatture, aggiornate il corpo del payload inviato nella funzione initiatePayment del componente CustomerDetails nell’applicazione frontend in modo che contenga la seguente proprietà:

invoiceNeeded: true

Dovrete aggiungere questa proprietà anche nel corpo del payload inviato al server nella funzione createTransactionSecret del componente IntegratedCheckout.

Poi aggiornate le route del backend per verificare la presenza di questa nuova proprietà e aggiornate le chiamate all’SDK di Stripe di conseguenza.

Per il metodo di pagamento in hosting, per aggiungere la funzionalità di fatturazione, aggiornate il metodo hostedCheckout aggiungendo le seguenti righe di codice:

@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();
	}

Questo controllerà la presenza del campo invoiceNeeded e imposterà i parametri di creazione di conseguenza.

Aggiungere una fattura a un pagamento integrato è un po’ complicato. Non potete semplicemente impostare un parametro per indicare a Stripe di creare automaticamente una fattura con il pagamento. Dovete creare manualmente la fattura e poi un intento di pagamento collegato.

Se l’intento di pagamento viene pagato e completato con successo, la fattura viene contrassegnata come pagata; altrimenti, la fattura rimane non pagata. Sebbene tutto ciò abbia un senso logico, può essere un po’ complesso da implementare (soprattutto quando non ci sono esempi chiari o riferimenti da seguire).

Per implementarlo, aggiornate il metodo integratedCheckout in modo che assomigli a questo:

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();
	}

Il vecchio codice di questo metodo viene spostato nel blocco if che controlla se il campo invoiceNeeded è false. Se è vero, il metodo ora crea una fattura con le voci della fattura e la segna come finalizzata in modo che possa essere pagata.

Quindi, recupera l’intento di pagamento creato automaticamente quando la fattura è stata finalizzata e invia il segreto del cliente da questo intento di pagamento al cliente. Quando il cliente completa il flusso di pagamento integrato, il pagamento viene riscosso e la fattura viene contrassegnata come pagata.

Questo completa la configurazione necessaria per iniziare a generare fatture dalla vostra applicazione. Potete visitare la sezione fatture della vostra bacheca di Stripe per vedere le fatture che la vostra applicazione genera per ogni acquisto e pagamento di un abbonamento.

Tuttavia, Stripe vi permette anche di accedere alle fatture tramite le sue API per creare un’esperienza self-service che permetta ai clienti di scaricare le fatture ogni volta che lo desiderano.

Per farlo, create un nuovo componente nella cartella frontend/routes chiamato ViewInvoices.tsx. Incollate il seguente codice:

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

In modo simile a CancelSubscription, questo componente mostra un campo di input per il cliente che può inserire la propria email e un pulsante per cercare le fatture. Una volta trovate le fatture, il campo di inserimento e il pulsante vengono nascosti e al cliente viene mostrato un elenco di fatture con il numero di fattura, l’importo totale e un pulsante per scaricare il PDF della fattura.

Per implementare il metodo di backend che cerca le fatture di un determinato cliente e invia le informazioni pertinenti (numero di fattura, importo e URL del PDF), aggiungete il seguente metodo nella vostra classe PaymentController nel 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;
	}

Il metodo cerca innanzitutto il cliente in base all’indirizzo email fornito. Poi cerca le fatture di questo cliente che sono contrassegnate come pagate. Una volta trovato l’elenco delle fatture, estrae il numero della fattura, l’importo e l’URL del PDF e invia un elenco di queste informazioni all’applicazione del cliente.

Ecco come si presenta il flusso delle fatture:

Un flusso utente che mostra come recuperare e accedere alle fatture per un utente.
Visualizzazione delle fatture

Questo completa lo sviluppo della nostra applicazione Java dimostrativa (frontend e backend). Nella prossima sezione scoprirete come distribuire l’applicazione su Kinsta per potervi accedere online.

Distribuzione dell’applicazione su Kinsta

Una volta che la vostra applicazione è pronta, potete distribuirla su Kinsta. Kinsta supporta le distribuzioni dal vostro provider Git preferito (Bitbucket, GitHub o GitLab). Collegando i repository del codice sorgente della vostra applicazione a Kinsta, l’applicazione viene distribuita automaticamente ogni volta che viene apportata una modifica al codice.

Preparate i vostri progetti

Per distribuire le vostre app in produzione, individuate i comandi di compilazione e distribuzione che Kinsta userà. Per il frontend, assicuratevi che il file package.json contenga i seguenti script:

"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"
  },

Dovrete anche installare il pacchetto serve npm che vi permette di servire siti web statici. Questo pacchetto verrà utilizzato per servire la build di produzione della vostra applicazione dall’ambiente di distribuzione di Kinsta. Potete installarlo eseguendo il seguente comando:

npm i serve

Una volta creata la vostra applicazione, l’intera applicazione verrà impacchettata in un unico file, index.html, poiché la configurazione di React che state utilizzando in questo tutorial è pensata per creare applicazioni a pagina singola. Anche se questo non fa una grande differenza per i vostri utenti, dovete impostare alcune configurazioni extra per gestire il routing e la navigazione del browser in queste applicazioni.

Con la configurazione attuale, è possibile accedere all’applicazione solo dall’URL di base dell’installazione client. Se l’URL di base dell’installazione è example.com, qualsiasi richiesta a example.com/some-route porterà a errori HTTP 404.

Questo perché il vostro server ha un solo file da servire, il file index.html. Una richiesta inviata a example.com/some-route inizierà a cercare il file some-route/index.html, che non esiste; di conseguenza riceverà una risposta 404 Not Found.

Per risolvere questo problema, create un file chiamato serve.json nella cartella frontend/public e salvate in esso il seguente codice:

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

Questo file istruirà serve a riscrivere tutte le richieste in arrivo per indirizzarle al file index.html, mostrando comunque il percorso a cui è stata inviata la richiesta originale nella risposta. Questo vi aiuterà a servire correttamente le pagine di successo e di fallimento della vostra applicazione quando Stripe reindirizzerà i vostri clienti alla vostra applicazione.

Per il backend, create un Dockerfile per creare l’ambiente giusto per la vostra applicazione Java. L’uso di un Dockerfile assicura che l’ambiente fornito alla vostra applicazione Java sia lo stesso su tutti gli host (sia quello di sviluppo locale che quello di distribuzione di Kinsta) e che voi possiate assicurarvi che l’applicazione funzioni come previsto.

Per fare ciò, create un file chiamato Dockerfile nella cartella del backend e salvate i seguenti contenuti:

FROM openjdk:22-oraclelinux8

LABEL maintainer="krharsh17"

WORKDIR /app

COPY . /app

RUN ./mvnw clean package

EXPOSE 8080

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

Questo file indica al runtime di usare l’immagine Java OpenJDK come base per il contenitore di distribuzione, di eseguire il comando ./mvnw clean package per creare il file JAR della vostra applicazione e di usare il comando java -jar <jar-file> per eseguirlo. Questo completa la preparazione del codice sorgente per il deployment su Kinsta.

Configurare i repository GitHub

Per iniziare a distribuire le applicazioni, create due repository GitHub per ospitare il codice sorgente delle vostre applicazioni. Se usate la GitHub CLI, potete farlo tramite il terminale eseguendo i seguenti comandi:

# 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

Questo dovrebbe creare nuovi repository GitHub nel vostro account e inviarvi il codice delle vostre applicazioni. Dovreste essere in grado di accedere ai repository frontend e backend. Quindi, distribuite questi repository su Kinsta seguendo i seguenti passaggi:

  1. Accedete o create il vostro account Kinsta nel cruscotto MyKinsta.
  2. Nella barra laterale di sinistra, fate clic su Applicazioni e poi su Aggiungi applicazione.
  3. Nella finestra di dialogo che appare, scegliete il repository che volete distribuire. Se avete più branch, potete selezionare il branch desiderato e dare un nome alla vostra applicazione.
  4. Selezionate uno dei data center disponibili dall’elenco delle opzioni 25. Kinsta rileva automaticamente il comando di avvio della vostra applicazione.

Ricordate che dovete fornire alle vostre applicazioni frontend e backend alcune variabili d’ambiente per farle funzionare correttamente. L’applicazione frontend ha bisogno delle seguenti variabili d’ambiente:

  • VITE_STRIPE_API_KEY
  • VITE_SERVER_BASE_URL
  • VITE_CLIENT_BASE_URL

Per distribuire l’applicazione backend, fate esattamente quello che abbiamo fatto per il frontend, ma per il passaggio Ambiente di build, selezionate il pulsante di opzione Usa Dockerfile per impostare l’immagine del container e inserite Dockerfile come percorso del Dockerfile per l’applicazione backend.

Il modulo di richiesta di aggiunta che chiede di fornire i dettagli dell'ambiente di costruzione.
Impostazione dei dettagli dell’ambiente di compilazione

Ricordatevi di aggiungere le variabili d’ambiente del backend:

  • CLIENT_BASE_URL
  • STRIPE_API_KEY

Una volta completata la distribuzione, andate alla pagina dei dettagli della vostra applicazione e accedete all’URL della distribuzione da lì.

La pagina dei dettagli dell'applicazione con un riquadro rosso che mostra dove trovare l'URL dell'installazione.
L’URL ospitato per le applicazioni distribuite su Kinsta

Estraete gli URL di entrambe le app distribuite. Andate alla bacheca di Stripe per ottenere le chiavi API segrete e pubblicabili.

Fornite la chiave pubblicabile di Stripe alla vostra applicazione frontend (non la chiave segreta). Inoltre, assicuratevi che gli URL di base non abbiano uno slash (/) alla fine. I percorsi hanno già degli slash iniziali, quindi l’aggiunta di uno slash alla fine degli URL di base comporterà l’aggiunta di due slash agli URL finali.

Per l’applicazione di backend, aggiungete la chiave segreta dalla bacheca di Stripe (non la chiave pubblicabile). Inoltre, assicuratevi che l’URL del vostro cliente non abbia uno slash (/) alla fine.

Una volta aggiunte le variabili, andate alla scheda Distribuzioni dell’applicazione e fate clic sul pulsante Distribuisci di nuovo per la vostra applicazione backend. Questo completa la configurazione una tantum necessaria per fornire le credenziali alle distribuzioni Kinsta tramite le variabili d’ambiente.

In seguito, potrete effettuare il commit delle modifiche al vostro controllo di versione. Kinsta effettuerà automaticamente il redeploy della vostra applicazione se avete spuntato l’opzione durante il deploy; in caso contrario, dovrete attivare il redeploy manualmente.

Riepilogo

In questo articolo abbiamo approfondito come funzionano Stripe e i flussi di pagamento che offre. Inoltre, grazie a un esempio dettagliato, abbiamo visto come integrare Stripe nella vostra applicazione Java per accettare pagamenti una tantum, impostare abbonamenti, offrire prove gratuite e generare fatture di pagamento.

Usando Stripe e Java insieme, potete offrire ai vostri clienti una soluzione di pagamento solida, in grado di scalare e di integrarsi perfettamente con l’ecosistema di applicazioni e strumenti esistenti.

Usate Stripe nella vostra applicazione per ricevere i pagamenti? Se sì, quale flusso preferite: in hosting, personalizzato o in-app? Fatecelo sapere nei commenti qui sotto!

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 ;).