Com o aumento das transações digitais, a capacidade de integrar perfeitamente os gateways de pagamento se tornou uma habilidade essencial para os desenvolvedores. Seja para marketplaces ou produtos SaaS, um processador de pagamentos é crucial para coletar e processar os pagamentos dos usuários.

Este artigo explica como integrar o Stripe em um ambiente Spring Boot, como configurar assinaturas, oferecer testes gratuitos e criar páginas de autoatendimento para que seus clientes façam download das faturas de pagamento.

O que é o Stripe?

O Stripe é uma plataforma de processamento de pagamentos reconhecida mundialmente e disponível em 46 países. É uma ótima opção se você quiser criar uma integração de pagamento em seu aplicativo web devido ao seu grande alcance, ótima reputação e documentação detalhada.

Entendendo os conceitos comuns do Stripe

É útil entender alguns conceitos comuns que o Stripe usa para coordenar e realizar operações de pagamento entre várias partes. O Stripe oferece duas abordagens para você implementar a integração de pagamentos em seu aplicativo.

Você pode incorporar os formulários do Stripe no seu aplicativo para uma experiência de cliente dentro do aplicativo (Payment Intent) ou redirecionar os clientes para uma página de pagamento hospedada pelo Stripe, onde o Stripe gerencia o processo e informa ao seu aplicativo quando um pagamento é bem-sucedido ou não (Payment Link).

Payment Intent (intenção de pagamento)

Ao lidar com pagamentos, é importante reunir os detalhes do cliente e do produto antecipadamente, antes de solicitar as informações do cartão e o pagamento. Esses detalhes incluem a descrição, o valor total, a forma de pagamento e muito mais.

O Stripe requer que você colete esses detalhes no seu aplicativo e gere um objeto PaymentIntent no backend. Essa abordagem permite que o Stripe formule uma solicitação de pagamento para essa intenção. Após a conclusão do pagamento, você pode recuperar consistentemente os detalhes do pagamento, inclusive sua finalidade, por meio do objeto PaymentIntent.

Para evitar as complexidades de integrar o Stripe diretamente à sua base de código, considere utilizar o Stripe Checkouts para uma solução de pagamento hospedada. Assim como na criação de um PaymentIntent, você criará um CheckoutSession com detalhes do pagamento e do cliente. Em vez de iniciar um PaymentIntent no aplicativo, o CheckoutSession gera um link de pagamento para o qual você redireciona seus clientes. Esta é a aparência de uma página de pagamento hospedada:

A página de checkout hospedada pelo Stripe.
A página de checkout hospedada pelo Stripe.

Após o pagamento, o Stripe redireciona de volta para o seu aplicativo, permitindo tarefas pós-pagamento, como confirmações e solicitações de entrega. Para maior confiabilidade, configure um webhook de backend para atualizar o Stripe, garantindo a retenção dos dados de pagamento mesmo que os clientes fechem acidentalmente a página após o pagamento.

Embora eficaz, esse método carece de flexibilidade de personalização e design. Também pode ser difícil configurá-lo corretamente para aplicativos para dispositivos móveis, em que uma integração nativa pareceria muito mais perfeita.

Chaves de API

Ao trabalhar com a API do Stripe, você precisará de acesso às chaves de API para que seus aplicativos cliente e servidor interajam com o backend do Stripe. Você pode acessar suas chaves de API do Stripe no seu painel de desenvolvedor do Stripe. Veja como ficaria:

O painel do Stripe mostrando as chaves de API.
O painel do Stripe mostrando as chaves de API.

Como funcionam os pagamentos no Stripe?

Para entender como funcionam os pagamentos no Stripe, você precisa entender todas as partes interessadas envolvidas. Quatro participantes estão envolvidos em cada transação de pagamento:

  1. Cliente (comprador): A pessoa que pretende pagar por um serviço/produto.
  2. Comerciante: Você, o proprietário da empresa, é responsável por receber pagamentos e vender serviços/produtos.
  3. Adquirente: Um banco que processa pagamentos em seu nome (do comerciante) e encaminha sua solicitação de pagamento para o banco dos seus clientes. Os adquirentes podem fazer parcerias com terceiros para ajudar a processar os pagamentos.
  4. Banco emissor: O banco que concede crédito e emite cartões e outros métodos de pagamento para os consumidores.

Aqui está um fluxo de pagamento típico entre essas partes interessadas em um nível muito alto.

Como funcionam os pagamentos on-line.
Como funcionam os pagamentos on-line.

O cliente informa ao comerciante que está disposto a pagar. Então o comerciante encaminha os detalhes relacionados ao pagamento para o banco adquirente, que coleta o pagamento do banco emissor do cliente e informa ao comerciante que o pagamento foi bem-sucedido.

Essa é uma visão geral de altíssimo nível do processo de pagamento. Como comerciante, você só precisa se preocupar em coletar a intenção de pagamento, passá-la para o processador de pagamento e lidar com o resultado do pagamento. Entretanto, conforme discutido anteriormente, há duas maneiras de fazer isso.

Ao criar uma sessão de checkout gerenciada pelo Stripe, na qual o Stripe cuida da coleta dos detalhes do pagamento, o fluxo típico fica assim:

O fluxo de trabalho de pagamento no checkout hospedado pelo Stripe.
O fluxo de trabalho de pagamento no checkout hospedado pelo Stripe. (Fonte: Stripe Docs).

Com os fluxos de pagamento personalizados, a decisão é realmente sua. Você pode projetar a interação entre cliente e servidor, o comprador e a API do Stripe com base nas necessidades do seu aplicativo. Você pode adicionar a esse fluxo de trabalho a coleta de endereço, geração de fatura, cancelamento, avaliações gratuitas, etc., conforme necessário.

Agora que você entende como funcionam os pagamentos do Stripe, está pronto para começar a criá-lo em seu aplicativo Java.

Integração do Stripe no aplicativo Spring Boot

