Mit der Zunahme digitaler Transaktionen ist die Fähigkeit, Zahlungsgateways nahtlos zu integrieren, zu einer entscheidenden Fähigkeit für Entwickler geworden. Ob für Marktplätze oder SaaS-Produkte, ein Zahlungsabwickler ist unerlässlich, um die Zahlungen der Nutzer/innen zu sammeln und zu verarbeiten.

In diesem Artikel erfährst du, wie du Stripe in eine Spring Boot-Umgebung integrierst, wie du Abonnements einrichtest, kostenlose Testversionen anbietest und Selbstbedienungsseiten erstellst, auf denen deine Kunden ihre Rechnungen herunterladen können.

Was ist Stripe?

Stripe ist eine weltweit bekannte Plattform zur Zahlungsabwicklung, die in 46 Ländern verfügbar ist. Sie ist eine gute Wahl, wenn du eine Zahlungsintegration in deine Webanwendung einbauen willst, denn sie hat eine große Reichweite, einen guten Ruf und eine ausführliche Dokumentation.

Allgemeine Stripe-Konzepte verstehen

Es ist hilfreich, einige allgemeine Konzepte zu verstehen, die Stripe verwendet, um Zahlungsvorgänge zwischen mehreren Parteien zu koordinieren und auszuführen. Stripe bietet zwei Ansätze für die Implementierung der Zahlungsintegration in deiner Anwendung.

Du kannst entweder die Formulare von Stripe in deine Anwendung einbetten, um den Kunden ein In-App-Erlebnis zu bieten (Payment Intent), oder die Kunden auf eine von Stripe gehostete Zahlungsseite umleiten, auf der Stripe den Prozess verwaltet und deine Anwendung darüber informiert, ob eine Zahlung erfolgreich war oder nicht (Payment Link).

Zahlungsintention

Bei der Zahlungsabwicklung ist es wichtig, die Kunden- und Produktdaten im Voraus zu erfassen, bevor du sie zur Eingabe ihrer Kartendaten und zur Zahlung aufforderst. Diese Details umfassen die Beschreibung, den Gesamtbetrag, die Zahlungsart und mehr.

Stripe verlangt von dir, dass du diese Daten in deiner Anwendung sammelst und ein PaymentIntent Objekt im Backend generierst. Auf diese Weise kann Stripe eine Zahlungsanforderung für diese Absicht formulieren. Nach Abschluss der Zahlung kannst du die Zahlungsdetails, einschließlich des Verwendungszwecks, immer über das PaymentIntent Objekt abrufen.

Zahlungslinks

Um die Komplexität der Integration von Stripe direkt in deine Codebasis zu vermeiden, kannst du Stripe Checkouts als gehostete Zahlungslösung nutzen. Wie bei der Erstellung von PaymentIntent erstellst du ein CheckoutSession mit den Zahlungs- und Kundendaten. Anstatt eine In-App PaymentIntent zu initiieren, generiert die CheckoutSession einen Zahlungslink, über den du deine Kunden weiterleitest. So sieht eine gehostete Zahlungsseite aus:

Die von Stripe gehostete Kassenseite mit den Rechnungsdetails auf der linken Seite und dem Formular zur Erfassung der Zahlungsdetails auf der rechten Seite.
Die von Stripe gehostete Kassenseite

Nach der Zahlung leitet Stripe zurück zu deiner Anwendung und ermöglicht so Aufgaben nach der Zahlung wie Bestätigungen und Lieferanfragen. Konfiguriere einen Backend-Webhook, um Stripe zu aktualisieren, damit die Zahlungsdaten auch dann erhalten bleiben, wenn die Kunden die Seite nach der Zahlung versehentlich schließen.

Diese Methode ist zwar effektiv, aber es fehlt ihr an Flexibilität bei der Anpassung und Gestaltung. Außerdem kann es schwierig sein, sie für mobile Apps richtig einzurichten, wo eine native Integration viel nahtloser aussehen würde.

API-Schlüssel

Wenn du mit der Stripe-API arbeitest, brauchst du API-Schlüssel für deine Client- und Server-Anwendungen, um mit dem Stripe-Backend zu interagieren. Du kannst auf deine Stripe-API-Schlüssel in deinem Stripe-Entwickler-Dashboard zugreifen. So würde es aussehen:

Der Entwicklerbereich des Stripe-Dashboards zeigt die Registerkarte API-Schlüssel.
Das Stripe-Dashboard mit den API-Schlüsseln

Wie funktionieren Zahlungen in Stripe?

Um zu verstehen, wie Zahlungen in Stripe funktionieren, musst du alle beteiligten Akteure verstehen. An jeder Zahlungstransaktion sind vier Akteure beteiligt:

  1. Kunde: Die Person, die für eine Dienstleistung/ein Produkt bezahlen will.
  2. Händler: Du, der Geschäftsinhaber, bist für den Empfang von Zahlungen und den Verkauf von Dienstleistungen/Produkten verantwortlich.
  3. Acquirer: Eine Bank, die Zahlungen für dich (den Händler) abwickelt und deine Zahlungsanfragen an die Banken deiner Kunden weiterleitet. Acquirer können mit Dritten zusammenarbeiten, um die Zahlungen zu bearbeiten.
  4. Ausstellende Bank: Die Bank, die Kredite vergibt und Karten und andere Zahlungsmittel an Verbraucher ausgibt.

Hier ist ein typischer Zahlungsfluss zwischen diesen Akteuren auf einer sehr hohen Ebene.

Ein grundlegender Arbeitsablauf, der zeigt, wie Online-Zahlungen vom Kunden, Händler, Acquirer und der ausstellenden Bank abgewickelt werden.
So funktionieren Online-Zahlungen

Der Kunde teilt dem Händler mit, dass er bereit ist zu zahlen. Der Händler leitet dann die Zahlungsdaten an seine Acquiring-Bank weiter, die die Zahlung von der Bank des Kunden einzieht und den Händler über den Erfolg der Zahlung informiert.

Dies ist ein sehr detaillierter Überblick über den Zahlungsprozess. Als Händler musst du dich nur um die Erfassung der Zahlungsabsicht, die Weiterleitung an den Zahlungsabwickler und die Bearbeitung des Zahlungsergebnisses kümmern. Wie bereits erwähnt, gibt es jedoch zwei Möglichkeiten, wie du das machen kannst.

Wenn du eine von Stripe verwaltete Kaufabwicklung erstellst, bei der Stripe sich um die Erfassung der Zahlungsdaten kümmert, sieht der typische Ablauf folgendermaßen aus:

Der Stripe Hosted Checkout-Zahlungsworkflow zeigt, wie die Zahlung zwischen dem Client, dem Server, der Stripe API und der Hosted Stripe Checkout-Seite abgewickelt wird.
Der von Stripe verwaltete Checkout-Zahlungsablauf. (Quelle: Stripe Docs)

Bei benutzerdefinierten Zahlungsabläufen liegt es wirklich an dir. Du kannst die Interaktion zwischen deinem Client, Server, Kunden und der Stripe-API nach den Bedürfnissen deiner Anwendung gestalten. Du kannst diesen Workflow je nach Bedarf um Adresserfassung, Rechnungserstellung, Stornierung, kostenlose Tests usw. erweitern.

Jetzt, wo du weißt, wie Stripe-Zahlungen funktionieren, kannst du damit beginnen, sie in deine Java-Anwendung zu integrieren.

Stripe-Integration in Spring Boot-Anwendung

