While we are using Laravel 7.x we still don’t have stripe connect feature with laravel cashier-stripe for multi vendor. This issue was raised in 2017 but the cashier team has rejected it because they don’t want extra maintenance burden.
So, we are going to implement it ourselves with available laravel cashier-stripe options.
It’s ideal for business models like marketplaces and software platforms. Vendors can connect their stripe account and get paid directly. Even vendors can manage subscription plans, customers and their payments with your platform. Your platform can get the application fee for that.
In short, multi vendors can manage their stripe accounts with your platform by using stripe connect feature with laravel cashier for multi vendor.
Install Required packages,
We are going to learn about following things,
Let’s create a PHP class to manage stripe connect accounts.
<?php namespace App\Traits; use Stripe\Customer; use Stripe\Account; use Stripe\Stripe; use Stripe\AccountLink; use Stripe\StripeClient; use Stripe\OAuth; class StripeConnect { public static function prepare() { Stripe::setApiKey("use your secret key here"); } /** * @param $to * @param array $params * @return Stripe */ public static function getOrCreateAccount($user, $params = [], $config = []) { self::prepare(); $params = array_merge([ "type" => $config['account_type'] ?? 'standard' ], $params); return self::create($user, 'account_id', function () use ($params) { return Account::create($params); }); } /** * @param $to * @param array $params * @return Stripe */ public static function getOrCreateCustomer($token, $user, $params = []) { self::prepare(); $params = array_merge([ "email" => $user->email, 'source' => $token, ], $params); return self::create($user, 'customer_id', function () use ($params) { return Customer::create($params); }); } public static function deleteAccount( String $account_id) { try { $stripe = new StripeClient( env("STRIPE_SECRET") ); $stripe->accounts->delete($account_id, [] ); return true; } catch (\Throwable $th) { throw $th; return false; } } public static function createAccountLink( $vendorId, $config = [] ) { self::prepare(); $account_links = AccountLink::create([ 'account' => $vendorId, 'refresh_url' => $config['refresh_url'] ?? '', 'return_url' => $config['return_url'] ?? '', 'type' => 'account_onboarding', ]); return $account_links; } public static function getVendor( $vendorId ) { self::prepare(); return Account::retrieve($vendorId); } public static function getCustomer( $customerId ) { self::prepare(); return Customer::retrieve($customerId); } public static function disconnectStripeAccount( $account_id ) { self::prepare(); return OAuth::deauthorize([ 'client_id' => 'CLIENT_ID', 'stripe_user_id' => $account_id, ]); } /** * @param $user * @param $id_key * @param $callback * @return Stripe */ private static function create($user, $id_key, $callback) { $vendor = $user->stripeAccount; if (!$vendor || !$vendor->$id_key) { $id = call_user_func($callback)->id; if (!$vendor) { $vendor = $user->stripeAccount()->create([$id_key => $id]); } else { $vendor->$id_key = $id; $vendor->save(); } } return $vendor; } }
This trait will help us to do following tasks
This controller will do the following things
<?php namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use Illuminate\Http\Request; use App\Traits\StripeConnect; use App\Models\StripeAccount; use Illuminate\Support\Facades\DB; class StripeConnectController extends Controller { public function startOnBoardProcess() { try { //code... $user = auth()->user(); $vendor = StripeConnect::getOrCreateAccount($user, ["email" => $user->email]); $getAccountLink = StripeConnect::createAccountLink($vendor->account_id, [ "return_url" => url()->current()."/$vendor->account_id/return", "refresh_url" => url()->previous() ]); return redirect($getAccountLink->url); } catch (\Throwable $th) { throw $th; } } public function returnFromOnBoardProcess(Request $request, $vendor_id) { $vendor = StripeConnect::getVendor($vendor_id); $account = StripeAccount::where("account_id", $vendor->id)->first(); $account->charges_enabled = (int) $vendor->charges_enabled; $account->save(); if ( $account->charges_enabled ) { $message = ["success" => "Stripe connect account is successfuly added!"]; } else if ( $vendor->details_submitted === false ) { $message = ["error" => "Please try again, You haven't complete the stripe connect process!"]; } else { $message = ["info" => "Stripe connect account is under review!"]; } return redirect()->route("settings.index")->with($message); } public function destroy( $account_id ) { try { DB::beginTransaction(); $user = auth()->user(); $user->stripeAccount->where('account_id', $account_id)->delete(); //StripeConnect::disconnectStripeAccount($account_id); $user->plans()->where('is_offline', 0)->delete(); DB::commit(); return redirect()->back()->with(['success' => 'Successfully disconnected your stripe account!']); } catch (\Throwable $th) { DB::rollBack(); throw $th; return redirect()->back()->with(['error' => 'Stripe account failed to disconnect!']); } } }
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateStripeAccountsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('stripe_accounts', function (Blueprint $table) { $table->id(); $table->foreignId("user_id"); $table->string("user_type"); $table->string("account_id")->nullable(); $table->string("customer_id")->nullable(); $table->tinyInteger("charges_enabled")->default(0); $table->softDeletes(); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('stripe_accounts'); } }
We will maintain the charge_enable column, so when it is 1 or true then the user will be able to collect the payments through his/her stripe connect account.
As we have morphed it with a user, we can retrieve the user who has a stripe account connected.
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class StripeAccount extends Model { use SoftDeletes; protected $fillable = [ "user_type", "user_id", "account_id", "customer_id", "charges_enabled" ]; public function user() { return $this->morphTo(); } }
if (!function_exists('getStripeAccountId')) { function getStripeAccountId() { $user = auth()->user(); $account_id = null; if ($user && $user->stripeAccount) { $user_account = $user->stripeAccount->where('charges_enabled', 1)->first(); $account_id = $user_account ? $user_account->account_id : null; } return $account_id; } }
If the user has the charge enabled which means the user has successfully connected with stripe connect accounts, this helper function can return the stripe account id otherwise it will return null.
We have to merge one more option to stripe default options that is stripe account. Then return the cashier stripe options like in the code below.
public function stripeOptions(array $options = []) { $options = array_merge($options, [ 'stripe_account' => getStripeAccountId(), ]); return Cashier::stripeOptions($options); }
It will help us to create the setupIntent for the user with a stripe connect account.
trait SubscriptionTrait { public function paymentForm() { $user = auth()->user(); if ( $user ) { return view('subscription.payment', [ 'intent' => $user->createSetupIntent(), 'user' => $user ]); } abort(403, 'Access Denied!'); } }
Here we are using our global helper function to retrieve the stripe account id.
<meta name="stripe_account_id" content="{{ getStripeAccountId( () ) }}" /> <meta name="stripe_pub_key" content="{{ env('STRIPE_PUBLIC_KEY') }}">
var stripe, elements, cardNumber, cardExpiry, cardCvc; var elementStyles = { base: { color: '#32325D', fontWeight: 500, fontFamily: 'Source Code Pro, Consolas, Menlo, monospace', fontSize: '16px', fontSmoothing: 'antialiased', '::placeholder': { color: '#CFD7DF', }, ':-webkit-autofill': { color: '#e39f48', }, }, invalid: { color: '#E25950', '::placeholder': { color: '#FFCCA5', }, }, }; var elementClasses = { focus: 'focused', empty: 'empty', invalid: 'invalid', }; const stripeCard = { init : function () { const StripeAccountId = document.querySelector('meta[name="stripe_account_id"]'); const StripePublicKey = document.querySelector('meta[name="stripe_pub_key"]'); var options = {}; console.log({StripeAccountId}, {StripePublicKey}); if (StripeAccountId.content) { console.log('found account id'); options = { stripeAccount: StripeAccountId.content }; } stripe = Stripe(StripePublicKey.content, options); elements = stripe.elements({ fonts: [{ cssSrc: 'https://fonts.googleapis.com/css?family=Source+Code+Pro', }, ], locale: window.__exampleLocale }); cardNumber = elements.create('cardNumber', { showIcon: true, style: elementStyles, classes: elementClasses, }); cardExpiry = elements.create('cardExpiry', { style: elementStyles, classes: elementClasses, }); cardCvc = elements.create('cardCvc', { style: elementStyles, classes: elementClasses, class: 'login-input', }); }, mountElements : function () { console.log(elements); var inputs = document.querySelectorAll('.cell.example.example2 .input'); Array.prototype.forEach.call(inputs, function(input) { input.addEventListener('focus', function() { input.classList.add('focused'); }); input.addEventListener('blur', function() { input.classList.remove('focused'); }); input.addEventListener('keyup', function() { if (input.value.length === 0) { input.classList.add('empty'); } else { input.classList.remove('empty'); } }); }); cardNumber.mount('#example2-card-number'); cardExpiry.mount('#example2-card-expiry'); cardCvc.mount('#example2-card-cvc'); const cardButton = document.getElementById('card-button'); const clientSecret = cardButton.dataset.secret; const cardElement = cardNumber cardButton.addEventListener('click', async(e) => { e.preventDefault(); const { setupIntent, error } = await stripe.confirmCardSetup( clientSecret, { payment_method: { card: cardElement, } } ); if (error) { //setTimeout(function() { $(".error").find('span').text(error.message); // }, 3000); // Display "error.message" to the user... } else { // console.log(setupIntent); $('#payment_method').val(setupIntent.payment_method); $('#paymentForm').submit(); } }); } } stripeCard.init();
Retrieve the stripe connect account from the meta and merge into stripe options to generate the suitable card intent.
If these things are not set, it will throw an error “setup_intent is not found!”
The user setup intent from the subscription trait and card intent from the payment js should be the same. It will be the same if the user’s stripe connect account setup well on both sides subscription trait and payment js.
If both side stripe connect accounts are not added or null passed then also everything will look fine and directly work with the stripe account whose stripe keys are using.
If you are managing stripe plans with your admin dashboard then you need to add a stripe account there also otherwise user subscription with different subscription plans and payment with stripe connect account will throw an error.
public function setKey() { Stripe::setApiKey("your secret key here"); Stripe::setAccountId(getStripeAccountId()); }
That’s it, how we can use stripe connect feature with laravel cashier-stripe.
Hope you find this article useful.
See you in the next learning chapter.
Today we are going to learn about managing multiple PHP versions on ubuntu with xampp.…
Let's understand about how to use coding to improve your website's SEO. In today’s computerized…
Let's understand the most important linux commands for web developers. Linux, as an open-source and…
Today we are going to discuss top 75+ Laravel interview questions asked by top MNCs.Laravel,…
Today we will discuss about the Mailtrap integration with laravel 10 .Sending and receiving emails…
Today we are going to integrate FCM (Firebase Cloud Messaging) push notifications with ionic application.Firebase…