Para iniciar a integração com o Stripe, você deve criar um aplicativo de frontend para interagir com o backend Java e iniciar os pagamentos. Neste tutorial, você criará um aplicativo React para acionar vários tipos de pagamentos e assinaturas, de modo a obter uma compreensão clara dos seus mecanismos.

Observação: este tutorial não abordará a criação de um site de eCommerce completo; o objetivo principal é orientá-lo no simples processo de integração do Stripe ao Spring Boot.

Configurando os projetos de frontend e backend

Crie um novo diretório e estruture um projeto React usando o Vite, executando o seguinte comando:

npm create vite@latest

Defina o nome do projeto como frontend (ou outro nome, se preferir), o framework como React e a variante como TypeScript. Navegue até o diretório do projeto e instale o Chakra UI para a rápida estruturação de elementos de interface de usuário executando o seguinte comando:

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

Você também instalará o react-router-dom em seu projeto para roteamento no lado do cliente executando o comando abaixo:

npm i react-router-dom

Agora você está pronto para começar a criar seu aplicativo de frontend. Esta é a página inicial que você vai criar.

A página inicial concluída do aplicativo frontend.
A página inicial concluída do aplicativo frontend.

Ao clicar em qualquer botão dessa página, você será levado a páginas de checkout separadas com formulários de pagamento. Para começar, crie uma nova pasta chamada routes em seu diretório frontend/src. Dentro dessa pasta, crie um arquivo Home.tsx. Esse arquivo conterá o código do caminho inicial do seu aplicativo (/). Cole o seguinte código no arquivo:

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

Para ativar a navegação em seu aplicativo, atualize o arquivo App.tsx para configurar a classe RouteProvider de react-router-dom.

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

function App() {

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

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

export default App

Execute o comando npm run dev para visualizar seu aplicativo em https://localhost:5173.

Isso conclui a configuração inicial necessária para o aplicativo de frontend. Em seguida, crie um aplicativo de backend usando o Spring Boot. Para inicializar o aplicativo, você pode usar o site spring initializr (se o seu IDE oferecer suporte à criação de aplicativos Spring, você não precisará usar o site).

O IntelliJ IDEA oferece suporte à criação de aplicativos Spring Boot. Comece escolhendo a opção New Project no IntelliJ IDEA. Em seguida, escolha Spring Initializr no painel esquerdo. Insira os detalhes do seu projeto de backend: Name (backend), Location (o diretório stripe-payments-java), Language (Java), e Type (Maven). Para os nomes de Group e Artifact, use com.kinsta.stripe-java e backend, respectivamente.

A caixa de diálogo do novo projeto IDEA.
A caixa de diálogo do novo projeto IDEA.

.

Clique no botão Next. Daí adicione dependências ao seu projeto escolhendo Spring Web no menu suspenso Web no painel de dependências, e clique no botão Create.

Escolhendo as dependências.
Escolhendo as dependências.

Isso criará o projeto Java e o abrirá em seu IDE. Agora você pode prosseguir com a criação dos vários fluxos de checkout usando o Stripe.

Aceitando pagamentos on-line para compras de produtos

A funcionalidade mais importante e amplamente utilizada do Stripe é aceitar pagamentos únicos de clientes. Nesta seção, você aprenderá duas maneiras de integrar o processamento de pagamentos no seu aplicativo com o Stripe.

Checkout hospedado

Primeiro, você cria uma página de checkout que aciona um fluxo de trabalho de pagamento hospedado em que você só aciona um pagamento a partir do seu aplicativo frontend. O Stripe então se encarrega de coletar os detalhes do cartão do cliente e o pagamento, compartilhando apenas o resultado da operação de pagamento no final.

Esta é a aparência da página de checkout:

A página concluída do checkout hospedado.
A página concluída do checkout hospedado.

Essa página tem três componentes principais: CartItem — representa cada item no carrinho; TotalFooter — exibe o valor total; CustomerDetails — coleta os detalhes do cliente. Você pode reutilizar esses componentes para criar formulários de checkout para outros cenários neste artigo, tais como checkout integrado e assinaturas.

Criação do frontend

Crie uma pasta components em seu diretório frontend/src. Na pasta components, crie um novo arquivo CartItem.tsx e cole o código a seguir:

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

O código acima define duas interfaces a serem usadas como tipos para as propriedades passadas para o componente. O tipo ItemData é exportado para ser reutilizado em outros componentes.

O código retorna o layout de um componente de item de carrinho. Ele utiliza as propriedades fornecidas para renderizar o item na tela.

Em seguida, crie um arquivo TotalFooter.tsx no diretório components e cole o seguinte código:

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

O componente TotalFooter exibe o valor total do carrinho e usa o valor mode para renderizar condicionalmente um texto específico.

Por fim, crie o componente CustomerDetails.tsx e cole o seguinte código:

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

O código acima exibe um formulário com dois campos de entrada — para coletar o nome e o e-mail do usuário. Quando o botão Checkout é clicado, o método initiatePayment é chamado para enviar a solicitação de checkout para o backend.

Ele solicita o endpoint que você passou para o componente e envia as informações do cliente e os itens do carrinho como parte da solicitação e, em seguida, redireciona o usuário para a URL recebida do servidor. Essa URL levará o usuário a uma página de checkout hospedada no servidor do Stripe. Você verá como criar essa URL logo mais.

Observação: esse componente usa a variável de ambiente VITE_SERVER_BASE_URL para a URL do servidor backend. Defina-a criando um arquivo .env na raiz do seu projeto:

VITE_SERVER_BASE_URL=http://localhost:8080

Todos os componentes foram criados. Agora vamos prosseguir para construir o caminho do checkout hospedado usando os componentes. Para fazer isso, crie um novo arquivo HostedCheckout.tsx na pasta routes com o seguinte código:

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

Esse caminho utiliza os três componentes que você acabou de criar para montar uma tela de checkout. Todos os modos do componente estão configurados como checkout, e o endpoint /checkout/hosted é fornecido ao componente de formulário para iniciar a solicitação de checkout com precisão.

O componente usa um objeto Products para preencher o array de itens. Em cenários reais, esses dados vêm da API do carrinho, que contém os itens selecionados pelo usuário. No entanto, para este tutorial, uma lista estática de um script preenche o array. Defina o array criando um arquivo data.ts na raiz do seu projeto de frontend e armazenando o seguinte código nele:

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

Esse arquivo define dois itens no array de produtos que são renderizados no carrinho. Você pode ajustar os valores dos produtos.

Como última etapa da criação do frontend, crie duas novas rotas para lidar com o sucesso e o fracasso. A página de checkout hospedada pelo Stripe redirecionará os usuários para o seu aplicativo nessas duas rotas com base no resultado da transação. O Stripe também fornecerá às suas rotas um payload relacionado à transação, como o ID da sessão de checkout, que você pode usar para recuperar o objeto da sessão de checkout correspondente e acessar dados relacionados ao checkout, como o método de pagamento, detalhes da fatura, etc.

Para fazer isso, crie um arquivo Success.tsx no diretório src/routes e salve o seguinte código nele:

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

Após a renderização, esse componente mostra a mensagem “Success!” e imprime na tela todos os parâmetros de consulta de URL. Ele também inclui um botão para redirecionar os usuários para a página inicial do aplicativo.

Ao criar aplicativos do mundo real, essa página é onde você lidaria com transações não críticas do lado do aplicativo que dependem do sucesso da transação em questão. Por exemplo, se você estiver criando uma página de checkout para uma loja on-line, poderá usar essa página para mostrar uma confirmação ao usuário e um prazo de entrega estimado dos produtos comprados.

Em seguida, crie um arquivo Failure.tsx com o seguinte código:

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

Esse componente é semelhante ao do Success.tsx e exibe a mensagem “Failure!” (Falha!) quando renderizado.

Para tarefas essenciais, como entrega de produtos, envio de e-mails ou qualquer parte crítica do seu fluxo de compras, use webhooks. Webhooks são rotas de API em seu servidor que o Stripe pode invocar quando ocorre uma transação.

O webhook recebe detalhes completos da transação (por meio do objeto CheckoutSession), permitindo que você a registre no banco de dados do seu aplicativo e acione os fluxos de trabalho de sucesso ou falha correspondentes. Como o seu servidor está sempre acessível ao Stripe, nenhuma transação é perdida, garantindo a funcionalidade consistente da sua loja on-line.

Por fim, atualize o arquivo App.tsx para que ele fique parecido com este:

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

Isso garantirá que os componentes Success e Failure sejam renderizados nas rotas /success e /failure, respectivamente.

Isso conclui a configuração do frontend. Em seguida, configure o backend para criar o endpoint /checkout/hosted.

Criando o backend

Abra o projeto de backend e instale o Stripe SDK adicionando as seguintes linhas ao array de dependências no seu arquivo pom.xml:

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

Em seguida, carregue as alterações do Maven em seu projeto para instalar as dependências. Se o seu IDE não for compatível com isso por meio da interface do usuário, execute o comando maven dependency:resolve ou maven install. Se você não tiver a CLI maven, use o wrapper mvnw do Spring initializr ao criar o projeto.

Depois que as dependências estiverem instaladas, crie um novo controlador REST para tratar as solicitações HTTP de entrada para o seu aplicativo de backend. Para fazer isso, crie um arquivo PaymentController.java no diretório src/main/java/com/kinsta/stripe-java/backend e adicione o seguinte código:

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

}

