Flutter: Card payments made easy with Stripe and Ruby on Rails

Goutham Kumar
Flutter Community
Published in
8 min readNov 9, 2019

This article explains how to make SCA and PCI compliant card payments using Stripe from mobile applications using Flutter and Ruby on Rails.

Note:The example uses the stripe_payment package by Jonas Bark.Thanks to Harshitha Raj for authoring the Rails part of the article.

Introduction

There are countless articles available for this, yet, most of them do not explain the actual processes that happen when we use the plugin and how to save the cards and charge it the next time. So, I thought it would be of some help to show how I was able to implement it in one of my projects.

Stripe is a payment gateway with lots of useful features such as recurring payments, escrow accounts, and real-time credit card number validation using Luhn’s algorithm. With the introduction of SCA rules in the UK, Stripe also provides SCA compliant API’s and PaymentIntents make your work very simple.

So let’s get started with it.

Stripe setup

The first step would be to create an account in Stripe and switch to Test mode. We will be provided with a secret key and a publishable key. Our Flutter application is linked with the stripe account using the publishable key and merchantId provided by Stripe.

For this, we have to add the stripe_payment package to our project. Follow the instructions on the installation page of the package.

Now, let’s set up the connection in the initState() method of your widget or before initiating the checkout process(depends on your usage).

StripePayment.setOptions(
StripeOptions(
publishableKey: "YOUR_PUBLISHABLE_KEY",
merchantId: "YOUR_MERCHANT_ID",
androidPayMode: 'test',
),
);

The above code connects the application with your Stripe account and therefore, all payments made by this app will be received in your account which you can keep track of in the Stripe dashboard.

Adding a new Card

Next, let us create a Credit Card form to accept a new card from the customer.

void setError(dynamic error) {
//Handle your errors
}
await StripePayment.paymentRequestWithCardForm(
CardFormPaymentRequest(),
).catchError(setError);

The paymentRequestWithCardForm() method takes in an argument of type CardFormPaymentRequest(). You can create an object of CardFormPaymentRequest and pass it to the function if you wish to add a billing address. This method creates a native view of the credit card details page in iOS and Android as follows:

Native views of Credit Card form in iOS and Android

Validation of the card

The Credit Card number field is validated using Luhn’s algorithm in real-time and also the card type is detected automatically using regular expressions by Stripe. So, we don’t have to worry about the authenticity of the card number. If the user’s card is not supported in a particular geographical region, Stripe handles that and declines the card automatically with an error message. More importantly, Stripe does not save your Credit card number or CVC. It follows a tokenization algorithm to securely save the details of your card which can be reused later by the user.

Once your card is valid and has been accepted, stripe creates an object of type PaymentMethod which includes the expiry date of the card, the last 4 digits(an object of type Card contains this information) and of course, a payment method id. The payment method id will be used for saving the cards which we will see in the upcoming steps.

Customer Creation

Once you have received the paymentMethod object, you can create a new Customer in Stripe for that particular user. Since this part is mostly handled in the server-side, we will just be passing the payment method’s id to the back end.

void setError(dynamic error) {
//Handle your errors
}
await StripePayment.paymentRequestWithCardForm(
CardFormPaymentRequest(),
).then(
(PaymentMethod paymentMethod) async {
final http.Response response = await http.post(
'YOUR_URL/{paymentMethod.id}',
headers: <String, String>{
//Headers for your request
},
);
}
).catchError(setError);

Once, you receive the paymentMethod id in the server-side, you can create a new Customer and attach this id to that Customer.

Integration with Rails

The first step in Rails would be the installation of ‘stripe’ gem into your application.

gem stripe

Then your controller needs to be configured with the test secret key provided in your Stripe account.

require "stripe"
Stripe.api_key = "<YOUR_SECRET_KEY>"

Creating a new Customer

Now, let’s create a Stripe customer and then attach the payment method that is returned by the client-side to the customer.

@customer = Stripe::Customer.create({
description: 'Customer creation',
email: xxx@example.com,
payment_method: '<payment_method_id>', #returned from client-side
})

The customer object is used for tracking multiple payments associated with the same customer. The above code snippet creates a customer with their email and the payment method is attached to the customer object (For additional attributes in Stripe customer creation, refer to the Stripe documentation).

Creating a PaymentIntent

Once a customer object is created, you can create a PaymentIntent. PaymentIntent object is created to simplify the complexity of asynchronous payment flow by keeping track of the lifecycle of the checkout initiated by the customer. For each transaction, exactly one payment intent is created.

@intent = Stripe::PaymentIntent.create({
amount: price_in_cents,
currency: 'gbp',
payment_method_types: ['card'],
customer: @customer.id,
payment_method: @payment_method_id,
confirmation_method: 'manual',
confirm: true
})

Here confirmation_method can be automatic or manual. Automatic confirmation method is handled by the publishable key and is used when the payment intent is confirmed on the client-side. Manual confirmation method is handled by the secret key and the payment intent status results in either ‘requires_confirmation’ or ‘requires_action’ after the next actions are performed.

