How to add a one-time payment?
A one-time payment refers to a type of transaction in which a user pays for a specific product or a set of products only once, without incurring any additional payments, such as a subscription.
The ShipFast provides an exemplary one-time payment flow implementation. The code displayed below is readily available in the pristine ShipFast repository for your ease of use. Feel free to customize it as needed to suit your requirements.
Create payment intent
Use the Payment Intents API to build an integration that can handle complex payment flows. It tracks a payment from creation through checkout, and triggers additional authentication steps when required.
Some of the advantages of using the Payment Intents API include:
- Automatic authentication handling
- No double charges
- No idempotency key issues
- Support for Strong Customer Authentication (SCA) and similar regulatory changes
Source: Payment Intents API documentation
Create serializer
This is a simplest possible serializer that does not accept any input parameters. In real-life implementation you of course need to add some sort of product identifier, basket ID, or similar to calculate the price.
In the following example we hardcoded $15 as the amount to be paid.
from djstripe import models as djstripe_models
from rest_framework import serializers, exceptions
class PaymentIntentSerializer(serializers.ModelSerializer):
class Meta:
model = djstripe_models.PaymentIntent
fields = ('id', 'amount', 'currency', 'client_secret')
read_only_fields = ('id', 'amount', 'currency', 'client_secret')
def create(self, validated_data):
request = self.context['request']
(customer, _) = djstripe_models.Customer.get_or_create(request.user)
payment_intent_response = djstripe_models.PaymentIntent._api_create(
amount=15*100, # $15
currency="usd",
customer=customer.id,
setup_future_usage="off_session",
)
return djstripe_models.PaymentIntent.sync_from_stripe_data(payment_intent_response)
Create mutation object
The serializer was, of course, just the beginning. Next, you should create a mutation object and incorporate it into the GraphQL schema. This will enable the frontend to call it when the user submits the form.
from djstripe import models as djstripe_models
from common.graphql import mutations
from . import serializers
class CreatePaymentIntentMutation(mutations.CreateModelMutation):
class Meta:
model = djstripe_models.PaymentIntent
serializer_class = serializers.PaymentIntentSerializer
class Mutation(graphene.ObjectType):
create_payment_intent = CreatePaymentIntentMutation.Field()
If you want to learn more about creating mutations check out How to add a new mutation to back-end API guide.
Re-generate GraphQL types
You can either restart the dev server or run the following command to re-generate GraphQL types:
pnpm nx run webapp:graphql:generate-types
Create payment intent in web app
Let's create a hook that wraps the logic of managing payment intents.
import { useMutation } from '@apollo/client';
import { StripePaymentIntentType, gql } from '@shipfast/webapp-api-client/graphql';
import { GraphQLError } from 'graphql';
export const stripeCreatePaymentIntentMutation = gql(/* GraphQL */ `
mutation stripeCreatePaymentIntentMutation_(
$input: CreatePaymentIntentMutationInput!
) {
createPaymentIntent(input: $input) {
paymentIntent {
id
amount
clientSecret
currency
pk
}
}
}
`);
export const useStripePaymentIntent = () => {
const [commitCreatePaymentIntentMutation, { loading }] = useMutation(
stripeCreatePaymentIntentMutation
);
const createPaymentIntent = async (): Promise<{
errors?: readonly GraphQLError[];
paymentIntent?: StripePaymentIntentType | null;
}> => {
const { data, errors } = await commitCreatePaymentIntentMutation({});
if (errors) return { errors };
return { paymentIntent: data?.createPaymentIntent?.paymentIntent };
};
return { createPaymentIntent, loading };
};
The ShipFast repository already includes a hook similar to this as an implemented example. You can find it in
the following path: packages/webapp-libs/webapp-finances/src/components/stripePayment.hooks.ts
. This hook is provided
for your convenience. Additionally, it supports the updating of payment intents when your customer decides to change
the products they are interested in.
Confirm payment
Next you'll create a hook that uses react-stripe-js
library to collect information from CardNumberElement
and
confirm the payment.
Read official Stripe Elements documentation to learn how to build Stripe forms.
import { StripePaymentIntentType } from '@shipfast/webapp-api-client/graphql';
import {
CardNumberElement,
useElements,
useStripe,
} from '@stripe/react-stripe-js';
export const useStripePayment = () => {
const stripe = useStripe();
const elements = useElements();
const confirmPayment = async (paymentIntent: StripePaymentIntentType) => {
if (!stripe) return null;
const card = elements?.getElement(CardNumberElement) ?? null;
if (!card) return null;
return await stripe.confirmCardPayment(paymentIntent.clientSecret, {
payment_method: {
card,
},
});
};
return { confirmPayment };
};
Combine into a form
For now the creation of form itself goes beyond the scope of this guide but in future you'll be able to find concrete implementation of Stripe elements with react-hook-form.
Read official Stripe Elements documentation to learn how to build Stripe forms.
The simplest approach is to create the payment intent in form submit handler and confirm it right away:
const { createPaymentIntent } = useStripePaymentIntent();
const { confirmPayment } = useStripePayment();
const handleSubmit = form.handleSubmit(async () => {
const { paymentIntent, errors } = await createPaymentIntent();
if (!errors) {
confirmPayment(paymentIntent);
}
});