O código acima importa as dependências essenciais do Stripe e estabelece a classe PaymentController. Essa classe tem duas anotações: @RestController e @CrossOrigin. A anotação @RestController instrui o Spring Boot a tratar essa classe como um controlador, e seus métodos agora podem usar anotações @Mapping para lidar com solicitações HTTP de entrada.

A anotação @CrossOrigin marca todos os endpoints definidos nessa classe como abertos a todas as origens conforme as regras CORS. No entanto, essa prática não é recomendada na produção devido às possíveis vulnerabilidades de segurança de vários domínios da internet.

Para obter os melhores resultados, é recomendável hospedar os servidores de backend e frontend no mesmo domínio para contornar os problemas de CORS. Como alternativa, se isso não for viável, você pode especificar o domínio do seu cliente de frontend (que envia solicitações ao servidor de backend) usando a anotação @CrossOrigin, assim:

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

A classe PaymentController extrairá a chave da API do Stripe das variáveis de ambiente para que você possa fornecê-la ao Stripe SDK posteriormente. Ao executar o aplicativo, você deve fornecer a chave da API do Stripe ao aplicativo por meio de variáveis de ambiente.

Localmente, você pode criar uma nova variável de ambiente no seu sistema temporariamente (adicionando uma frase KEY=VALUE antes do comando usado para iniciar o servidor de desenvolvimento) ou permanentemente (atualizando os arquivos de configuração do terminal ou definindo uma variável de ambiente no painel de controle no Windows).

Em ambientes de produção, seu provedor de implantação (como a Kinsta) fornecerá a você uma opção separada para preencher as variáveis de ambiente usadas pelo seu aplicativo.

Se você estiver usando o IntelliJ IDEA (ou um IDE semelhante), clique em Run Configurations na parte superior direita do IDE e clique na opção Edit Configurations… na lista suspensa que se abre para atualizar o seu comando de execução e definir a variável de ambiente.

Abrindo a caixa de diálogo de configurações de execução/depuração.
Abrindo a caixa de diálogo de configurações de execução/depuração.

.

Isso abrirá uma caixa de diálogo na qual você poderá fornecer as variáveis de ambiente para o aplicativo usando o campo Environment variables. Digite a variável de ambiente STRIPE_API_KEY no formato VAR1=VALUE. Você pode encontrar sua chave de API no site Stripe Developers. Você deve fornecer o valor da Secret Key dessa página.

