Laravel 7.x, 8.x – Stripe connect with Laravel cashier for multi vendor
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.
What is a Stripe connect account?
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,
Connect and Disconnect with stripe connect account via stripe onboard process
Stripe Connect Trait
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
- Create new vendor account
- Delete vendor account
- Connect vendor account with stripe connect through onboard process
- Start onboard process through account links
- Disconnect the stripe connect account
Stripe Connect Controller
This controller will do the following things
- Start onboard process
- Return from the onboard process and retrieve the information
- Destroy and disconnect the stripe connect accounts
<?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!']); } } }
Maintain the stripe connect account details with our database
Stripe Accounts migration
<?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'); } }
Stripe Accounts model
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(); } }
Helper function to retrieve stripe connect account from our database
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.
Override stripe options via billable model (e.g. User)
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!'); } }
Add Stripe connect account id to layout meta and retrieve into payment js
Add meta to layout files
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') }}">
Retrieve meta into payment js
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.
Subscription plans
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.