Um mit der Stripe-Integration zu beginnen, musst du eine Frontend-Anwendung erstellen, die mit dem Java-Backend interagiert und Zahlungen auslöst. In diesem Leitfaden baust du eine React-Anwendung, um verschiedene Zahlungsarten und Abonnements auszulösen, damit du die Mechanismen besser verstehst.

Hinweis: In diesem Tutorial geht es nicht darum, eine komplette E-Commerce-Website zu erstellen, sondern vor allem darum, dich durch den einfachen Prozess der Integration von Stripe in Spring Boot zu führen.

Einrichten der Frontend- und Backend-Projekte

Erstelle ein neues Verzeichnis und rüste ein React-Projekt mit Vite, indem du den folgenden Befehl ausführst:

npm create vite@latest

Setze den Projektnamen auf Frontend (oder einen beliebigen Namen), das Framework auf React und die Variante auf TypeScript. Navigiere in das Projektverzeichnis und installiere Chakra UI für die schnelle Erstellung von UI-Elementen, indem du den folgenden Befehl ausführst:

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

Außerdem installierst du react-router-dom in deinem Projekt für das clientseitige Routing, indem du den folgenden Befehl ausführst:

npm i react-router-dom

Jetzt kannst du mit der Erstellung deiner Frontend-Anwendung beginnen. Hier ist die Homepage, die du erstellen wirst.

Die fertige Startseite für die Frontend-Anwendung mit einer Kopfzeile und Schaltflächen für den Zugriff auf alle Seiten der Anwendung.
Die fertige Startseite für die Frontend-Anwendung.

Wenn du auf eine beliebige Schaltfläche auf dieser Seite klickst, gelangst du zu separaten Kassenseiten mit Zahlungsformularen. Um zu beginnen, erstelle einen neuen Ordner namens routes in deinem frontend/src Verzeichnis. In diesem Ordner erstellst du eine Datei Home.tsx. Diese Datei enthält den Code für die Home-Route deiner Anwendung (/). Füge den folgenden Code in die Datei ein:

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

Um die Navigation in deiner Anwendung zu aktivieren, aktualisiere deine App.tsx-Datei, um die Klasse RouteProvider von react-router-dom zu konfigurieren.

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

Führe den Befehl npm run dev aus, um eine Vorschau deiner Anwendung auf https://localhost:5173 zu erhalten.

Damit sind die ersten Schritte für die Frontend-Anwendung abgeschlossen. Als Nächstes erstellst du eine Backend-Anwendung mit Spring Boot. Um die Anwendung zu initialisieren, kannst du die Website spring initializr verwenden (wenn deine IDE die Erstellung von Spring-Apps unterstützt, brauchst du die Website nicht zu verwenden).

IntelliJ IDEA unterstützt die Erstellung von Spring Boot-Anwendungen. Wähle zunächst die Option Neues Projekt in IntelliJ IDEA. Wähle dann Spring Initializr aus dem linken Fenster. Gib die Details deines Backend-Projekts ein: Name (Backend), Speicherort (stripe-payments-java Verzeichnis), Sprache (Java) und Typ (Maven). Als Gruppen- und Artefaktnamen verwendest du com.kinsta.stripe-java bzw. backend.

Der IntelliJ IDEA-Dialog für neue Projekte zeigt die ausgefüllten Details für das neue Projekt an.
Der IDEA-Dialog für neue Projekte

Klicke auf die Schaltfläche Weiter. Füge dann deinem Projekt Abhängigkeiten hinzu, indem du Spring Web aus dem Dropdown-Menü Web im Abhängigkeitsbereich auswählst und auf die Schaltfläche Erstellen klickst.

Der IntelliJ IDEA-Assistent für neue Projekte zeigt die Abhängigkeiten an, die der Benutzer für seine neue Anwendung ausgewählt hat.
Auswahl der Abhängigkeiten

Damit wird das Java-Projekt erstellt und in deiner IDE geöffnet. Jetzt kannst du mit der Erstellung der verschiedenen Checkout-Flows mit Stripe fortfahren.

Akzeptieren von Online-Zahlungen für Produktkäufe

Die wichtigste und am häufigsten genutzte Funktion von Stripe ist die Annahme von Einmalzahlungen von Kunden. In diesem Abschnitt lernst du zwei Möglichkeiten kennen, wie du die Zahlungsabwicklung mit Stripe in deine Anwendung integrieren kannst.

Hosted Checkout

Zunächst erstellst du eine Checkout-Seite, die einen gehosteten Zahlungsworkflow auslöst, bei dem du eine Zahlung nur von deiner Frontend-Anwendung aus auslöst. Stripe kümmert sich dann um die Erfassung der Kartendaten des Kunden und den Einzug der Zahlung und teilt am Ende nur das Ergebnis des Zahlungsvorgangs mit.

So würde die Kassenseite aussehen:

Die fertige gehostete Kassenseite
Die fertige gehostete Kassenseite

Diese Seite hat drei Hauptkomponenten: CartItemTotalFooter – zeigt den Gesamtbetrag an; CustomerDetails – sammelt die Kundendaten. Du kannst diese Komponenten wiederverwenden, um Checkout-Formulare für andere Szenarien in diesem Artikel zu erstellen, z. B. für integrierte Checkouts und Abonnements.

Aufbau des Frontends

Erstelle einen Komponentenordner in deinem Frontend/src-Verzeichnis. Erstelle im Komponentenordner eine neue Datei CartItem.tsx und füge den folgenden Code ein:

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

Der obige Code definiert zwei Schnittstellen, die als Typen für die Eigenschaften verwendet werden, die an die Komponente übergeben werden. Der Typ ItemData wird zur Wiederverwendung in anderen Komponenten exportiert.

Der Code gibt das Layout einer Warenkorbkomponente zurück. Er verwendet die übergebenen Requisiten, um den Artikel auf dem Bildschirm darzustellen.

Als Nächstes erstellst du eine Datei TotalFooter.tsx im Komponentenverzeichnis und fügst den folgenden Code ein:

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

Die Komponente TotalFooter zeigt den Gesamtwert des Warenkorbs an und verwendet den Wert mode, um einen bestimmten Text anzuzeigen.

Schließlich erstellst du die Komponente CustomerDetails.tsx und fügst den folgenden Code ein:

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

Der obige Code zeigt ein Formular mit zwei Eingabefeldern an, um den Namen und die E-Mail-Adresse des Benutzers zu erfassen. Wenn der Checkout-Button angeklickt wird, wird die Methode initiatePayment aufgerufen, um die Checkout-Anfrage an das Backend zu senden.

Sie fordert den Endpunkt an, den du der Komponente übergeben hast, und sendet die Kundendaten und die Artikel des Warenkorbs als Teil der Anfrage und leitet den Nutzer dann an die vom Server erhaltene URL weiter. Diese URL führt den Nutzer zu einer Kassenseite, die auf dem Server von Stripe gehostet wird. Wie du diese URL erstellst, erfährst du gleich.

Hinweis: Diese Komponente verwendet die Umgebungsvariable VITE_SERVER_BASE_URL für die URL des Backend-Servers. Setze sie, indem du eine .env-Datei im Stammverzeichnis deines Projekts erstellst:

VITE_SERVER_BASE_URL=http://localhost:8080

Alle Komponenten sind nun erstellt. Fahren wir nun fort, die Hosted Checkout Route mit den Komponenten zu erstellen. Dazu erstellst du eine neue Datei HostedCheckout.tsx in deinem routes-Ordner mit folgendem Code:

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