O painel do Stripe que mostra as chaves de API.
O painel do Stripe que mostra as chaves de API.

Se ainda não o fez, crie uma nova conta do Stripe para obter acesso às chaves da API.

Depois que você tiver configurado a chave de API, prossiga para criar o endpoint. Esse endpoint coletará os dados do cliente (nome e e-mail), criará um perfil de cliente para ele no Stripe, caso ainda não exista, e criará uma sessão de checkout para permitir que os usuários paguem pelos itens do carrinho.

Aqui está o código do método hostedCheckout:

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

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

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

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

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

        }

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

        return session.getUrl();
    }

Ao criar a sessão de checkout, o código usa o nome do produto recebido do cliente, mas não usa os detalhes de preço da solicitação. Essa abordagem evita a possível manipulação de preços no lado do cliente, em que agentes mal-intencionados podem enviar preços reduzidos na solicitação de checkout para pagar menos por produtos e serviços.

Para evitar isso, o método hostedCheckout consulta seu banco de dados de produtos (via ProductDAO) para recuperar o preço correto do item.

Além disso, o Stripe oferece várias classes Builder seguindo o padrão de design builder. Essas classes auxiliam na criação de objetos de parâmetro para solicitações ao Stripe. O snippet de código fornecido também faz referência a variáveis de ambiente para buscar a URL do aplicativo cliente. Essa URL é necessária para que o objeto da sessão de checkout seja redirecionado adequadamente após pagamentos bem-sucedidos ou não.

Para executar esse código, defina a URL do aplicativo cliente por meio de variáveis de ambiente, da mesma forma como a chave da API do Stripe foi fornecida. Como o aplicativo cliente é executado por meio do Vite, a URL do aplicativo local deve ser http://localhost:5173. Inclua isso nas variáveis de ambiente por meio do IDE, do terminal ou do painel de controle do sistema.

CLIENT_BASE_URL=http://localhost:5173

Além disso, forneça ao aplicativo um ProductDAO do qual você possa consultar os preços dos produtos. O Data Access Object (DAO) interage com fontes de dados (como bancos de dados) para acessar dados relacionados ao aplicativo. Embora a configuração de um banco de dados de produtos esteja fora do escopo deste tutorial, uma implementação simples que você pode fazer seria adicionar um novo arquivo ProductDAO.java no mesmo diretório que o PaymentController.java e colar o seguinte código:

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

    }
}

Isso inicializará um array de produtos e permitirá que você consulte os dados do produto usando seu identificador (ID). Você também precisará criar um DTO (Data Transfer Object) para permitir que o Spring Boot serialize automaticamente o payload de entrada do cliente e apresente a você um objeto simples para acessar os dados. Para fazer isso, crie um novo arquivo RequestDTO.java e cole o código a seguir:

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

}

Esse arquivo define um POJO que contém o nome do comprador, o e-mail e a lista de itens com os quais ele está fazendo o checkout.

Por fim, implemente o método CustomerUtil.findOrCreateCustomer() para criar o objeto Customer no Stripe, caso ele ainda não exista. Para fazer isso, crie um arquivo com o nome CustomerUtil e adicione o seguinte código a ele:

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

Essa classe também contém outro método findCustomerByEmail que permite que você procure clientes no Stripe usando seus endereços de e-mail. A API Customer Search é usada para procurar os registros de clientes no banco de dados do Stripe e a API Customer Create é usada para criar os registros de clientes conforme necessário.

Com isso, você conclui a configuração de backend necessária para o fluxo de checkout hospedado. Agora você pode testar o aplicativo executando os aplicativos frontend e backend em seus IDEs ou terminais separados. Aqui está a aparência do fluxo de sucesso:

Um fluxo de checkout hospedado bem-sucedido.
Um fluxo de checkout hospedado bem-sucedido.

Ao testar integrações com o Stripe, você sempre pode usar os seguintes detalhes de cartão para simular transações com cartão:

Número do cartão: 4111 1111 1111 1111 1111
Mês e ano de validade: 12 / 25
CVV: [qualquer número de três dígitos]
Nome no cartão: [qualquer nome]

Se você optar por cancelar a transação em vez de pagar, veja como seria o fluxo de falha:

Um fluxo de checkout hospedado com falha.
Um fluxo de checkout hospedado com falha.

Isso conclui a configuração de uma experiência de checkout hospedado no Stripe integrado ao seu aplicativo. Você pode consultar os documentos do Stripe para saber mais sobre como personalizar sua página de checkout, coletar mais detalhes do cliente, e mais.

Checkout integrado

Uma experiência de checkout integrado se refere à criação de um fluxo de pagamento que não redireciona o usuário para fora do seu aplicativo (como no fluxo de checkout hospedado) e renderiza o formulário de pagamento no próprio aplicativo.

Criar uma experiência de checkout integrado significa lidar com os detalhes de pagamento dos clientes, o que envolve informações sensíveis, como números de cartão de crédito, ID do Google Pay, etc. Nem todos os aplicativos são projetados para lidar com esses dados de forma segura.

Para eliminar o ônus de atender a padrões como o PCI-DSS, o Stripe fornece elementos que você pode usar no aplicativo para coletar detalhes de pagamento, ao mesmo tempo deixando que o Stripe gerencie a segurança e processe os pagamentos de forma segura.

Criando o frontend

Para começar, instale o Stripe React SDK em seu aplicativo de frontend para acessar os Elementos Stripe executando o seguinte comando no diretório de frontend:

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

Em seguida, crie um novo arquivo chamado IntegratedCheckout.tsx em seu diretório frontend/src/routes e salve o seguinte código nele:

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

Esse arquivo define dois componentes, IntegratedCheckout e CheckoutForm. O CheckoutForm define um formulário simples com um PaymentElement do Stripe que coleta os detalhes de pagamento dos clientes e um botão Pay que aciona uma solicitação de coleta de pagamento.