If the status of the intent is requires_confirmation, the payment is confirmed automatically since we provide explicit confirmation using the confirm attribute. Then the status is changed to succeeded and the after payment logic is handled if necessary.

If the status of the intent is requires_action, then the client_secret in the paymentIntent object is returned to the client-side to perform authentication.

if intent.status == ‘requires_action’
return intent.client_secret
elsif intent.status == ‘requires_confirmation’
intent = Stripe::PaymentIntent.confirm('<payment_intent_id>')
elsif intent.status == ‘succeeded’
...after payment logic...

After the authentication is performed by the client-side, the status of the payment intent changes to requires_confirmation. Then the payment intent is confirmed using the id of the intent returned from the client-side. After confirmation, the status is changed to ‘succeeded’ and the after payment logic is handled if necessary.

Note: The PaymentIntent can be created and confirmed in the client-side also using the curl command and confirmPaymentIntent() method(refer the official Stripe documentation). But since we’re using a database to store the transaction details, I just preferred doing it in the server-side.

Authenticating a paymentIntent

After receiving the client_secret of the paymentIntent from the server-side, we can authenticate the paymentIntent from the client-side using the authenticatePaymentIntent() method.

void setError(dynamic error) {
print('Error---------- ${error.toString()}');
}
await StripePayment.paymentRequestWithCardForm(
CardFormPaymentRequest(),
).then(
(PaymentMethod paymentMethod) async {
final http.Response response = await http.post(
'YOUR_URL/{paymentMethod.id}',
headers: <String, String>{
//Headers for your request
},
);

/* This is just a sample code to assign the client_secret. It might vary according to your design pattern */
final http.Response clientSecretResponse = await http.get(
'YOUR_URL/getClientSecret',
headers: <String, String>{
//Headers for your request
},
);
String clientSecret=clientSecretResponse.body['client_secret];
StripePayment.authenticatePaymentIntent(
clientSecret: clientSecret,
).then(
(paymentIntent) async {
/* Check the authentication satus of your Payment intent. Add the logic to be executed after successful authentication */final http.Response response = await http.post(
'YOUR_URL/{paymentIntent.id}',
headers: <String, String>{
//Headers for your request
},
);
/* Once the paymentIntent id is received, the confirmation of the paymentIntent is done in the server-side */
},//Used to handle the failed authentication scenario ).catchError(setError);
},
).catchError(setError);
}

And voila!!! We have successfully made our first payment using Stripe.

Saving and Reusing cards

Now there is the question of how to save these cards used by the customer for future transactions. Stripe makes it extremely easy to do this. Remember how you added the PaymentMethod to the Customer? The Card object in the PaymentMethod object consists of the card details along with a unique fingerprint for each card based on the Card number. So, we just have to save the payment method in the Customer object and whenever we want to display the saved card details, just fetch the payment methods attached to that customer and return the card objects.

The PaymentIntents can be created with any of these existing PaymentMethod ids. The Payment Intent just uses the card information provided in the PaymentMethod id using which it was created and therefore, that same card will be charged. So instead of creating a PaymentMethod, we just pass the existing PaymentMethod id to the server-side.

For charging the existing customer, we need the customer id returned by the stripe customer object at the time of creation. So, let’s store the stripe customer id in the database along with the unique user id. Already existing customers can be retrieved using the following code snippet.

@customer = Stripe::Customer.retrieve(<stripe_customer_id>)

Retrieving saved cards

If the customer already exists, there is a possibility of two payment methods: one is adding a new card and the other, using the already existing card. We shall use the fingerprint attached to the card in each PaymentMethod to retrieve the existing cards. Note that the fingerprint for each card is unique. If the fingerprint already exists, just create a PaymentIntent using the existing PaymentMethod. If a new card is added, the existing fingerprint check using the PaymentMethod will fail. So, first, attach the PaymentMethod to the retrieved customer and create the payment intent with the given PaymentMethod for the completion of the payment.

payment_methods = Stripe::PaymentMethod.list({
customer: @customer.id,
type: ‘card’,
})
card_fingerprint = Stripe::PaymentMethod.retrieve(params[:payment_method_id]).try(:card).try(:fingerprint)@existing_method = payment_methods.data.select{|method| method.card.fingerprint == card_fingerprint}.last if card_fingerprintunless @existing_method
payment_method = Stripe::PaymentMethod.attach(
params[:payment_method_id],
{
customer: @customer.id,
}
)
@payment_method_id = params[:payment_method_id]
else
@payment_method_id = @existing_method.id
end

A new PaymentIntent is created using @payment_method_id for each transaction and the client_secret in the PaymentIntent object is passed to the client-side.

In this way, we can just keep charging the existing card by using the payment method only. To show the details of your purchase, I have created a Ticket Widget in Flutter. Kindly check that out too.

The source code can be found at stripe_integration_example. Since most of the processes require an API call to the server-side, it is recommended to complete the server-side coding and then start with client-side integration.

Ticket to show the purchase details

Note: Since this is the first time we are trying this , it would be of great help if anyone could provide better methods or suggestions. In the next part, we will see about escrow accounts and how to handle payments between the customer, merchant and the receiver.

--

--