Diese Route verwendet die drei Komponenten, die du gerade erstellt hast, um einen Checkout-Bildschirm zu erstellen. Alle Komponenten sind als Kasse konfiguriert und der Endpunkt /checkout/hosted wird der Formularkomponente zur Verfügung gestellt, um die Kassenanfrage korrekt zu initiieren.

Die Komponente verwendet ein Products Objekt, um das Item-Array zu füllen. In der Praxis kommen diese Daten von deiner Warenkorb-API und enthalten die vom Nutzer ausgewählten Artikel. In diesem Leitfaden wird das Array jedoch mit einer statischen Liste aus einem Skript gefüllt. Definiere das Array, indem du eine data.ts-Datei im Stammverzeichnis deines Frontend-Projekts erstellst und den folgenden Code darin speicherst:

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

Diese Datei definiert zwei Elemente im Produkt-Array, die im Warenkorb angezeigt werden. Du kannst die Werte der Produkte beliebig verändern.

Im letzten Schritt der Entwicklung des Frontends erstellst du zwei neue Routen, um Erfolg und Misserfolg zu behandeln. Die von Stripe gehostete Kassenseite leitet die Nutzer je nach Ergebnis der Transaktion über diese beiden Routen in deine Anwendung weiter. Stripe stellt deinen Routen auch transaktionsbezogene Daten zur Verfügung, z. B. die ID der Kassensitzung, die du verwenden kannst, um das entsprechende Kassensitzungsobjekt abzurufen und auf kassenbezogene Daten wie die Zahlungsmethode, Rechnungsdetails usw. zuzugreifen.

Dazu erstellst du eine Datei Success.tsx im Verzeichnis src/routes und speicherst den folgenden Code darin:

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

Beim Rendern zeigt diese Komponente die Meldung „Success!“ an und gibt alle URL-Abfrageparameter auf dem Bildschirm aus. Außerdem enthält sie eine Schaltfläche, mit der die Benutzer zur Homepage der Anwendung weitergeleitet werden.

Wenn du eine reale Anwendung entwickelst, ist diese Seite der Ort, an dem du unkritische app-seitige Transaktionen abwickelst, die vom Erfolg der jeweiligen Transaktion abhängen. Wenn du z. B. eine Kassenseite für einen Onlineshop erstellst, könntest du diese Seite nutzen, um dem Nutzer eine Bestätigung zu geben und ihm mitzuteilen, wann seine gekauften Produkte geliefert werden.

Als Nächstes erstellst du eine Datei Failure.tsx mit dem folgenden Code darin:

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

Diese Komponente ähnelt der Success.tsx und zeigt die Meldung „Failure!“ an, wenn sie gerendert wird.

Für wichtige Aufgaben wie die Produktlieferung, das Versenden von E-Mails oder andere kritische Teile deines Kaufprozesses solltest du Webhooks verwenden. Webhooks sind API-Routen auf deinem Server, die Stripe aufrufen kann, wenn eine Transaktion stattfindet.

Der Webhook erhält die vollständigen Transaktionsdetails (über das CheckoutSession Objekt), sodass du sie in deiner Anwendungs-Datenbank protokollieren und entsprechende Erfolgs- oder Misserfolgs-Workflows auslösen kannst. Da dein Server für Stripe immer erreichbar ist, entgehen dir keine Transaktionen, was die konsistente Funktionalität deines Onlineshops gewährleistet.

Zum Schluss aktualisierst du die Datei App.tsx so, dass sie wie folgt aussieht:

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

Dadurch wird sichergestellt, dass die Komponenten Success und Failure auf den Routen /success bzw. /failure gerendert werden.

Damit ist die Einrichtung des Frontends abgeschlossen. Als Nächstes richtest du das Backend ein, um den Endpunkt /checkout/hosted zu erstellen.

Erstellen des Backends

Öffne das Backend-Projekt und installiere das Stripe SDK, indem du die folgenden Zeilen in das Array dependencies in deiner pom.xml-Datei einfügst:

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

Als Nächstes lädst du die Maven-Änderungen in dein Projekt, um die Abhängigkeiten zu installieren. Wenn deine IDE dies nicht über die Benutzeroberfläche unterstützt, führe entweder den Befehl maven dependency:resolve oder maven install aus. Wenn du nicht über die maven CLI verfügst, verwende den mvnw Wrapper von Spring initializr, wenn du das Projekt erstellst.

Sobald die Abhängigkeiten installiert sind, erstellst du einen neuen REST-Controller, der die eingehenden HTTP-Anfragen für deine Backend-Anwendung bearbeitet. Dazu erstellst du eine Datei PaymentController.java im Verzeichnis src/main/java/com/kinsta/stripe-java/backend und fügst den folgenden Code hinzu:

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

}

Der obige Code importiert wichtige Stripe-Abhängigkeiten und richtet die Klasse PaymentController ein. Diese Klasse trägt zwei Annotationen: @RestController und @CrossOrigin. Die Annotation @RestController weist Spring Boot an, diese Klasse als Controller zu behandeln, und ihre Methoden können nun @Mapping Annotationen verwenden, um eingehende HTTP-Anfragen zu bearbeiten.

Die Annotation @CrossOrigin kennzeichnet alle in dieser Klasse definierten Endpunkte als offen für alle Herkünfte gemäß den CORS-Regeln. Aufgrund potenzieller Sicherheitslücken in verschiedenen Internet-Domänen wird von dieser Praxis in der Produktion jedoch abgeraten.

Um optimale Ergebnisse zu erzielen, ist es ratsam, sowohl Backend- als auch Frontend-Server auf derselben Domain zu hosten, um CORS-Probleme zu umgehen. Wenn das nicht möglich ist, kannst du alternativ die Domäne deines Frontend-Clients (der Anfragen an den Backend-Server sendet) mit der @CrossOrigin -Annotation angeben, etwa so:

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

Die Klasse PaymentController extrahiert den Stripe-API-Schlüssel aus den Umgebungsvariablen, um ihn später dem Stripe SDK zur Verfügung zu stellen. Wenn du die Anwendung ausführst, musst du der Anwendung deinen Stripe-API-Schlüssel über Umgebungsvariablen zur Verfügung stellen.

Lokal kannst du eine neue Umgebungsvariable in deinem System erstellen, entweder temporär (indem du eine KEY=VALUE Phrase vor dem Befehl zum Starten deines Entwicklungsservers hinzufügst) oder dauerhaft (indem du die Konfigurationsdateien deines Terminals aktualisierst oder eine Umgebungsvariable in der Systemsteuerung von Windows setzt).

In Produktionsumgebungen bietet dir dein Deployment Provider (z. B. Kinsta) eine separate Option, um die von deiner Anwendung verwendeten Umgebungsvariablen einzutragen.

Wenn du IntelliJ IDEA (oder eine ähnliche IDE) verwendest, klickst du oben rechts in der IDE auf Run Configurations und dann in der sich öffnenden Dropdown-Liste auf die Option Edit Configurations…, um deinen Run-Befehl zu aktualisieren und die Umgebungsvariable zu setzen.

Das IntelliJ IDEA-Fenster zeigt, von wo aus du auf die Einstellungen für die Run/Debug-Konfigurationen zugreifen kannst.
Das Dialogfeld für die Ausführungs-/Debug-Konfigurationen wird geöffnet