Esse componente também chama os hooks useStripe() e useElements() para criar uma instância do Stripe SDK que você pode usar para criar solicitações de pagamento. Ao clicar no botão Pay, o método stripe.confirmPayment() do Stripe SDK é chamado, coletando os dados de pagamento do usuário da instância dos elementos e enviando-os ao backend do Stripe com uma URL de sucesso para redirecionamento caso a transação seja bem-sucedida.

O formulário de checkout foi separado do restante da página porque os hooks useStripe() e useElements() precisam ser chamados do contexto de um provedor Elements, o que foi feito na instrução de retorno IntegratedCheckout. Se você movesse as chamadas do hook do Stripe diretamente para o componente IntegratedCheckout, elas estariam fora do escopo do provedor Elements e, portanto, não funcionariam.

O componente IntegratedCheckout reutiliza os componentes CartItem e TotalFooter para renderizar os itens do carrinho e o valor total. Ele também renderiza dois campos de entrada para coletar as informações do cliente e um botão Initiate payment que envia uma solicitação ao servidor backend do Java para criar a chave secreta do cliente usando os detalhes do cliente e do carrinho. Assim que a chave secreta do cliente é recebida, o CheckoutForm é renderizado para lidar com a coleta dos detalhes de pagamento do cliente.

Além disso, o useEffect é usado para chamar o método loadStripe. Esse efeito é executado apenas uma vez quando o componente é renderizado, para que o Stripe SDK não seja carregado várias vezes quando os estados internos do componente forem atualizados.

Para executar o código acima, você também precisará adicionar duas novas variáveis de ambiente ao seu projeto de frontend: VITE_STRIPE_API_KEY e VITE_CLIENT_BASE_URL. A variável de chave da API do Stripe manterá a chave da API publicável do painel do Stripe, e a variável de URL da base do cliente conterá o link para o aplicativo cliente (que é o próprio aplicativo frontend), de modo que possa ser passado para o SDK do Stripe para lidar com redirecionamentos de sucesso e falha.

Para fazer isso, adicione o seguinte código ao seu arquivo .env no diretório frontend:

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

Por fim, atualize o arquivo App.tsx para incluir o componente IntegratedCheckout na rota /integrated-checkout do aplicativo de frontend. Adicione o seguinte código no array passado para a chamada createBrowserRouter no componente App:

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

Isso conclui a configuração necessária no frontend. Em seguida, crie uma nova rota no servidor de backend que cria a chave secreta do cliente necessária para lidar com sessões de checkout integradas no aplicativo de frontend.

Criação do backend

Para garantir que a integração de frontend não seja abusada por invasores (já que o código do frontend é mais fácil de ser violado que o do backend), o Stripe exige que você gere um segredo de cliente exclusivo no seu servidor backend e verifica cada solicitação de pagamento integrado com o segredo de cliente gerado no backend para garantir que é realmente o seu aplicativo que está tentando coletar pagamentos. Para fazer isso, você precisa configurar outro caminho no backend que crie segredos de cliente com base nas informações do cliente e do carrinho.

Para criar a chave secreta do cliente em seu servidor, crie um novo método em sua classe PaymentController com o nome integratedCheckout e salve o seguinte código nele:

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

        Stripe.apiKey = STRIPE_API_KEY;

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

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

        PaymentIntent paymentIntent = PaymentIntent.create(params);

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

De maneira similar àquela como a sessão de checkout foi criada usando uma classe builder que aceita a configuração para a solicitação de pagamento, o fluxo de checkout integrado exige que você crie uma sessão de pagamento com o valor, a moeda e os métodos de pagamento. Ao contrário da sessão de checkout, você não pode associar itens de linha a uma sessão de pagamento, a menos que crie uma fatura, o que você aprenderá em uma seção posterior deste tutorial.

Como você não está passando os itens de linha para o construtor da sessão de checkout, precisa calcular manualmente o valor total dos itens do carrinho e enviar o valor para o backend do Stripe. Use o ProductDAO para localizar e adicionar os preços de cada produto no carrinho.

Para fazer isso, defina um novo método calculateOrderAmount e adicione o seguinte código a ele:

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

Isso deve ser suficiente para configurar o fluxo de checkout integrado no frontend e no backend. Você pode reiniciar os servidores de desenvolvimento para o servidor e o cliente e experimentar o novo fluxo de checkout integrado no aplicativo frontend. É assim que o fluxo integrado se parece:

Um fluxo de checkout integrado.
Um fluxo de checkout integrado.

Isso completa um fluxo básico de checkout integrado em seu aplicativo. Agora você pode ir além e explorar a documentação do Stripe para personalizar os métodos de pagamento ou integrar mais componentes para ajudá-lo com outras operações, tais como coleta de endereços, solicitações de pagamento, integração de links e muito mais!

Configuração de assinaturas para serviços recorrentes

Atualmente, uma oferta comum das lojas on-line é a assinatura. Se você estiver criando um mercado para serviços ou oferecendo um produto digital periodicamente, a assinatura é a solução perfeita para dar aos seus clientes acesso periódico ao seu serviço por uma taxa pequena em comparação com uma compra única.

O Stripe pode ajudá-lo a configurar e cancelar assinaturas facilmente. Você também pode oferecer avaliações gratuitas como parte da assinatura para que os usuários possam experimentar sua oferta antes de se comprometerem com ela.

Configuração de uma nova assinatura

A configuração de uma nova assinatura é simples usando o fluxo de checkout hospedado. Você só precisará alterar alguns parâmetros ao criar a solicitação de checkout e criar uma nova página (reutilizando os componentes existentes) para mostrar uma página de checkout para uma nova assinatura. Para começar, crie um arquivo NewSubscription.tsx na pasta components do frontend. Cole o seguinte código nele:

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

No código acima, os dados do carrinho são obtidos do arquivo data.ts e contêm apenas um item para simplificar o processo. Em cenários reais, você pode ter vários itens como parte de um pedido de assinatura.

