Create a Peer-to-Peer Payment App With Laravel and Stripe
Time to read: 10 minutes
Peer-to-Peer (P2P) payment systems have become a huge part of our lives in the past decade. For example, PayPal, Venmo, and Cash App have become a part of internet lingo, just like Google. P2P is so important that social media and communication apps, such as Snapchat, WeChat, and Facebook, have incorporated these features in recent years.
In this tutorial, you'll create a P2P payment app called "Twilmo" that will be based on the RILT architecture (React, Inertia, Laravel, and Tailwind CSS) and backed by Stripe. Even though it's named "Twilmo", it will be very simplistic, and will not incorporate most of the features that are in Venmo.
The beautiful thing about this is that by the end you should have a good idea of how to expand the app to incorporate more advanced features.
Prerequisites
To follow this tutorial, you will need the following:
- A Stripe account
- Composer globally installed
- Node and NPM installed
- Laravel 9 installed and set up
- A database to use with the application
- Basic knowledge of Laravel and its templating structure
- Basic knowledge of React and JSX
Laravel application setup
First, make sure you have your database set up and create a database called twilmo
. After setting up your database, navigate to the folder where you store your projects and run the following script in your terminal, to generate a new Laravel project and change into the newly generated directory (twilmo):
Next, install Laravel Breeze, which is a way for us to easily set up basic authentication features for Laravel. These features include registration, login, password, reset, etc. It will also help us to easily add a React frontend to our Laravel application. Run the following command:
Then, publish the Breeze library's resources in the project by running:
Next, add Stripe support. If you don't know what Stripe is, it's a service that provides an API for processing payments on the internet. Developers love using Stripe because of its ease of use and its excellent documentation. To use Stripe in this project, run the following command:
Once it is installed, you need to set the required environment variables. The most important for this project are the ones for your database and for Stripe.
First, open .env and edit the database configurations, so that they match your database's configuration.
Then, add the Stripe configurations to the end of .env.
Then, retrieve your Stripe Publishable Key and the Secret Key. You can find these values in the Stripe dashboard under "Developers > API Keys". Then, replace the two Stripe placeholder values at the end of .env with the two keys.
Models and migrations
You'll be creating two models with their related migrations and controllers, Account
and Transaction
. Create them by running the following, two commands:
The Account
model will need fields to denote the balance, the Stripe customer ID (used to access Stripe resources related to the customer such as payment methods), and other details such as street address.
In database/migrations/ open the accounts migration file; the migration file's name will start with a timestamp followed by create_accounts_table. Then, update the up()
function to match the code below.
For the Transaction
model, you'll need fields to denote the sending user, the receiving user, and the amount being transferred. So, open the migrations file in database/migrations which ends with "create_transactions_table.php", and update its up()
function to match the following code.
Once you're done, run the migrations by running the command below.
Now, open the User
model (app/Models/User.php) and add the following function to it. This uses Eloquent, to tie it to the Account
model (app/Models/Account.php).
For the Account
model, you need to specify the fields that are mass-assignable. You also need to make sure money is presented in the right way, by using a cast that ensures decimals always have two places.
To do both of these things, update the body of the Account
model to match the code below,
For the Transaction
model (app/Models/Transaction.php), all you need to do is define the fields that are mass-assignable (user_from
and user_to
) which describe the sender and the receiver, respectively.
Do that by updating the body of the Transaction
model to match the code below.
Register user route
With the default configuration, Laravel redirects users to http://localhost:8000/dashboard when they are logged in. For this app, we'll leave that alone and build a profile page where we see our balance, add a card, and send people money.
First, the redirect URL needs to be changed to http://localhost:8000/profile. You can do this by changing just one line of code in app/Providers/RouteServiceProvider.php, which you can see below.
Once you've done that, you need to add some new routes. In routes/web.php, add the following route definitions before require __DIR__.'/auth.php';
. They add a route to handle the user's profile and their transactions.
Then, add the following use
statements to the top of the file.
The auth-related mechanisms aren't ready just yet, as more work needs to be done in the AccountController
and the RegisteredUserController
, located in app/Http/Controllers.
Starting with the AccountController
, the /profile
page needs to display information about the user. To do this, replace the existing index
method with the following code:
Then, add the use
statements below to the top of the file.
Then, in the store
method of the RegisteredUserController
, (app/Http/Controllers/Auth/RegisteredUserController.php), add the following code just before event(new Registered($user));
. This will create an account, as part of the user creation process.
Then, include the use
statement below at the top of the file.
Next, there needs to be a way to create a Stripe customer ID number every time a new account is created, and to store the ID number in the application's database. Luckily, Stripe's excellent API supports this functionality.
To do that, create a trait that will house all the methods related to Stripe. In the app/ directory, create a new directory, named Traits, and in that directory create a new Trait named StripeHelperTrait.php. In the new file, add the code below.
The constructor sets the Stripe secret key to use in each of the Trait's methods. The first method that was made here is createCustomer
. The customer_id
returned from this method will be used for other Stripe methods, such as retrieving payment methods associated with the customer.
Now, let's go back to the store
method of the RegisteredUserController
and make some changes, so that the Stripe customer ID can be saved to the database.
To do that, replace the existing store
method with the version below.
Then, add the following use
statement to the top of the file.
After that, add the following use
statement inside the body of the class.
Now in resources/js/Pages/Auth/Register.js, you need to add a few fields for the account. Replace the return
statement in the component with the following code:
Now, create a new JavaScript file, Profile.js, in resources/js/Pages. Then, copy the JavaScript code below, which is a slightly updated version of the React and Tailwind CSS code from Creative Tim's profile page example, into the new file.
In the resources/js/Components/ directory, create a new file, named Navbar.js, and paste the code below into it.
Before you test the auth routes, you need to implement validation for registration by creating a form request. To do that, run the following command in the root directory of the project.
The generated file, RegisterRequest.php, will appear in app/Http/Requests. Replace its contents with the following code:
Then, import the newly generated form request into RegisteredUserController
by updating the request validation in the store
method, to match the highlighted line in the code below.
Then, add the following use
statement to the top of the class.
Create transactions with Stripe
Now you're at the heart of the tutorial. The P2P app works by enabling the user to send money to another user if their balance has sufficient funds, and from a card attached to their account.
The first thing you need to do is to add more methods to StripeHelperTrait
, that you created earlier. By doing this, the user can save a credit or debit card to their account. This saved card will then be used to make a transaction, if needs be.
According to Stripe's documentation, you first need to create a Stripe SetupIntent
object. This is an object that creates payment methods for future use. To do this, add the function below to app/Traits/StripeHelperTrait.php.
From this object, the client secret that the client-side code uses to securely process a transaction is obtained.
Notice that a $customer_id
parameter is passed into the method. This parameter's value will be retrieved from the database.
You'll also need a way to get the payment card that is associated with the account. To do this, add the following method to the trait.
Finally, you'll need a way for the credit card to be charged, if there is a card on file. This is done by creating a Stripe PaymentIntent
object and providing the customer ID and payment method ID to it. To do this, add the following method to StripeHelperTrait
.
This completes the StripeHelperTrait
. It can now be used in a controller. But before that happens, let's work on the FormRequest.
The next thing to do is to create the TransactionRequest
, to make sure that:
- The user won't be able to send money from their balance, if there are insufficient funds.
- The user that the money is being sent to actually exists.
- The user isn't sending money to themself.
To do that, first run the following command.
The generated file, TransactionRequest.php, will be created in app/Http/Requests. Replace the generated file with the code below.
In the code above, the rules
method uses Laravel's exists validator to check the submitted email address, email
, against the email
field in the user
table in the database. If it's there, it passes. If it doesn't, the message "This user doesn't exist" is displayed.
It then uses the not_in
rule to ensure that the supplied email address does not match the email of the authenticated user.
It then uses an After Validation Hook, which calls the lowBalance()
function, immediately after all the rules have been checked. The function returns false
if there is a credit card, or if the account balance is greater than the transaction amount. However, if the account balance is less than the transaction amount, it returns true
, and displays an error message.
In the TransactionController
(app/Http/Controllers/TransactionController.php), it uses the card on file if the balance is low, or the account balance if it's sufficient. So that this can happen, you need to update the respective balances of the sender and the receiver. To do that, replace the existing store
method, with the code below.
Then, add the following use
statements to the top of the class.
After that, add the following use
statement inside the body of the class.
There's just one more thing on the backend that you need to do. Remember, earlier, when you created the method for making a SetupIntent
? Well, you need to return one when the profile is loaded so that you can save the card against the customer.
To do this, update the AccountController
's index
method to match the following code:
Then, add the following use
statement to the top of the file.
After that, add the following use
statement inside the body of the class.
Add the frontend code
With the backend functionality just about done, it's time to update the frontend. First, you need to install a few important Stripe libraries for the client. To do this, run the command below.
In resources/js/Pages/Profile.js, you need to load the Elements
component and loadStripe
from the newly installed libraries. loadStripe
will use your Stripe Publishable Key to create a Stripe Promise. This is then passed into the Elements
component, which will wrap the credit card form. Finally, when the card information is entered successfully, the card form needs to disappear.
To do all this, update resources/js/Pages/Profile.js to match the following code:
Now, you need to include the PaymentElement
that is needed to build the form in SetupForm.js. To do that, go to resources/js/Components/ and create a new file named SetupForm.js. Then, copy the code below into the newly created file.
You don't need to worry about creating custom fields or handling input as Stripe will do that automatically for you. When the card is successfully submitted, JavaScript will reload the page.
Specifically, in the code above, redirect
is set to if_required
. For many payment methods, Stripe will take the user to different pages on their website to complete the setup and then redirect the user back to the URL set in the redirect
property. To keep things simple and prevent redirects, it is set to if_required
.
With all of the frontend components created, compile them by running the following command.
Test the app
It's time to play around with your new app. First, start the application by running the following command in the root directory of your project.
Then, go to http://localhost:8000, where you should see the default Laravel landing page. The links for logging in and registering should be at the top right. Click the register link.
Once you're on the registration page, input information into all the fields. The only field that is optional is Address 2. After successful completion, click on the Register button which will take you to the Profile page.
Register the receiver
Next, using a different browser, or a new Private/Incognito browser tab/window with the same browser, repeat the previous steps to register an account for the receiver.
Register the sender's credit card
In the sender's browser window/tab, register a credit card. Stripe offers a myriad of test cards that you can use when testing transactions. However, use 4242 4242 4242 4242
to register a Visa card. For the expiry date and CVC (Card Verification Value/Code) numbers, use whatever values that you like.
After you've filled in the form, click Submit. After submission, the page should refresh, and it should show "Card available", if successful.
Send funds to the receiver
Finally, it's time to send funds to the receiver. Enter the email address of the receiver, set an amount larger than your available Stripe balance for the amount to send, and click Send. The page should refresh and a ‘Success' message should be visible above the form, as you can see in the screenshot below.
If you like, continue playing around with the app by sending money from an account that doesn't have a card set up, or try to send money to an account that doesn't exist. There should be validation errors in both scenarios.
Conclusion
You've just learned the very basics of creating a P2P payment application that can be readily expanded if you want. With Twilio's API, you can send an SMS every time a transaction is made. Of course, you'll need to collect phone numbers to do that. You could also show a history of transactions by creating a hasMany
relationship between the User model and Transaction.
What makes coding a huge project like this a Breeze on Laravel is that it takes care of so many authentication issues, setting it apart from many other frameworks out there.
Lloyd Miller is a freelance full-stack web developer based in New York. He enjoys creating customer-facing products with Laravel and React, and documenting his journey through blog posts at https://blog.lloydmiller.dev
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.