Es öffnet sich ein Dialogfeld, in dem du die Umgebungsvariablen für deine Anwendung im Feld Umgebungsvariablen angeben kannst. Gib die Umgebungsvariable STRIPE_API_KEY im Format VAR1=VALUE ein. Deinen API-Schlüssel findest du auf der Stripe Developers Website. Du musst den Wert des geheimen Schlüssels von dieser Seite angeben.

 Das Stripe-Dashboard mit einem Pfeil, der anzeigt, wo du nach API-Schlüsseln suchen kannst. Die Schlüssel sind geschwärzt, um sensible Informationen zu verbergen.
Das Stripe-Dashboard zeigt die API-Schlüssel an

Wenn du es noch nicht getan hast, erstelle ein neues Stripe-Konto, um Zugang zu den API-Schlüsseln zu erhalten.

Sobald du den API-Schlüssel eingerichtet hast, kannst du den Endpunkt erstellen. Dieser Endpunkt sammelt die Kundendaten (Name und E-Mail), erstellt ein Kundenprofil in Stripe, falls noch keines vorhanden ist, und erstellt eine Kassensitzung, damit die Nutzer/innen die Artikel im Warenkorb bezahlen können.

So sieht der Code für die Methode hostedCheckout aus:

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

Beim Aufbau der Kassensitzung verwendet der Code den Namen des Produkts, den er vom Kunden erhalten hat, aber nicht die Preisangaben aus der Anfrage. Dieser Ansatz verhindert potenzielle Preismanipulationen auf Kundenseite, bei denen böswillige Akteure reduzierte Preise in der Checkout-Anfrage senden könnten, um weniger für Produkte und Dienstleistungen zu bezahlen.

Um dies zu verhindern, fragt die Methode hostedCheckout deine Produktdatenbank ab (über ProductDAO), um den korrekten Artikelpreis abzurufen.

Außerdem bietet Stripe verschiedene Builder Klassen an, die dem Builder Design Pattern folgen. Diese Klassen helfen bei der Erstellung von Parameterobjekten für Stripe-Anfragen. Das mitgelieferte Codeschnipsel verweist auch auf Umgebungsvariablen, um die URL der Client-Anwendung abzurufen. Diese URL wird benötigt, damit das Checkout-Sitzungsobjekt nach erfolgreichen oder fehlgeschlagenen Zahlungen entsprechend weitergeleitet wird.

Um diesen Code auszuführen, musst du die URL der Kundenanwendung über Umgebungsvariablen festlegen, ähnlich wie bei der Angabe des Stripe-API-Schlüssels. Da die Client-Anwendung über Vite läuft, sollte die lokale App-URL http://localhost:5173 lauten. Füge sie über deine IDE, dein Terminal oder dein Systemsteuerungspanel in deine Umgebungsvariablen ein.

CLIENT_BASE_URL=http://localhost:5173

Gib der Anwendung außerdem eine ProductDAO, um die Produktpreise abzurufen. Data Access Object (DAO) interagiert mit Datenquellen (z. B. Datenbanken), um auf app-bezogene Daten zuzugreifen. Das Einrichten einer Produktdatenbank würde den Rahmen dieses Leitfadens sprengen, aber eine einfache Implementierung wäre es, eine neue Datei ProductDAO.java im selben Verzeichnis wie PaymentController.java hinzuzufügen und den folgenden Code einzufügen:

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

    }
}

Damit wird ein Array mit Produkten initialisiert und du kannst die Produktdaten anhand ihrer Kennung (ID) abfragen. Außerdem musst du ein DTO (Data Transfer Object) erstellen, damit Spring Boot die vom Client eingehenden Nutzdaten automatisch serialisieren und dir ein einfaches Objekt für den Zugriff auf die Daten zur Verfügung stellen kann. Dazu erstellst du eine neue Datei RequestDTO.java und fügst den folgenden Code ein:

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

}

Diese Datei definiert ein POJO, das den Kundennamen, die E-Mail und die Liste der Artikel enthält, mit denen der Kunde auscheckt.

Implementiere schließlich die Methode CustomerUtil.findOrCreateCustomer(), um das Kundenobjekt in Stripe zu erstellen, falls es noch nicht existiert. Dazu erstellst du eine Datei mit dem Namen CustomerUtil und fügst den folgenden Code hinzu:

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

Diese Klasse enthält außerdem eine weitere Methode findCustomerByEmail, mit der du Kunden in Stripe anhand ihrer E-Mail-Adressen suchen kannst. Die Kundensuche-API wird verwendet, um die Kundendatensätze in der Stripe-Datenbank zu suchen, und die Kundenerstellungs-API wird verwendet, um die Kundendatensätze nach Bedarf zu erstellen.

Damit ist die Einrichtung des Backends für den gehosteten Checkout abgeschlossen. Du kannst die Anwendung jetzt testen, indem du die Frontend- und die Backend-App in ihren IDEs oder auf separaten Terminals ausführst. So würde der erfolgreiche Ablauf aussehen:

Ein Benutzerablauf, der zeigt, wie ein erfolgreicher Checkout über die gehostete Stripe-Seite aussieht.
Ein erfolgreicher Hosted Checkout Flow

Wenn du Stripe-Integrationen testest, kannst du immer die folgenden Kartendaten verwenden, um Kartentransaktionen zu simulieren:

Kartennummer: 4111 1111 1111 1111
Gültigkeitsmonat und -jahr: 12 / 25
CVV: Beliebige dreistellige Zahl
Name auf der Karte: Beliebiger Name

Wenn du dich dafür entscheidest, die Transaktion zu stornieren, anstatt zu bezahlen, sieht der Ablauf folgendermaßen aus:

Ein User Flow, der zeigt, wie ein fehlgeschlagener Checkout über die gehostete Stripe-Seite aussieht.
Ein fehlgeschlagener Hosted Checkout Flow

Damit ist die Einrichtung einer von Stripe gehosteten Kaufabwicklung in deiner Anwendung abgeschlossen. In den Stripe-Dokumenten erfährst du mehr darüber, wie du deine Kassenseite anpassen kannst, wie du weitere Details vom Kunden sammelst und vieles mehr.

Integrierter Checkout

Eine integrierte Kaufabwicklung bedeutet, dass du einen Zahlungsablauf erstellst, der deine Nutzer nicht außerhalb deiner Anwendung weiterleitet (wie bei der gehosteten Kaufabwicklung), sondern das Zahlungsformular direkt in deiner Anwendung anzeigt.

Eine integrierte Kaufabwicklung bedeutet, dass du die Zahlungsdaten deiner Kunden verarbeiten musst, was sensible Informationen wie Kreditkartennummern, Google Pay ID usw. beinhaltet. Nicht alle Anwendung sind für den sicheren Umgang mit diesen Daten ausgelegt.

Um dich von der Einhaltung von Standards wie PCI-DSS zu entlasten, bietet Stripe Elemente an, die du in der Anwendung verwenden kannst, um Zahlungsdaten zu sammeln, während Stripe die Sicherheit verwaltet und die Zahlungen sicher abwickelt.

Das Frontend erstellen

Installiere zunächst das Stripe React SDK in deiner Frontend-Anwendung, um auf die Stripe-Elemente zuzugreifen, indem du den folgenden Befehl in deinem Frontend-Verzeichnis ausführst:

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

Als Nächstes erstellst du eine neue Datei namens IntegratedCheckout.tsx in deinem frontend/src/routes-Verzeichnis und speicherst den folgenden Code darin:

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

Diese Datei definiert zwei Komponenten, IntegratedCheckout und CheckoutForm. Die CheckoutForm definiert ein einfaches Formular mit einer PaymentElement von Stripe, die die Zahlungsdaten der Kunden sammelt, und einer Schaltfläche Bezahlen, die eine Zahlungsaufforderung auslöst.