Para renderizar esse componente no caminho correto, adicione o seguinte código no array passado para a chamada createBrowserRouter no componente App.tsx:

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

Isso conclui a configuração necessária no frontend. No backend, crie um novo caminho /subscription/new para criar uma nova sessão de checkout hospedada para um produto de assinatura. Crie um método newSubscription no diretório backend/src/main/java/com/kinsta/stripejava/backend e salve o seguinte código nele:

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

O código nesse método é bastante semelhante ao código no método hostedCheckout, exceto pelo fato de que o modo definido para criar a sessão é assinatura em vez de produto, e, antes de criar a sessão, um valor é definido para o intervalo de recorrência da assinatura.

Isso instrui o Stripe a tratar esse checkout como um checkout de assinatura em vez de um pagamento único. Semelhante ao método hostedCheckout, esse método também retorna a URL da página do checkout hospedado como resposta HTTP para o cliente. O cliente é configurado para redirecionar para a URL recebida, permitindo que o comprador conclua o pagamento.

Você pode reiniciar os servidores de desenvolvimento para cliente e servidor e ver a nova página de assinatura em ação. Aqui está o que você vê:

Um fluxo de checkout hospedado de assinatura.
Um fluxo de checkout hospedado de assinatura.

Cancelamento de uma assinatura existente

Agora que você sabe como criar novas assinaturas, vamos aprender como permitir que seus clientes cancelem assinaturas existentes. Como o aplicativo de demonstração criado neste tutorial não contém nenhuma configuração de autenticação, use um formulário para permitir que o cliente insira seu e-mail para procurar suas assinaturas e, em seguida, forneça a cada item de assinatura um botão de cancelamento para que o usuário possa cancelá-lo.

Para fazer isso, você precisará fazer o seguinte:

  1. Atualize o componente CartItem para mostrar um botão de cancelamento na página de cancelamento de assinaturas.
  2. Crie um componente CancelSubscription que primeiro mostre um campo de entrada e um botão para o cliente pesquisar assinaturas usando seu endereço de e-mail e, em seguida, renderize uma lista de assinaturas usando o componente CartItem atualizado.
  3. Crie um novo método no servidor de backend que possa procurar assinaturas no backend do Stripe usando o endereço de e-mail do cliente.
  4. Crie um novo método no servidor de backend que possa cancelar uma assinatura com base no ID de assinatura passado a ele.

Comece atualizando o componente CartItem para que ele fique parecido com este:

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

Em seguida, crie um componente CancelSubscription.tsx no diretório routes do frontend e salve o seguinte código nele:

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

Esse componente renderiza um campo de entrada e um botão para que os clientes insiram seus e-mails e comecem a procurar assinaturas. Se assinaturas forem encontradas, o campo de entrada e o botão serão ocultados e uma lista de assinaturas será exibida na tela. Para cada item de assinatura, o componente passa um método removeSubscription que solicita ao servidor de backend Java que cancele a assinatura no backend do Stripe.

Para anexá-lo ao caminho /cancel-subscription em seu aplicativo de frontend, adicione o seguinte código no array passado para a chamada createBrowserRouter no componente App:

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

Para pesquisar assinaturas no servidor de backend, adicione um método viewSubscriptions na classe PaymentController do seu projeto de backend com o seguinte conteúdo:

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

O método acima primeiro encontra o objeto do comprador para o usuário fornecido no Stripe. Em seguida, ele procura as assinaturas ativas do comprador. Quando a lista de assinaturas é recebida, ele extrai os itens delas e encontra os produtos correspondentes no banco de dados de produtos do aplicativo para enviar ao frontend. Isso é importante porque o ID com o qual o frontend identifica cada produto no banco de dados do aplicativo pode ou não ser o mesmo que o ID do produto armazenado no Stripe.

Por fim, crie um método cancelSubscription</code na classe PaymentController e cole o código abaixo para excluir uma assinatura com base no ID de assinatura que foi passado:

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

Esse método recupera o objeto de assinatura do Stripe, chama o método cancel nele, e então retorna o status da assinatura para o cliente. Entretanto, para poder executar esse método, você precisa atualizar seu objeto DTO para adicionar o campo subscriptionId. Para isso, você deve adicionar o seguinte campo e método na classe RequestDTO:

package com.kinsta.stripejava.backend;

import com.stripe.model.Product;

public class RequestDTO {
    // … other fields …

    // Add this
    String subscriptionId;

    // … other getters …

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

}

Depois de adicionar isso, execute novamente o servidor de desenvolvimento para os aplicativos backend e frontend e veja o fluxo de cancelamento em ação:

Um fluxo de cancelamento de assinatura.
Um fluxo de cancelamento de assinatura.

Configuração de avaliações gratuitas para assinaturas com transações de valor zero

Um recurso comum à maioria das assinaturas modernas é oferecer um curto período de teste gratuito antes de cobrar do usuário. Isso permite que os usuários explorem o produto ou serviço sem investir nele. No entanto, é melhor armazenar os detalhes de pagamento do cliente ao inscrevê-lo para a avaliação gratuita, para que você possa cobrá-lo facilmente assim que a avaliação terminar.

O Stripe simplifica muito a criação de assinaturas assim. Para começar, gere um novo componente no diretório frontend/routes chamado SubscriptionWithTrial.tsx e cole o seguinte código:

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

Esse componente reutiliza os componentes criados anteriormente. A principal diferença entre este e o componente NewSubscription é que ele passa o modo para TotalFooter como trial em vez de subscription. Isso faz com que o componente TotalFooter renderize um texto dizendo que o cliente pode iniciar a avaliação gratuita agora, mas será cobrado depois de um mês.

Para anexar esse componente ao caminho /subscription-with-trial em seu aplicativo frontend, adicione o seguinte código no array passado para a chamada createBrowserRouter no componente App:

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

Para criar o fluxo de checkout para assinaturas com trial no backend, crie um novo método chamado newSubscriptionWithTrial na classe PaymentController e adicione o seguinte código:

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

