
Re-use payment methods in Stripe
In order to re-use payment methods using Stripe, you need to ask the user to enter his card detail for at-least once.
Stripe will save the credit card details securely on their server and return the payment method ID.
You can save that payment method ID in your database and later on, you can show user a list of all his saved payment methods (cards).
He can just tap on any card and pay without having to enter his card details again.
.env
This file is usually at the root of your project. You need to place your Stripe publishable and secret keys. You can get them from your Stripe dashboard.
STRIPE_PUBLIC_KEY=
STRIPE_SECRET_KEY=
Install Stripe SDK
Then you need to run the following command at the root of your project to install Stripe SDK.
composer require stripe/stripe-php
buy.blade.php
In this file, we will:
- Render Stripe payment widget
- Show list of payment methods
- Call AJAX requests to the backend APIs
<input type="hidden" id="baseUrl" value="{{ url('/') }}" />
<input type="hidden" id="stripe-public-key" value="{{ env('STRIPE_PUBLIC_KEY') }}" />
<script src="https://js.stripe.com/v3/"></script>
<input type="hidden" id="user-name" value="{{ auth()->user()->name ?? '' }}" />
<input type="hidden" id="user-email" value="{{ auth()->user()->email ?? '' }}" />
<input type="hidden" id="user-phone" value="{{ auth()->user()->phone ?? '' }}" />
<div id="app"></div>
<script>
var stripe = null;
var cardElement = null;
const stripePublicKey = document.getElementById("stripe-public-key").value || "";
const baseUrl = document.getElementById("baseUrl").value || "";
</script>
<script type="text/babel">
function App() {
const [submitting, setSubmitting] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const [paymentMethods, setPaymentMethods] = React.useState([]);
const [name, setName] = React.useState(document.getElementById("user-name").value);
const [email, setEmail] = React.useState(document.getElementById("user-email").value);
const [phone, setPhone] = React.useState(document.getElementById("user-phone").value);
async function fetchPaymentMethods() {
setLoading(true);
try {
const response = await axios.post(
baseUrl + "/user/payment-methods"
);
if (response.data.status == "success") {
setPaymentMethods(response.data.data);
} else {
swal.fire("Error", response.data.message, "error");
}
} catch (exp) {
swal.fire("Error", exp.message, "error");
} finally {
setLoading(false);
}
}
async function fetchStripeClientSecret(form) {
let clientSecret = "";
try {
const formData = new FormData(form);
const response = await axios.post(
baseUrl + "/stripe/generate-client-secret",
formData
);
if (response.data.status == "success") {
clientSecret = response.data.client_secret;
} else {
swal.fire("Error", response.data.message, "error");
}
} catch (exp) {
swal.fire("Error", exp.message, "error");
}
return clientSecret;
}
async function payNow(event) {
setSubmitting(true);
const form = event.currentTarget;
const formData = new FormData(form);
const clientSecret = await fetchStripeClientSecret(form);
if (clientSecret == "") {
swal.fire("Error", "Failed to make the payment.", "error");
setSubmitting(false);
return;
}
stripe
.confirmCardSetup(clientSecret, {
payment_method: {
card: cardElement,
billing_details: {
name: name,
email: email,
phone: phone
},
},
})
.then(async function(result) {
if (result.error) {
console.log(result.error);
setSubmitting(false);
} else {
console.log("The card has been verified successfully. payment_method_id:", result.setupIntent.payment_method);
try {
formData.append("payment_method_id", result.setupIntent.payment_method);
const response = await axios.post(
baseUrl + "/payment-success",
formData
);
if (response.data.status == "success") {
swal.fire("Payment successfull", response.data.message, "success")
.then(function () {
window.location.href = baseUrl;
});
} else {
swal.fire("Error", response.data.message, "error");
}
} catch (exp) {
swal.fire("Error", exp.message, "error");
} finally {
setSubmitting(false);
}
}
});
}
function payWithPaymentMethod(method) {
swal.fire({
title: "Pay now ",
text: "Proceed payment with card ending at " + method.last4,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes, do it!"
}).then(async (result) => {
if (result.isConfirmed) {
setLoading(true);
try {
const formData = new FormData();
formData.append("method_id", method.id);
const response = await axios.post(
baseUrl + "/payment-methods/pay",
formData
);
if (response.data.status == "success") {
swal.fire("Payment successfull", response.data.message, "success")
.then(function () {
window.location.href = baseUrl;
});
} else {
swal.fire("Error", response.data.message, "error");
}
} catch (exp) {
swal.fire("Error", exp.message, "error");
} finally {
setLoading(false);
}
}
});
}
React.useEffect(function () {
stripe = Stripe(stripePublicKey);
var elements = stripe.elements();
cardElement = elements.create('card');
cardElement.mount('#stripe-card-element');
fetchPaymentMethods();
}, []);
return (
<>
{ loading && (
<div className="overlay-loader">
<div className="spinner"></div>
</div>
) }
<div className="row">
<div className="col-md-12">
<form onSubmit={ function (event) {
event.preventDefault();
payNow(event);
} }>
<div id="stripe-card-element" className="mb-3"></div>
{ paymentMethods.length > 0 && (
<>
<h2 className="mb-3">My payment methods</h2>
<div className="row">
<div className="payment-methods">
{ paymentMethods.map(function (method, index) {
let lastUsedAt = method.updated_at;
const date = new Date(lastUsedAt);
const day = date.getDate();
const month = date.toLocaleString("en-US", { month: "long" });
const year = date.getFullYear();
lastUsedAt = `${ day } ${ month }, ${ year }`;
return (
<div className="card" key={ `method-${ index }` }
onClick={ function () {
payWithPaymentMethod(method);
} }>
<div className="card-header pb-0">
<i className="fa fa-credit-card icon-card"></i>
</div>
<div className="card-body card-details">
<span className="last4">**** **** **** { method.last4 }</span>
<span className="last-used">Last used on: <strong>{ lastUsedAt }</strong></span>
</div>
</div>
)
} ) }
</div>
</div>
</>
) }
<input type="submit" value="Buy" className="btn btn-primary"
disabled={ submitting } />
</form>
</div>
</div>
</>
);
}
ReactDOM.createRoot(
document.getElementById("app")
).render(<App />);
</script>
web.php
Here we will mention all routes that are being used in the AJAX requests in the previous step.
use App\Http\Controllers\PaymentController;
Route::group([
"middleware" => ["auth"]
], function () {
Route::post("/payment-methods/pay", [PaymentController::class, "charge"]);
Route::post("/user/payment-methods", [PaymentController::class, "fetch"]);
Route::post("/payment-success", [PaymentController::class, "payment_success"]);
Route::post("/stripe/generate-client-secret", [PaymentController::class, "generate_stripe_client_secret"]);
});
create_users_table.php
Before moving to the next step, make sure you have “stripe_customer_id” column in your “users” table.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password')->nullable();
$table->string("stripe_customer_id")->nullable();
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
}
};
PaymentController.php
This is our main controller file that will handle:
- Generating client secret from Stripe.
- Verify payments made through client app.
- Save and return saved payment methods.
- Charge user on already saved payment method.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use DB;
use Validator;
class PaymentController extends Controller
{
private $amount = 10;
public function charge()
{
$validator = Validator::make(request()->all(), [
"method_id" => "required",
]);
if ($validator->fails())
{
return response()->json([
"status" => "error",
"message" => $validator->errors()->first()
]);
}
$me = auth()->user();
$method_id = request()->method_id ?? 0;
if (!$me->stripe_customer_id)
{
return response()->json([
"status" => "error",
"message" => "Stripe customer not found."
]);
}
$user_payment_method = DB::table("user_payment_methods")
->where("id", "=", $method_id)
->where("user_id", "=", $me->id)
->first();
if ($user_payment_method == null)
{
return response()->json([
"status" => "error",
"message" => "Payment method not found."
]);
}
$stripe = new \Stripe\StripeClient(env("STRIPE_SECRET_KEY"));
try
{
$payment_method = $stripe->paymentMethods->retrieve($user_payment_method->stripe_payment_method);
if ($payment_method->customer !== $me->stripe_customer_id)
{
return response()->json([
"status" => "error",
"message" => "Invalid payment method."
]);
}
$payment_intent = $stripe->paymentIntents->create([
'amount' => $this->amount * 100,
'currency' => 'usd',
'customer' => $me->stripe_customer_id,
'payment_method' => $user_payment_method->stripe_payment_method,
'off_session' => true,
'confirm' => true,
]);
if ($payment_intent->status == "succeeded")
{
DB::table("user_payment_methods")
->where("id", "=", $user_payment_method->id)
->update([
"updated_at" => now()->utc()
]);
return response()->json([
"status" => "success",
"message" => "Payment successfull."
]);
}
return response()->json([
"status" => "error",
"message" => "Payment failed."
]);
}
catch (\Exception $exp)
{
\Log::error('Stripe error', [
'user_id' => $me->id,
'error' => $exp->getMessage()
]);
return response()->json([
"status" => "error",
"message" => "Payment could not be processed."
]);
}
}
public function fetch()
{
$me = auth()->user();
$data = DB::table("user_payment_methods")
->select("id", "last4", "updated_at")
->where("user_id", "=", $me->id)
->orderBy("updated_at", "desc")
->get();
return response()->json([
"status" => "success",
"data" => $data
]);
}
public function payment_success()
{
$validator = Validator::make(request()->all(), [
"payment_method_id" => "required|string",
]);
if ($validator->fails())
{
return response()->json([
"status" => "error",
"message" => $validator->errors()->first()
]);
}
$me = auth()->user();
$payment_method_id = request()->payment_method_id ?? "";
$stripe = new \Stripe\StripeClient(env("STRIPE_SECRET_KEY"));
try
{
$payment_method = $stripe->paymentMethods->retrieve($payment_method_id);
$payment_method->attach([
'customer' => $me->stripe_customer_id
]);
$payment_intent = $stripe->paymentIntents->create([
'amount' => $this->amount * 100,
'currency' => 'usd',
'customer' => $me->stripe_customer_id,
'payment_method' => $payment_method_id,
'off_session' => true,
'confirm' => true,
]);
if ($payment_intent->status == "succeeded")
{
DB::table("user_payment_methods")
->insert([
"user_id" => $me->id,
"stripe_payment_method" => $payment_method->id,
"last4" => $payment_method->card?->last4 ?? "",
"created_at" => now()->utc(),
"updated_at" => now()->utc()
]);
return response()->json([
"status" => "success",
"message" => "Payment successfull."
]);
}
return response()->json([
"status" => "error",
"message" => "Payment failed."
]);
}
catch (\Exception $exp)
{
\Log::error('Stripe error', [
'user_id' => $me->id,
'error' => $exp->getMessage()
]);
return response()->json([
"status" => "error",
"message" => "Payment could not be processed."
]);
}
}
public function generate_stripe_client_secret()
{
$me = auth()->user();
try
{
$stripe = new \Stripe\StripeClient(env("STRIPE_SECRET_KEY"));
$customer_id = $me->stripe_customer_id ?? null;
if (is_null($customer_id))
{
$customer = $stripe->customers->create([
'email' => $me->email ?? "",
'name' => $me->name ?? ""
]);
$customer_id = $customer->id;
DB::table("users")
->where("id", "=", $me->id)
->update([
"stripe_customer_id" => $customer_id,
"updated_at" => now()->utc()
]);
}
$setup_intent = $stripe->setupIntents->create([
'customer' => $customer_id
]);
return response()->json([
"status" => "success",
"message" => "Payment intent has been fetched.",
"client_secret" => $setup_intent->client_secret
]);
}
catch (\Exception $exp)
{
\Log::error('Stripe error', [
'user_id' => $me->id,
'error' => $exp->getMessage()
]);
return response()->json([
"status" => "error",
"message" => "Payment could not be processed."
]);
}
}
}
create_user_payment_methods_table.php
This will be the migration file that will create the table in your database to save user’s payment methods.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('user_payment_methods', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger("user_id")->nullable();
$table->foreign("user_id")->references("id")->on("users")->onUpdate("CASCADE")->onDelete("CASCADE");
$table->string("stripe_payment_method")->nullable();
$table->string("last4")->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_payment_methods');
}
};
style.css
/* Full screen overlay */
.overlay-loader {
position: fixed;
inset: 0; /* top:0; right:0; bottom:0; left:0 */
z-index: 1056; /* Above Bootstrap modals (1055) */
background-color: rgba(0, 0, 0, 0.5); /* semi-transparent */
display: flex;
align-items: center;
justify-content: center;
}
/* Spinner */
.spinner {
width: 3rem;
height: 3rem;
border: 0.3rem solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spinner-border 0.75s linear infinite;
}
/* Bootstrap-like spinner animation */
@keyframes spinner-border {
to {
transform: rotate(360deg);
}
}
Want to see this in action?
We have implemented this in our File Manager website developed in Laravel. You can check it from here.