Diese Komponente ruft auch die Hooks useStripe() und useElements() auf, um eine Instanz des Stripe SDK zu erstellen, die du zum Erstellen von Zahlungsanforderungen verwenden kannst. Sobald du auf die Schaltfläche Bezahlen klickst, wird die Methode stripe.confirmPayment() aus dem Stripe SDK aufgerufen, die die Zahlungsdaten des Nutzers aus der Instanz der Elemente sammelt und an das Stripe-Backend sendet, zusammen mit einer Erfolgs-URL, zu der du weitergeleitet wirst, wenn die Transaktion erfolgreich war.

Das Checkout-Formular wurde vom Rest der Seite getrennt, weil die Hooks useStripe() und useElements() aus dem Kontext eines Elements Anbieters aufgerufen werden müssen, was in der Rückgabeanweisung von IntegratedCheckout geschieht. Wenn du die Stripe-Hook-Aufrufe direkt in die Komponente IntegratedCheckout verschieben würdest, lägen sie außerhalb des Bereichs des Elements Anbieters und würden daher nicht funktionieren.

Die Komponente IntegratedCheckout verwendet die Komponenten CartItem und TotalFooter, um die Artikel im Warenkorb und den Gesamtbetrag anzuzeigen. Sie zeigt außerdem zwei Eingabefelder für die Kundendaten und eine Schaltfläche Zahlung einleiten an, die eine Anfrage an den Java-Backend-Server sendet, um den geheimen Kundenschlüssel anhand der Kunden- und Warenkorbdaten zu erstellen. Sobald der geheime Kundenschlüssel eingegangen ist, wird die Seite CheckoutForm angezeigt, die die Zahlungsdaten des Kunden erfasst.

Außerdem wird useEffect verwendet, um die Methode loadStripe aufzurufen. Dieser Effekt wird nur einmal ausgeführt, wenn die Komponente gerendert wird, damit das Stripe SDK nicht mehrmals geladen wird, wenn die internen Zustände der Komponente aktualisiert werden.

Um den obigen Code auszuführen, musst du außerdem zwei neue Umgebungsvariablen zu deinem Frontend-Projekt hinzufügen: VITE_STRIPE_API_KEY und VITE_CLIENT_BASE_URL. Die Variable für den Stripe-API-Schlüssel enthält den veröffentlichbaren API-Schlüssel aus dem Stripe-Dashboard, und die Variable für die Client Base URL enthält den Link zur Client-Anwendung (die Frontend-Anwendung selbst), damit sie an das Stripe-SDK weitergegeben werden kann, um Erfolgs- und Fehlerumleitungen zu verarbeiten.

Dazu fügst du den folgenden Code in deine .env-Datei im Frontend-Verzeichnis ein:

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

Aktualisiere schließlich die App.tsx-Datei, um die Komponente IntegratedCheckout in die /integrated-checkout Route der Frontend-Anwendung einzubinden. Füge den folgenden Code in das Array ein, das an den Aufruf createBrowserRouter in der Komponente App übergeben wird:

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

Damit sind die erforderlichen Einstellungen am Frontend abgeschlossen. Als Nächstes erstellst du eine neue Route auf deinem Backend-Server, die den geheimen Client-Schlüssel erstellt, der für die integrierten Checkout-Sitzungen in deiner Frontend-Anwendung benötigt wird.

Aufbau des Backends

Um sicherzustellen, dass die Frontend-Integration nicht von Angreifern missbraucht wird (da der Frontend-Code leichter zu knacken ist als der Backend-Code), verlangt Stripe von dir, dass du ein eindeutiges Kundengeheimnis auf deinem Backend-Server generierst und jede integrierte Zahlungsanforderung mit dem im Backend generierten Kundengeheimnis verifizierst, um sicherzustellen, dass es tatsächlich deine Anwendung ist, die versucht, Zahlungen einzuziehen. Dazu musst du im Backend eine weitere Route einrichten, die anhand der Kunden- und Warenkorbinformationen Kundengeheimnisse erstellt.

Um den Schlüssel für das Kundengeheimnis auf deinem Server zu erstellen, erstelle eine neue Methode in deiner Klasse PaymentController mit dem Namen integratedCheckout und speichere den folgenden Code darin:

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

Ähnlich wie die Kassensitzung mit Hilfe einer Builder-Klasse erstellt wurde, die die Konfiguration für die Zahlungsanforderung übernimmt, musst du für den integrierten Checkout-Flow eine Zahlungssitzung mit dem Betrag, der Währung und den Zahlungsarten erstellen. Anders als bei der Kassensitzung kannst du einer Zahlungssitzung keine Einzelposten zuordnen, es sei denn, du erstellst eine Rechnung, was du in einem späteren Abschnitt des Leitfadens lernen wirst.

Da du die Einzelposten nicht an den Checkout Session Builder weitergibst, musst du den Gesamtbetrag für die Artikel im Warenkorb manuell berechnen und den Betrag an das Stripe Backend senden. Verwende dein ProductDAO, um die Preise für jedes Produkt im Warenkorb zu finden und hinzuzufügen.

Dazu definierst du eine neue Methode calculateOrderAmount und fügst den folgenden Code darin ein:

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

Das sollte ausreichen, um den integrierten Checkout-Flow sowohl auf dem Frontend als auch auf dem Backend einzurichten. Du kannst die Entwicklungsserver für den Server und den Client neu starten und den neuen integrierten Checkout-Flow in der Frontend-Anwendung ausprobieren. Hier siehst du, wie der integrierte Ablauf aussehen wird:

Ein User Flow, der zeigt, wie ein erfolgreicher integrierter Checkout mit der Stripe-Integration aussieht.
Ein integrierter Checkout-Flow

Damit ist der grundlegende integrierte Checkout-Flow in deiner App fertiggestellt. Du kannst nun die Stripe-Dokumentation weiter durchforsten, um die Zahlungsmethoden anzupassen oder weitere Komponenten zu integrieren, die dir bei anderen Vorgängen helfen, z. B. bei der Adresserfassung, bei Zahlungsanforderungen, bei der Integration von Links und vielem mehr!

Einrichten von Abonnements für wiederkehrende Dienste

Ein gängiges Angebot von Online-Shops ist heutzutage ein Abonnement. Egal, ob du einen Marktplatz für Dienstleistungen aufbaust oder ein digitales Produkt regelmäßig anbietest, ein Abonnement ist die perfekte Lösung, um deinen Kunden für eine geringe Gebühr im Vergleich zu einem einmaligen Kauf regelmäßig Zugang zu deinem Service zu geben.

Mit Stripe kannst du ganz einfach Abonnements einrichten und kündigen. Du kannst auch kostenlose Testversionen als Teil deines Abonnements anbieten, damit die Nutzer dein Angebot ausprobieren können, bevor sie sich binden.

Ein neues Abonnement einrichten

Das Einrichten eines neuen Abonnements ist mit dem gehosteten Kassenablauf ganz einfach. Du musst nur ein paar Parameter bei der Erstellung der Checkout-Anfrage ändern und eine neue Seite erstellen (indem du die bestehenden Komponenten wiederverwendest), um eine Checkout-Seite für ein neues Abonnement anzuzeigen. Erstelle zunächst eine Datei NewSubscription.tsx im Ordner mit den Frontend-Komponenten. Füge den folgenden Code darin ein:

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

Im obigen Code werden die Daten des Warenkorbs aus der Datei data.ts übernommen und enthalten nur einen Artikel, um den Prozess zu vereinfachen. In der Praxis kann es vorkommen, dass du mehrere Artikel in einem Abonnementauftrag hast.