Esse código é bastante semelhante ao do método newSubscription. A única (e fundamental) diferença é que um período de avaliação é passado para o objeto de parâmetros de criação de sessão com o valor de 30, indicando um período de avaliação gratuita de 30 dias.

Agora você pode salvar as alterações e executar novamente o servidor de desenvolvimento para o backend e o frontend para ver a assinatura com fluxo de trabalho de avaliação gratuita em ação:

Uma assinatura com fluxo de avaliação gratuita.
Uma assinatura com fluxo de avaliação gratuita.

Geração de faturas para pagamentos

No caso de assinaturas, o Stripe gera faturas automaticamente para cada pagamento, mesmo que seja uma transação de valor zero para a inscrição na versão de avaliação. Para pagamentos únicos, você pode optar por criar faturas se necessário.

Para começar a associar todos os pagamentos a faturas, atualize o corpo do payload que está sendo enviado na função initiatePayment do componente CustomerDetails no aplicativo de frontend para que contenha a seguinte propriedade:

invoiceNeeded: true

Você também precisará adicionar essa propriedade no corpo do payload que está sendo enviado ao servidor na função createTransactionSecret do componente IntegratedCheckout.

Em seguida, atualize os caminhos de backend para verificar essa nova propriedade e atualizar as chamadas do Stripe SDK concordemente.

No método de checkout hospedado, para adicionar a funcionalidade de faturamento, atualize o método hostedCheckout adicionando as seguintes linhas de código:

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

Isso verificará o campo invoiceNeeded e definirá os parâmetros de criação de acordo com isso.

Adicionar uma fatura a um pagamento integrado é um pouco complicado. Você não pode simplesmente definir um parâmetro para instruir o Stripe a criar automaticamente uma fatura com o pagamento. Você deve criar manualmente a fatura e, em seguida, uma intenção de pagamento vinculada.

Se a intenção de pagamento for paga e concluída com êxito, a fatura será marcada como paga; caso contrário, a fatura permanecerá não paga. Embora isso faça sentido lógico, pode ser um pouco complexo de implementar (especialmente quando não há exemplos claros ou referências a seguir).

Para implementar isso, atualize o método integratedCheckout para que ele fique assim:

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

O código antigo desse método é movido para o bloco if que verifica se o campo invoiceNeeded é false. Se for achado verdadeiro, o método agora cria uma fatura com itens de fatura e a marca como finalizada para que possa ser paga.

Em seguida, ele recupera a intenção de pagamento criada automaticamente quando a fatura foi finalizada e envia o segredo do cliente dessa intenção de pagamento para o cliente. Quando o cliente conclui o fluxo de checkout integrado, o pagamento é coletado e a fatura é marcada como paga.

Com isso você conclui a configuração necessária para começar a gerar faturas a partir do seu aplicativo. Você pode ir até a seção de faturas no painel do Stripe para ver as faturas que seu aplicativo gera com cada compra e pagamento de assinatura.

No entanto, o Stripe também permite que você acesse as faturas por meio de sua API para criar uma experiência de autoatendimento para os clientes baixarem as faturas sempre que quiserem.

Para fazer isso, crie um novo componente no diretório frontend/routes chamado ViewInvoices.tsx. Cole o seguinte código nele:

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

Semelhante ao componente CancelSubscription, esse componente exibe um campo de entrada para que o cliente digite seu e-mail e um botão para pesquisar faturas. Quando as faturas são encontradas, o campo de entrada e o botão são ocultos, e uma lista de faturas com o número da fatura, o valor total e um botão para fazer o download do PDF da fatura é exibida para o cliente.

Para implementar o método de backend que pesquisa as faturas de um determinado cliente e envia de volta as informações relevantes (número da fatura, valor e URL do PDF), adicione o seguinte método em sua classe PaymentController no backend;

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

        Stripe.apiKey = STRIPE_API_KEY;

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

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

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

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

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

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

            response.add(map);
        }

        return response;
    }

O método primeiro procura o cliente pelo endereço de e-mail fornecido a ele. Em seguida, procura as faturas desse cliente marcadas como pagas. Quando a lista de faturas é encontrada, ele extrai o número da fatura, o valor e a URL do PDF e envia de volta uma lista dessas informações para o aplicativo cliente.

É assim que o fluxo de faturas se parece:

A user flow showing how to retrieve and access invoices for a user.
Visualizando faturas.

Com isso, você conclui o desenvolvimento do nosso aplicativo Java de demonstração (frontend e backend). Na próxima seção, você aprenderá como implantar esse aplicativo na Kinsta para poder acessá-lo on-line.

Implementando seu aplicativo na Kinsta

Quando seu aplicativo estiver pronto, você poderá implantá-lo na Kinsta. A Kinsta suporta implantações a partir do seu provedor Git de preferência (Bitbucket, GitHub ou GitLab). Ao conectar o repositório do código-fonte do seu aplicativo à Kinsta, ela implantará automaticamente seu aplicativo sempre que houver uma alteração no código.

Prepare seus projetos

Para implantar seus aplicativos na produção, identifique os comandos de build e implantação que a Kinsta usará. Para o frontend, certifique-se de que seu arquivo package.json tenha os seguintes scripts definidos:

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

Você também precisará instalar o pacote npm serve, que permite que você sirva sites estáticos. Esse pacote será usado para servir a build de produção do seu aplicativo a partir do ambiente de implantação da Kinsta. Você pode instalá-lo executando o seguinte comando:

npm i serve

Depois que você compilar seu aplicativo usando o vite, todo o aplicativo será empacotado em um único arquivo, index.html, visto que a configuração do React que você está usando neste tutorial destina-se a criar aplicativos de página única. Embora isso não faça uma grande diferença para os usuários, você precisa definir algumas configurações extras para lidar com o roteamento e a navegação nesses aplicativos por meio do navegador.

