Laravel

Laravel 7.x, 8.x – Stripe connect with Laravel cashier for multi vendor

Share your learning

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,

  1. Laravel Cashier-stripe
  2. Stripe-php SDK

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

  1. Create new vendor account
  2. Delete vendor account
  3. Connect vendor account with stripe connect through onboard process
  4. Start onboard process through account links
  5. Disconnect the stripe connect account

Stripe Connect Controller

This controller will do the following things

  1. Start onboard process
  2. Return from the onboard process and retrieve the information
  3. 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.

Satpal

Recent Posts

How to Switch PHP Versions in XAMPP Easily: Managing Multiple PHP Versions on Ubuntu

Today we are going to learn about managing multiple PHP versions on ubuntu with xampp.…

1 year ago

How to Use Coding to Improve Your Website’s SEO Ranking?

Let's understand about how to use coding to improve your website's SEO. In today’s computerized…

1 year ago

Most Important Linux Commands for Web Developers

Let's understand the most important linux commands for web developers. Linux, as an open-source and…

1 year ago

Top 75+ Laravel Interview Questions Asked by Top MNCs

Today we are going to discuss top 75+ Laravel interview questions asked by top MNCs.Laravel,…

1 year ago

Mailtrap Integration for Email Testing with Laravel 10

Today we will discuss about the Mailtrap integration with laravel 10 .Sending and receiving emails…

1 year ago

Firebase Cloud Messaging (FCM) with Ionic 6: Push Notifications

Today we are going to integrate FCM (Firebase Cloud Messaging) push notifications with ionic application.Firebase…

1 year ago