Um diese Komponente auf der richtigen Route darzustellen, füge den folgenden Code in das Array ein, das an den createBrowserRouter -Aufruf in der Komponente App.tsx übergeben wird:

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

Damit ist die Einrichtung auf dem Frontend abgeschlossen. Im Backend erstellst du eine neue Route /subscription/new, um eine neue gehostete Checkout-Sitzung für ein Abonnementprodukt zu erstellen. Erstelle eine Methode newSubscription im Verzeichnis backend/src/main/java/com/kinsta/stripejava/backend und speichere den folgenden Code darin:

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

Der Code in dieser Methode ist dem Code in der Methode hostedCheckout sehr ähnlich, nur dass der Modus für die Erstellung der Sitzung Abonnement statt Produkt ist und vor der Erstellung der Sitzung ein Wert für das Wiederholungsintervall für das Abonnement festgelegt wird.

Dadurch wird Stripe angewiesen, diese Kasse als Abonnement-Kasse zu behandeln und nicht als Einmalzahlung. Ähnlich wie die Methode hostedCheckout gibt auch diese Methode die URL der gehosteten Checkout-Seite als HTTP-Antwort an den Kunden zurück. Der Client wird auf die empfangene URL umgeleitet, damit der Kunde die Zahlung abschließen kann.

Du kannst die Entwicklungsserver sowohl für den Client als auch für den Server neu starten und die neue Abo-Seite in Aktion sehen. So sieht sie aus:

Ein User Flow, der zeigt, wie eine erfolgreiche Abo-Kaufabwicklung über die von Stripe gehostete Seite aussieht.
Eine gehostete Abonnement-Kaufabwicklung

Ein bestehendes Abonnement kündigen

Nachdem du nun weißt, wie du neue Abonnements erstellen kannst, wollen wir nun lernen, wie du deinen Kunden die Möglichkeit gibst, bestehende Abonnements zu kündigen. Da die Demo-Anwendung in diesem Tutorial keine Authentifizierungseinstellungen enthält, kannst du ein Formular verwenden, in das der Kunde seine E-Mail-Adresse eingeben kann, um seine Abonnements zu überprüfen.

Hierfür musst du Folgendes tun:

  1. Aktualisiere die Komponente CartItem so, dass sie auf der Seite „Abonnements kündigen“ eine Schaltfläche für die Kündigung anzeigt.
  2. Erstelle eine CancelSubscription Komponente, die zunächst ein Eingabefeld und eine Schaltfläche anzeigt, über die der Kunde anhand seiner E-Mail-Adresse nach Abonnements suchen kann, und dann eine Liste der Abonnements mit der aktualisierten CartItem Komponente anzeigt.
  3. Erstelle eine neue Methode im Backend-Server, die anhand der E-Mail-Adresse des Kunden nach Abonnements im Stripe-Backend suchen kann.
  4. Erstelle eine neue Methode im Backend-Server, die ein Abonnement anhand der übergebenen Abonnement-ID kündigen kann.

Beginne damit, die Komponente CartItem zu aktualisieren, damit sie wie folgt aussieht:

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

Als Nächstes erstellst du eine Komponente CancelSubscription.tsx im Routenverzeichnis deines Frontends und speicherst den folgenden Code darin:

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

Diese Komponente zeigt ein Eingabefeld und eine Schaltfläche an, in die Kunden ihre E-Mail eingeben können, um nach Abonnements zu suchen. Wenn Abonnements gefunden werden, werden das Eingabefeld und die Schaltfläche ausgeblendet und eine Liste der Abonnements wird auf dem Bildschirm angezeigt. Für jedes Abonnement übergibt die Komponente eine removeSubscription Methode, die den Java-Backend-Server auffordert, das Abonnement im Stripe-Backend zu kündigen.

Um die Komponente mit der /cancel-subscription -Route deiner Frontend-Anwendung zu verbinden, füge den folgenden Code in das Array ein, das an den createBrowserRouter -Aufruf in der Komponente App übergeben wird:

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

Um nach Abonnements auf dem Backend-Server zu suchen, füge eine viewSubscriptions Methode in der PaymentController Klasse deines Backend-Projekts mit folgendem Inhalt hinzu:

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

Die obige Methode findet zunächst das Kundenobjekt für den angegebenen Nutzer in Stripe. Dann sucht sie nach aktiven Abonnements des Kunden. Sobald sie die Liste der Abonnements erhalten hat, extrahiert sie die Artikel daraus und sucht die entsprechenden Produkte in der Produktdatenbank der Anwendung, um sie an das Frontend zu senden. Das ist wichtig, denn die ID, mit der das Frontend jedes Produkt in der Anwendungs-Datenbank identifiziert, kann mit der in Stripe gespeicherten Produkt-ID übereinstimmen oder auch nicht.

Zum Schluss erstellst du eine cancelSubscription</code method in the PaymentController class and paste the code below to delete a subscription based on the subscription ID passed.

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

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

        Subscription deletedSubscription =
                subscription.cancel();

        return deletedSubscription.getStatus();
    }

Diese Methode ruft das Abonnement-Objekt von Stripe ab, ruft die Cancel-Methode dafür auf und gibt dann den Abonnement-Status an den Kunden zurück. Um diese Methode ausführen zu können, musst du dein DTO-Objekt aktualisieren und das Feld subscriptionId hinzufügen. Dazu fügst du das folgende Feld und die folgende Methode in der Klasse RequestDTO hinzu:

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

}

Sobald du dies hinzugefügt hast, kannst du den Entwicklungsserver sowohl für das Backend als auch für die Frontend-Anwendung erneut starten und den Abbruchvorgang in Aktion sehen:

Ein User Flow, der zeigt, wie eine erfolgreiche Abo-Kündigung über die von Stripe gehostete Seite aussieht.
Ein Abo-Kündigungsfluss

Einrichten von kostenlosen Testversionen für Abonnements mit Nullwerttransaktionen

Die meisten modernen Abonnements bieten eine kurze kostenlose Testphase an, bevor sie dem Nutzer in Rechnung gestellt werden. So können die Nutzer das Produkt oder die Dienstleistung ausprobieren, ohne dafür zu investieren. Es ist jedoch ratsam, die Zahlungsdaten des Kunden zu speichern, wenn er sich für die kostenlose Testphase anmeldet, damit du ihn nach Ablauf der Testphase einfach abrechnen kannst.

Stripe vereinfacht die Erstellung solcher Abonnements erheblich. Um zu beginnen, erstelle eine neue Komponente im Verzeichnis Frontend/Routen mit dem Namen SubscriptionWithTrial.tsx und füge den folgenden Code ein:

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

Diese Komponente verwendet die zuvor erstellten Komponenten wieder. Der Hauptunterschied zwischen dieser Komponente und der Komponente NewSubscription besteht darin, dass sie den Modus für TotalFooter als Testversion und nicht als Abonnement übergibt. Dies bewirkt, dass die Komponente TotalFooter einen Text anzeigt, der besagt, dass der Kunde die kostenlose Testversion jetzt starten kann, aber nach einem Monat bezahlt werden muss.

Um diese Komponente mit der /subscription-with-trial -Route in deiner Frontend-Anwendung zu verbinden, füge den folgenden Code in das Array ein, das dem createBrowserRouter -Aufruf in der App -Komponente übergeben wird:

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

Um den Checkout-Flow für Abonnements mit Testversion im Backend zu erstellen, erstelle eine neue Methode namens newSubscriptionWithTrial in der Klasse PaymentController und füge den folgenden Code hinzu:

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