Com a configuração atual, você só pode acessar o aplicativo na URL de base da sua implantação. Se a URL base da implantação for example.com, qualquer solicitação para example.com/some-route levará a erros HTTP 404.

Isso ocorre porque seu servidor tem apenas um arquivo para servir, o arquivo index.html. Uma solicitação enviada para example.com/some-route começará a procurar o arquivo some-route/index.html, que não existe; portanto, receberá uma resposta 404 Not Found.

Para corrigir isso, crie um arquivo chamado serve.json na pasta frontend/public e salve o seguinte código nele:

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

Esse arquivo instruirá o serve a reescrever todas as solicitações recebidas para serem roteadas para o arquivo index.html, enquanto ainda mostra na resposta o caminho para o qual a solicitação original foi enviada. Isso ajudará você a servir corretamente as páginas de sucesso e fracasso do seu aplicativo quando o Stripe redirecionar os clientes de volta para o seu aplicativo.

Para o backend, crie um Dockerfile para configurar o ambiente certo para o seu aplicativo Java. O uso de um Dockerfile garante que o ambiente fornecido ao seu aplicativo Java seja o mesmo em todos os hosts (seja o host de desenvolvimento local ou o host de implantação da Kinsta) e você pode garantir que seu aplicativo seja executado conforme esperado.

Para fazer isso, crie um arquivo chamado Dockerfile na pasta backend e salve o seguinte conteúdo nele:

FROM openjdk:22-oraclelinux8

LABEL maintainer="krharsh17"

WORKDIR /app

COPY . /app

RUN ./mvnw clean package

EXPOSE 8080

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

Esse arquivo instrui o tempo de execução a usar a imagem Java do OpenJDK como base para o contêiner de implantação, a executar o comando ./mvnw clean package para criar o arquivo JAR do seu aplicativo e a usar o comando java -jar <jar-file> para executá-lo. Isso conclui a preparação do código-fonte para a implantação na Kinsta.

Configure repositórios do GitHub

Para começar a implantar os aplicativos, crie dois repositórios do GitHub para hospedar o código-fonte dos aplicativos. Se você usa a CLI do GitHub, faça isso por meio do terminal executando os seguintes comandos:

# 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

Isso deve criar novos repositórios do GitHub em sua conta e enviar o código dos aplicativos para eles. Você deve poder acessar os repositórios de frontend e backend. Em seguida, implemente esses repositórios na Kinsta seguindo estas etapas:

  1. Faça login ou crie sua conta Kinsta no painel MyKinsta.
  2. Na barra lateral esquerda, clique em Aplicativos e, em seguida, em Adicionar aplicativo.
  3. No modal que aparece, escolha o repositório que você deseja implantar. Se tiver vários branches, você pode selecionar o branch desejado e dar um nome ao seu aplicativo.
  4. Selecione um dos locais de centros de dados disponíveis na lista de 25 opções. A Kinsta detecta automaticamente o comando start para o seu aplicativo.

Lembre-se de que você precisa fornecer aos aplicativos frontend e backend algumas variáveis de ambiente para que eles funcionem corretamente. O aplicativo de frontend precisa das seguintes variáveis de ambiente:

  • VITE_STRIPE_API_KEY
  • VITE_SERVER_BASE_URL
  • VITE_CLIENT_BASE_URL

Para implantar o aplicativo de backend, faça exatamente o que fizemos para o frontend, mas na etapa Ambiente de build, selecione o botão de opção Usar Dockerfile para configurar a imagem do contêiner e insira Dockerfile como o caminho do Dockerfile para o aplicativo de backend.

The add application form asking to provide build environment details..
Definição dos detalhes do ambiente de build.

Lembre-se de adicionar as variáveis de ambiente de backend:

  • CLIENT_BASE_URL
  • STRIPE_API_KEY

Quando a implantação estiver concluída, vá para a página de detalhes dos aplicativos e acesse a URL da implantação a partir dela.

A URL hospedada para os aplicativos implantados na Kinsta.
A URL hospedada para os aplicativos implantados na Kinsta.

Extraia as URLs dos dois aplicativos implantados. Vá até o painel do Stripe para obter suas chaves de API secretas e publicáveis.

Certifique-se de fornecer a chave publicável do Stripe para o seu aplicativo frontend (não a chave secreta). Além disso, certifique-se de que suas URLs de base não tenham uma barra (/) no final. As rotas já possuem barras, e assim, se você também tiver uma barra no final das URLs de base, duas barras serão adicionadas às URLs finais.

Para o seu aplicativo backend, adicione a chave secreta do painel do Stripe (não a chave publicável). Além disso, certifique-se de que a URL do cliente não tenha uma barra (/) no final.

Depois que as variáveis forem adicionadas, vá para a aba Implantações do aplicativo e clique no botão de reimplantação do seu aplicativo de backend. Isso conclui a configuração única que você precisa para fornecer credenciais às suas implantações Kinsta por meio de variáveis de ambiente.

Em seguida, você pode fazer o commit das alterações no seu controle de versão. A Kinsta reimplantará automaticamente seu aplicativo se você marcar a opção durante a implantação; caso contrário, você precisará acionar a reimplantação manualmente.

Resumo

Neste artigo, você aprendeu como o Stripe funciona e os fluxos de pagamento que ele oferece. Você também aprendeu, por meio de um exemplo detalhado, a integrar o Stripe ao seu aplicativo Java para aceitar pagamentos únicos, configurar assinaturas, oferecer avaliações gratuitas e gerar faturas de pagamento.

Usando o Stripe e o Java juntos, você pode oferecer uma solução de pagamentos robusta aos seus clientes, que pode ser bem dimensionada e se integrar perfeitamente ao seu ecossistema existente de aplicativos e ferramentas.

Você usa o Stripe em seu aplicativo para coletar pagamentos? Se sim, qual dos fluxos você prefere: hospedado, personalizado ou no aplicativo? Diga-nos nos comentários abaixo!

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