Dieser Code ist dem der Methode newSubscription sehr ähnlich. Der einzige (und wichtigste) Unterschied besteht darin, dass dem Objekt session create parameters ein Testzeitraum mit dem Wert 30 übergeben wird, der einen kostenlosen Testzeitraum von 30 Tagen angibt.

Du kannst die Änderungen nun speichern und den Entwicklungsserver für das Backend und das Frontend erneut starten, um den Workflow für das Abonnement mit kostenloser Testphase in Aktion zu sehen:

Ein User Flow, der zeigt, wie ein erfolgreicher Checkout für ein Abonnement mit zusätzlicher kostenloser Testversion über die von Stripe gehostete Seite aussieht.
Ein Abonnement mit kostenloser Testphase

Rechnungen für Zahlungen generieren

Bei Abonnements erstellt Stripe automatisch Rechnungen für jede Zahlung, auch wenn es sich um eine Transaktion ohne Wert handelt, wie z.B. bei der Anmeldung zu einer Testversion. Für einmalige Zahlungen kannst du bei Bedarf auch Rechnungen erstellen.

Um alle Zahlungen mit Rechnungen zu verknüpfen, aktualisiere den Body des Payloads, der in der initiatePayment Funktion der CustomerDetails Komponente in der Frontend-Anwendung gesendet wird, um die folgende Eigenschaft zu enthalten:

invoiceNeeded: true

Du musst diese Eigenschaft auch in den Body des Payloads einfügen, der in der Funktion createTransactionSecret der Komponente IntegratedCheckout an den Server gesendet wird.

Als Nächstes aktualisierst du die Backend-Routen, um nach dieser neuen Eigenschaft zu suchen und die Stripe SDK-Aufrufe entsprechend zu aktualisieren.

Für die gehostete Checkout-Methode aktualisierst du die Methode hostedCheckout, indem du die folgenden Codezeilen hinzufügst, um die Rechnungsstellungsfunktionalität hinzuzufügen:

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

Dadurch wird nach dem Feld invoiceNeeded gesucht und die Erstellungsparameter werden entsprechend gesetzt.

Das Hinzufügen einer Rechnung zu einer integrierten Zahlung ist etwas knifflig. Du kannst nicht einfach einen Parameter setzen, der Stripe anweist, automatisch eine Rechnung mit der Zahlung zu erstellen. Du musst die Rechnung manuell erstellen und dann eine verknüpfte Zahlungsabsicht.

Wenn die Zahlungsabsicht erfolgreich bezahlt und abgeschlossen wird, wird die Rechnung als bezahlt markiert; andernfalls bleibt die Rechnung unbezahlt. Das ist zwar logisch, aber die Umsetzung kann etwas kompliziert sein (vor allem, wenn es keine klaren Beispiele oder Verweise gibt, denen du folgen kannst).

Um dies umzusetzen, musst du die Methode integratedCheckout so ändern, dass sie wie folgt aussieht:

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

Der alte Code dieser Methode wird in den Block if verschoben, der prüft, ob das Feld invoiceNeeded false ist. Wenn dies der Fall ist, erstellt die Methode jetzt eine Rechnung mit Rechnungsposten und markiert sie als abgeschlossen, damit sie bezahlt werden kann.

Dann ruft sie die Zahlungsabsicht ab, die automatisch erstellt wurde, als die Rechnung abgeschlossen wurde, und sendet das Kundengeheimnis aus dieser Zahlungsabsicht an den Kunden. Sobald der Kunde die integrierte Kaufabwicklung abgeschlossen hat, wird die Zahlung eingezogen und die Rechnung als bezahlt markiert.

Damit ist die Einrichtung abgeschlossen und du kannst mit der Erstellung von Rechnungen über deine Anwendung beginnen. In deinem Stripe-Dashboard kannst du im Bereich Rechnungen die Rechnungen einsehen, die deine Anwendung bei jedem Kauf und jeder Abo-Zahlung erstellt.

Stripe bietet dir aber auch die Möglichkeit, über seine API auf die Rechnungen zuzugreifen, damit deine Kunden die Rechnungen jederzeit selbst herunterladen können.

Dazu erstellst du eine neue Komponente im Verzeichnis frontend/routes mit dem Namen ViewInvoices.tsx. Füge den folgenden Code darin ein:

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

Ähnlich wie bei der Komponente CancelSubscription zeigt diese Komponente ein Eingabefeld an, in das der Kunde seine E-Mail eingeben kann, sowie eine Schaltfläche, mit der er nach Rechnungen suchen kann. Sobald Rechnungen gefunden wurden, werden das Eingabefeld und die Schaltfläche ausgeblendet und dem Kunden wird eine Liste der Rechnungen mit der Rechnungsnummer, dem Gesamtbetrag und einer Schaltfläche zum Herunterladen des Rechnungs-PDFs angezeigt.

Um die Backend-Methode zu implementieren, die nach Rechnungen eines bestimmten Kunden sucht und die entsprechenden Informationen (Rechnungsnummer, Betrag und PDF-URL) zurückschickt, fügst du die folgende Methode in deine PaymentController Klasse im Backend ein;

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

Die Methode sucht den Kunden zunächst über die angegebene E-Mail-Adresse. Dann sucht sie nach Rechnungen dieses Kunden, die als bezahlt markiert sind. Sobald die Liste der Rechnungen gefunden ist, extrahiert sie die Rechnungsnummer, den Betrag und die PDF-URL und sendet eine Liste mit diesen Informationen an die Kunden-Anwendung zurück.

So sieht der Fluss der Rechnungen aus:

Ein User Flow, der zeigt, wie man Rechnungen für einen Benutzer abruft und darauf zugreift.
Anzeigen von Rechnungen

Damit ist die Entwicklung unserer Java-Demo-Anwendung (Frontend & Backend) abgeschlossen. Im nächsten Abschnitt erfährst du, wie du diese Anwendung auf Kinsta bereitstellst, damit du sie online nutzen kannst.

Bereitstellen deiner Anwendung auf Kinsta

Sobald deine Anwendung fertig ist, kannst du sie auf Kinsta bereitstellen. Kinsta unterstützt Bereitstellungen von deinem bevorzugten Git-Anbieter (Bitbucket, GitHub oder GitLab). Verbinde die Quellcode-Repositories deiner Anwendung mit Kinsta und Kinsta stellt deine Anwendung automatisch bereit, sobald es eine Änderung am Code gibt.

Bereite deine Projekte vor

Um deine Anwendungen in die Produktion zu überführen, musst du die Build- und Deploy-Befehle festlegen, die Kinsta verwenden soll. Für das Frontend musst du sicherstellen, dass in deiner package.json-Datei die folgenden Skripte definiert sind:

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

Außerdem musst du das Paket serve npm installieren, mit dem du statische Websites bereitstellen kannst. Dieses Paket wird verwendet, um den Produktions-Build deiner Anwendung aus der Kinsta-Bereitstellungsumgebung bereitzustellen. Du kannst es installieren, indem du den folgenden Befehl ausführst:

npm i serve

Wenn du deine Anwendung mit vite erstellst, wird die gesamte Anwendung in eine einzige Datei, index.html, gepackt, da die Konfiguration von React, die du in diesem Tutorial verwendest, für die Erstellung von Single Page Applications gedacht ist. Für deine Nutzer/innen macht das zwar keinen großen Unterschied, aber du musst einige zusätzliche Konfigurationen einrichten, um das Browser-Routing und die Navigation in solchen Anwendungen zu steuern.

Mit der aktuellen Konfiguration kannst du deine App nur über die Basis-URL deines Deployments aufrufen. Wenn die Basis-URL des Einsatzes example.com ist, führen alle Anfragen an example.com/some-route zu HTTP 404 Fehlern.

Das liegt daran, dass dein Server nur eine Datei, die index.html-Datei, bereitstellen kann. Eine Anfrage, die an example.com/some-route gesendet wird, sucht nach der Datei some-route/index.html, die nicht existiert und erhält daher die Antwort 404 Not Found.

Um dies zu beheben, erstelle eine Datei namens serve.json in deinem Ordner frontend/public und speichere den folgenden Code darin:

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

Diese Datei weist serve an, alle eingehenden Anfragen so umzuschreiben, dass sie an die Datei index.html weitergeleitet werden, während in der Antwort weiterhin der Pfad angezeigt wird, an den die ursprüngliche Anfrage gesendet wurde. So kannst du die Erfolgs- und Misserfolgsseiten deiner Anwendung korrekt anzeigen, wenn Stripe deine Kunden zu deiner Anwendung zurückleitet.

Für das Backend erstellst du ein Dockerfile, um genau die richtige Umgebung für deine Java-Anwendung einzurichten. Mit einem Dockerfile stellst du sicher, dass die Umgebung für deine Java-Anwendung auf allen Hosts gleich ist (sei es dein lokaler Entwicklungshost oder der Kinsta Bereitstellungs-Host) und du kannst sicherstellen, dass deine Anwendung wie erwartet läuft.

Dazu erstellst du eine Datei namens Dockerfile im Backend-Ordner und speicherst den folgenden Inhalt darin:

FROM openjdk:22-oraclelinux8

LABEL maintainer="krharsh17"

WORKDIR /app

COPY . /app

RUN ./mvnw clean package

EXPOSE 8080

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

Diese Datei weist die Runtime an, das OpenJDK-Java-Image als Basis für den Deployment-Container zu verwenden, den Befehl ./mvnw clean package auszuführen, um die JAR-Datei deiner Anwendung zu bauen, und den Befehl java -jar <jar-file> zu verwenden, um sie auszuführen. Damit ist die Vorbereitung des Quellcodes für die Bereitstellung auf Kinsta abgeschlossen.

GitHub-Repositories einrichten

Um mit der Bereitstellung der Anwendungen zu beginnen, musst du zwei GitHub-Repositories einrichten, in denen der Quellcode deiner Anwendungen gespeichert wird. Wenn du das GitHub CLI verwendest, kannst du dies über das Terminal tun, indem du die folgenden Befehle ausführst:

# 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

Damit solltest du neue GitHub-Repositories in deinem Konto erstellen und den Code deiner Anwendungen dorthin pushen. Du solltest nun auf die Frontend- und Backend-Repositories zugreifen können. Als Nächstes stellst du diese Repositories auf Kinsta bereit, indem du die folgenden Schritte ausführst:

  1. Melde dich im MyKinsta-Dashboard an oder erstelle dein Kinsta-Konto.
  2. Klicke in der linken Seitenleiste auf Anwendungen und dann auf Anwendung hinzufügen.
  3. Wähle in dem daraufhin angezeigten Fenster das Repository aus, das du bereitstellen möchtest. Wenn du mehrere Zweige hast, kannst du den gewünschten Zweig auswählen und einen Namen für deine Anwendung vergeben.
  4. Wähle einen der verfügbaren Rechenzentrumsstandorte aus der Liste der 25 Optionen. Kinsta erkennt automatisch den Startbefehl für deine Anwendung.

Denke daran, dass du sowohl deine Frontend- als auch deine Backend-Anwendung mit einigen Umgebungsvariablen ausstatten musst, damit sie richtig funktionieren. Die Frontend-Anwendung benötigt die folgenden Umgebungsvariablen:

  • VITE_STRIPE_API_KEY
  • VITE_SERVER_BASE_URL
  • VITE_CLIENT_BASE_URL

Für die Bereitstellung der Backend-Anwendung tust du genau das, was wir für das Frontend getan haben, aber für den Schritt Build environment wählst du das Optionsfeld Use Dockerfile to set up container image aus und gibst Dockerfile als Pfad für die Dockerdatei deiner Backend-Anwendung ein.

Im Antragsformular für das Hinzufügen wirst du aufgefordert, Angaben zur Build-Umgebung zu machen.
Einstellen der Details der Build-Umgebung

Vergiss nicht, die Backend-Umgebungsvariablen hinzuzufügen:

  • CLIENT_BASE_URL
  • STRIPE_API_KEY

Sobald die Bereitstellung abgeschlossen ist, rufe die Detailseite deiner Anwendung auf und rufe dort die URL der Bereitstellung auf.

Die Anwendungs-Detailseite mit einem roten Kasten, der anzeigt, wo du die URL des Einsatzes findest.
Die gehostete URL für die auf Kinsta bereitgestellten Anwendungen

Extrahiere die URLs für die beiden bereitgestellten Anwendungen. Gehe zum Stripe-Dashboard, um deine geheimen und veröffentlichbaren API-Schlüssel zu erhalten.

Achte darauf, dass du deiner Frontend-Anwendung den veröffentlichbaren Stripe-Schlüssel zur Verfügung stellst (nicht den geheimen Schlüssel). Achte außerdem darauf, dass deine Basis-URLs keinen Schrägstrich (/) am Ende haben. Die Routen haben bereits führende Schrägstriche, so dass ein nachfolgender Schrägstrich am Ende der Basis-URLs dazu führt, dass zwei Schrägstriche zu den endgültigen URLs hinzugefügt werden.

Für deine Backend-Anwendung fügst du den geheimen Schlüssel aus dem Stripe-Dashboard hinzu (nicht den veröffentlichbaren Schlüssel). Achte außerdem darauf, dass deine Client-URL keinen Schrägstrich (/) am Ende hat.

Sobald die Variablen hinzugefügt sind, gehst du auf die Registerkarte Anwendungsbereitstellungen und klickst auf die Schaltfläche Neu bereitstellen für deine Backend-Anwendung. Damit ist die einmalige Einrichtung abgeschlossen, die du benötigst, um deine Kinsta-Einsätze über Umgebungsvariablen mit Anmeldeinformationen zu versorgen.

Nun kannst du die Änderungen in deine Versionskontrolle übertragen. Kinsta wird deine Anwendung automatisch neu bereitstellen, wenn du die Option beim Deployment aktiviert hast; andernfalls musst du die Neubereitstellung manuell auslösen.

Zusammenfassung

In diesem Artikel hast du gelernt, wie Stripe funktioniert und welche Zahlungsströme es bietet. Außerdem hast du anhand eines detaillierten Beispiels gelernt, wie du Stripe in deine Java-Anwendung integrierst, um Einmalzahlungen zu akzeptieren, Abonnements einzurichten, kostenlose Testversionen anzubieten und Rechnungen zu erstellen.

Wenn du Stripe und Java zusammen verwendest, kannst du deinen Kunden eine robuste Zahlungslösung anbieten, die gut skalierbar ist und sich nahtlos in dein bestehendes Ökosystem von Anwendungen und Tools integrieren lässt.

Nutzt du Stripe in deiner Anwendung, um Zahlungen einzuziehen? Wenn ja, welche der beiden Möglichkeiten bevorzugst du: gehostet, benutzerdefiniert oder in der Anwendung? Lass es uns in den Kommentaren unten wissen!

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