javascriptphpjquerylaravelplaid

How to implement the Plaid identity verification product in Laravel?


How to implement the Plaid identity verification product in Laravel? I am confused about implementing this product. I've tried the github.com/TomorrowIdeas/plaid-sdk-php package and successfully implemented the Investments product in Laravel. I want to implement an identity verification product in Laravel using a template from Plaid. Can someone explain how to implement the Plaid identity verification product in Laravel?


Solution

  • This issue has been solved using the Plaid SDK PHP and jQuery. Below is an example of the codes.

    plaid.js

    $(document).ready(function(){
        window.isVerificationLinkClicked = false;
        window.isInitialVerificationChecked = false;
        if($('.identity-verification').length) {
            getVerificationLink();
            checkVerificationStatus();
        }
    
        $('.identity-verification').on('click', function (event) {
            event.preventDefault();
            window.open($(this).data('verificationLink'), '_blank');
            window.isVerificationLinkClicked = true;
            checkVerificationStatus();
        })
    
        function createLinkToken() {
            $.ajax({
                headers: {
                    "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content")
                },
                url: "/createLinkToken",
                type: "GET",
                dataType: "json",
                success: function (response) {
                    const data = JSON.parse(response.data);
                    console.log('Link Token: ' + data.link_token);
                    linkPlaidAccount(data.link_token);
                },
                error: function (err) {
                    console.log('Error creating link token.');
                    const errMsg = JSON.parse(err);
                    alert(err.error_message);
                    console.error("Error creating link token: ", err);
                }
            });
        }
    
        function linkPlaidAccount(linkToken) {
            var linkHandler = Plaid.create({
                token: linkToken,
                onSuccess: function (public_token, metadata) {
                    var body = {
                        public_token: public_token,
                        accounts: metadata.accounts,
                        institution: metadata.institution,
                        link_session_id: metadata.link_session_id,
                        link_token: linkToken
                    };
                    $.ajax({
                        headers: {
                            "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content")
                        },
                        url: "/storePlaidAccount",
                        type: "POST",
                        data: body,
                        dataType: "json",
                        success: function (data) {
                            getInvestmentHoldings(data.item_id);
                        },
                        error: function (err) {
                            console.log('Error linking Plaid account.');
                            const errMsg = JSON.parse(err);
                            console.error("Error linking Plaid account: ", err);
                        }
                    });
                },
                onExit: function (err, metadata) {
                    console.log("linkBankAccount error=", err, metadata);
                    const errMsg = JSON.parse(err);
                            console.error("Error linking Plaid account: ", err);
    
                    linkHandler.destroy();
                    if (metadata.link_session_id == null && metadata.status == "requires_credentials") {
                        createLinkToken();
                    }
                }
            });
            linkHandler.open();
        }
    
        function checkVerificationStatus() {
            if(window.isInitialVerificationChecked == true && isVerificationLinkClicked == false) {
                return true;
            }
            window.isInitialVerificationChecked = true;
    
            $.ajax({
                url: '/check-identity-verification-status',
                type: 'GET',
                data: {
                    _token: $("meta[name='csrf-token']").attr("content"),
                },
                success: function(response) {
                    if (response.status == false) {
                        setTimeout(function() {
                            checkVerificationStatus();
                        }, 1000);
                    } else {
                        updateContactInformation();
                        $('.identity-verification').fadeOut(500, function () {
                            $('.identity-verification').remove();
                            window.isVerificationLinkClicked = false;
                        });
                    }
                },
                error: function(data) {
                    console.log(data);
                }
            });
        }
    
        function getVerificationLink() {
            $.ajax({
                url: '/generate-verification-link',
                type: 'GET',
                data: {
                    _token: $("meta[name='csrf-token']").attr("content"),
                },
                success: function(response) {
                    $('.identity-verification').data('verification-link', response.verificationLink);
                },
                error: function(data) {
                    console.log(data);
                }
            });
        }
    
        function updateContactInformation() {
            $.ajax({
                url: '/update-contact-information',
                type: 'GET',
                data: {
                    _token: $("meta[name='csrf-token']").attr("content"),
                },
                success: function(response) {
                    if(response.status == true) {
                        Swal.fire({
                            title: 'Information updated!',
                            text: "We have updated your contact information. ",
                            icon: 'info',
                            showCancelButton: false,
                            confirmButtonText: 'View',
                            allowEscapeKey: false,
                            allowOutsideClick: false,
                        }).then(function(result) {
                            if (result.isConfirmed) {
                                window.location.href = '/account-settings/personal-info';
                            }
                        });
                    }
                },
                error: function(data) {
                    console.log(data);
                }
            });
        }
    
    });
    

    PlaidController.php

    <?php
    
    namespace App\Http\Controllers;
    
    use App\Interfaces\PlaidRepositoryInterface;
    use App\Models\PlaidAccount;
    use App\Models\PlaidIdentityVerification;
    use Illuminate\Http\Request;
    use TomorrowIdeas\Plaid\Plaid;
    use Illuminate\Support\Facades\Log;
    use TomorrowIdeas\Plaid\Entities\User;
    use TomorrowIdeas\Plaid\PlaidRequestException;
    
    class PlaidController extends Controller
    {
        private PlaidRepositoryInterface $plaidRepository;
    
        public function __construct(PlaidRepositoryInterface $plaidRepository)
        {
            $this->plaidRepository = $plaidRepository;
        }
    
        public function createLinkToken()
        {
            $user_id = 1;
            $plaidUser = new User($user_id);
            $plaid = new Plaid(env('PLAID_CLIENT_ID'), env('PLAID_SECRET'), env('PLAID_ENV'));
            $response = $plaid->tokens->create('Plaid Test', 'en', ['US'], $plaidUser, ['investments'], env('PLAID_WEBHOOK'));
    
            return response()->json([
                'result' => 'success',
                'data' => json_encode($response)
            ], 200);
        }
    
        public function storePlaidAccount(Request $request)
        {
            $validator = \Validator::make($request->all(), [
                'public_token' => ['required', 'string']
            ]);
    
            if ($validator->fails()) {
                return response()->json(['result' => 'error', 'message' => $validator->errors()], 201);
            }
    
            $user_id = 1;
            Log::info('-----------------------------------------');
            Log::info('Plaid public_token : ' . $request->public_token . ', link_token: ' . $request->link_token);
            $plaid = new Plaid(env('PLAID_CLIENT_ID'), env('PLAID_SECRET'), env('PLAID_ENV'));
            $obj = $plaid->items->exchangeToken($request->public_token);
            Log::info('Plaid exchange token : ' . json_encode($obj));
    
            try {
                \DB::transaction(function () use($request, $obj, $user_id) {
                    foreach($request->accounts as $account) {
                        $query = PlaidAccount::where('account_id', isset($account['id']) ? $account['id'] : $account['account_id']);
                        if ($query->count() > 0) {
                            Log::info('[Update Plaid Account]: ' . json_encode($account));
                            $new_account = $query->first();
                            $new_account->plaid_item_id = $obj->item_id;
                            $new_account->plaid_access_token = $obj->access_token;
                            $new_account->plaid_public_token = $request->public_token;
                            $new_account->link_session_id = $request->link_session_id;
                            $new_account->link_token = $request->link_token;
                            $new_account->institution_id = $request->institution['institution_id'];
                            $new_account->institution_name = $request->institution['name'];
                            $new_account->account_id = isset($account['id']) ? $account['id'] : $account['account_id'];
                            $new_account->account_name = isset($account['name']) ? $account['name'] : $account['account_name'];
                            $new_account->account_mask = isset($account['account_number']) ? $account['account_number'] : $account['mask'];
                            $new_account->account_mask = null;
                            $new_account->account_type = isset($account['type']) ? $account['type'] : $account['account_type'];
                            $new_account->account_subtype = isset($account['subtype']) ? $account['subtype'] : $account['account_sub_type'];
                            $new_account->user_id = $user_id;
                            $new_account->save();
                        } else {
                            Log::info('[New Plaid Account]: ' . json_encode($account));
                            $new_account = ([
                                'plaid_item_id' => $obj->item_id,
                                'plaid_access_token' => $obj->access_token,
                                'plaid_public_token' => $request->public_token,
                                'link_session_id' => $request->link_session_id,
                                'link_token' => $request->link_token,
                                'institution_id'    => $request->institution['institution_id'],
                                'institution_name' => $request->institution['name'],
                                'account_id' => isset($account['id']) ? $account['id'] : $account['account_id'],
                                'account_name' => isset($account['name']) ? $account['name'] : $account['account_name'],
                                'account_mask' => isset($account['account_number']) ? $account['account_number'] : $account['mask'],
                                'account_mask' => null,
                                'account_type' => isset($account['type']) ? $account['type'] : $account['account_type'],
                                'account_subtype' => isset($account['subtype']) ? $account['subtype'] : $account['account_sub_type'],
                                'user_id' => $user_id
                            ]);
                            PlaidAccount::create($new_account);
                        }
                    }
                });
            } catch (\Exception $e) {
                Log::error('An error occurred linking a Plaid account: ' . $e->getMessage());
                return response()->json([
                    'message' => 'An error occurred attempting to link a Plaid account.'
                ], 200);
            }
            return response()->json([
                'message' => 'Successfully linked plaid account.',
                'item_id' => $obj->item_id,
            ], 200);
        }
    
        public function checkIdentityVerificationStatus() {
            $status = $this->plaidRepository->checkVerificationStatus();
            return response()->json([
                'status' => $status
            ]);
        }
    
        public function generateVerificationLink()
        {
            return response()->json([
                'verificationLink' => $this->plaidRepository->generateVerificationLink(),
            ]);
        }
    
        public function updateContactInformation() {
            $status = $this->plaidRepository->updateContactInformation();
    
            return response()->json([
                'status' => $status,
            ]);
        }
    }
    

    PlaidRepositoryInterface.php

    <?php
    
    
    namespace App\Interfaces;
    
    
    interface PlaidRepositoryInterface
    {
        /**
         * To generate the Verification link
         * @return String
         */
        public function generateVerificationLink();
    
        /**
         * To check if the user has completed the verification or not
         * @return boolean
         */
        public function checkVerificationStatus();
    
        /**
         * To update the verification status
         * @param String $identityVerificationId
         * @return mixed
         */
        public function updateVerificationStatus($identityVerificationId);
    
        /**
         * To update the contact information if the information does not match with the IDV.
         * @return boolean
         */
        public function updateContactInformation();
    
        /**
         * To save the uploaded documents in our system
         * @param $response
         * @return void
         */
        public function saveDocumentFiles($response);
    
        /**
         * To restart the verification process from the starting
         * @return mixed
         */
        public function retryVerificationProcess();
    }
    

    PlaidRepository.php

    <?php
    
    
    namespace App\Repositories;
    
    
    use App\Interfaces\PlaidRepositoryInterface;
    use App\Models\Country;
    use App\Models\PlaidIdentityVerification;
    use App\Models\State;
    use App\Models\User;
    use Illuminate\Support\Facades\Auth;
    use Illuminate\Support\Facades\Log;
    use Illuminate\Support\Facades\Storage;
    
    class PlaidRepository implements PlaidRepositoryInterface
    {
        private function getEnvironmentURL()
        {
            $plaidEnvironments = [
                "production" => "https://production.plaid.com/",
                "development" => "https://development.plaid.com/",
                "sandbox" => "https://sandbox.plaid.com/",
            ];
    
            return $plaidEnvironments[env('PLAID_ENV') ?? 'production'];
        }
    
        public function generateVerificationLink()
        {
    
            if ($this->checkVerificationStatus() == true) {
                return true;
            }
    
            $user = Auth::user();
    
            $plaidIdentityVerification = PlaidIdentityVerification::where('user_id', Auth::user()->id)->first();
    
            if (is_null($plaidIdentityVerification) == false) {
                if ($plaidIdentityVerification->status == 'failed') {
                    return $this->retryVerificationProcess();
                }
                return $plaidIdentityVerification->verification_link;
            }
    
    
            $curl = curl_init();
    
            $payload = [
                "client_id" => env('PLAID_CLIENT_ID'),
                "secret" => env('PLAID_SECRET'),
                "template_id" => env('PLAID_IDENTITY_VERIFICATION_TEMPLATE_ID'),
                "gave_consent" => false,
                "is_shareable" => true,
                "is_idempotent" => true,
                "user" => [
                    "client_user_id" => 'user-' . $user->id,
                    "email_address" => $user->email,
                ]
            ];
    
            curl_setopt_array($curl, array(
                CURLOPT_URL => $this->getEnvironmentURL() . 'identity_verification/create',
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_ENCODING => '',
                CURLOPT_MAXREDIRS => 10,
                CURLOPT_TIMEOUT => 0,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
                CURLOPT_CUSTOMREQUEST => 'POST',
                CURLOPT_POSTFIELDS => json_encode($payload),
                CURLOPT_HTTPHEADER => array(
                    'Content-Type: application/json'
                ),
            ));
    
            $response = json_decode(curl_exec($curl), true);
    
            curl_close($curl);
    
            $plaidIdentityVerification = new PlaidIdentityVerification();
            $plaidIdentityVerification->user_id = $user->id;
            $plaidIdentityVerification->identity_verification_id = $response['id'];
            $plaidIdentityVerification->verification_link = $response['shareable_url'];
            $plaidIdentityVerification->save();
    
            return $response['shareable_url'];
        }
    
        public function checkVerificationStatus()
        {
            $plaidIdentityVerification = PlaidIdentityVerification::where('user_id', Auth::user()->id)->first();
    
            if (is_null($plaidIdentityVerification) == false) {
                $this->updateVerificationStatus($plaidIdentityVerification->identity_verification_id);
                if ($plaidIdentityVerification->status == 'success') {
                    return true;
                }
            }
    
            return false;
        }
    
        public function updateVerificationStatus($identityVerificationId)
        {
            $payload = [
                "client_id" => env('PLAID_CLIENT_ID'),
                "secret" => env('PLAID_SECRET'),
                "identity_verification_id" => $identityVerificationId,
            ];
    
            $curl = curl_init();
    
            curl_setopt_array($curl, array(
                CURLOPT_URL => $this->getEnvironmentURL() . 'identity_verification/get',
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_ENCODING => '',
                CURLOPT_MAXREDIRS => 10,
                CURLOPT_TIMEOUT => 0,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
                CURLOPT_CUSTOMREQUEST => 'POST',
                CURLOPT_POSTFIELDS => json_encode($payload),
                CURLOPT_HTTPHEADER => array(
                    'Content-Type: application/json'
                ),
            ));
    
            $response = json_decode(curl_exec($curl), true);
    
            if ($response['documentary_verification'] !== null) {
                $plaidIdentityVerification = PlaidIdentityVerification::where('user_id', Auth::user()->id)->first();
                $plaidIdentityVerification->status = $response['documentary_verification']['status'];
    
                if (isset($response['documentary_verification']['status']) && $response['documentary_verification']['status'] == 'success') {
                    Auth::user()->removeRole('User');
                    Auth::user()->assignRole('Host');
                }
    
                $plaidIdentityVerification->response = json_encode($response);
    
                $lastKey = array_keys($response['documentary_verification']['documents']);
                $lastKey = array_values(array_reverse($lastKey))[0];
    
                $plaidIdentityVerification->document_type = ucfirst(str_replace('_', ' ', $response['documentary_verification']['documents'][$lastKey]['extracted_data']['category']));
                $plaidIdentityVerification->save();
            }
    
            curl_close($curl);
        }
    
        /**
         * To update the contact information if the information does not match with the IDV.
         *
         * @return bool
         */
        public function updateContactInformation()
        {
            $user = Auth::user();
            $userProfile = $user->profile;
    
            $plaidIdentificationVerification = $user->plaidIdentityVerification;
            $plaidIdentificationVerificationResponse = json_decode($plaidIdentificationVerification->response, true);
    
            $isInformationUpdated = false;
            if (!empty($plaidIdentificationVerificationResponse)) {
                if (is_null($userProfile->country) || $userProfile->country->iso2 != $plaidIdentificationVerificationResponse['user']['address']['country']) {
                    $country = Country::where('iso2', $plaidIdentificationVerificationResponse['user']['address']['country'])->first();
                    $user->profile->update([
                        'country_id' => $country->id,
                    ]);
                    $isInformationUpdated = true;
                }
    
                if (is_null($userProfile->state) || $userProfile->state->iso2 != $plaidIdentificationVerificationResponse['user']['address']['region']) {
                    $state = State::where('iso2', $plaidIdentificationVerificationResponse['user']['address']['region'])->first();
                    $user->profile->update([
                        'state_id' => $state->id,
                    ]);
                    $isInformationUpdated = true;
                };
    
                if ($userProfile->city != $plaidIdentificationVerificationResponse['user']['address']['city']) {
                    $user->profile->update([
                        'city' => $plaidIdentificationVerificationResponse['user']['address']['city'],
                    ]);
                    $isInformationUpdated = true;
                };
    
                if ($userProfile->address1 != $plaidIdentificationVerificationResponse['user']['address']['street']) {
                    $user->profile->update([
                        'address1' => $plaidIdentificationVerificationResponse['user']['address']['street'],
                    ]);
                    $isInformationUpdated = true;
                };
    
                if ($userProfile->address2 != $plaidIdentificationVerificationResponse['user']['address']['street2']) {
                    $user->profile->update([
                        'address2' => $plaidIdentificationVerificationResponse['user']['address']['street2'],
                    ]);
                    $isInformationUpdated = true;
                };
    
                if ($userProfile->zip_code != $plaidIdentificationVerificationResponse['user']['address']['postal_code']) {
                    $user->profile->update([
                        'zip_code' => $plaidIdentificationVerificationResponse['user']['address']['postal_code'],
                    ]);
                    $isInformationUpdated = true;
                };
    
                if ($userProfile->dob != $plaidIdentificationVerificationResponse['user']['date_of_birth']) {
                    $user->profile->update([
                        'dob' => $plaidIdentificationVerificationResponse['user']['date_of_birth'],
                    ]);
                    $isInformationUpdated = true;
                };
    
                if ($user->first_name != $plaidIdentificationVerificationResponse['user']['name']['given_name']) {
                    $user->first_name = $plaidIdentificationVerificationResponse['user']['name']['given_name'];
                    $isInformationUpdated = true;
                };
    
                if ($user->last_name != $plaidIdentificationVerificationResponse['user']['name']['family_name']) {
                    $user->last_name = $plaidIdentificationVerificationResponse['user']['name']['family_name'];
                    $isInformationUpdated = true;
                };
    
                if ($user->phone != $plaidIdentificationVerificationResponse['user']['phone_number']) {
                    $user->phone = $plaidIdentificationVerificationResponse['user']['phone_number'];
                    $user->phone_verified_at = date('Y-m-d H:i:s');
                    $isInformationUpdated = true;
                };
    
                $user->save();
    
            }
    
            $this->saveDocumentFiles($plaidIdentificationVerificationResponse);
    
            return $isInformationUpdated;
        }
    
        public function saveDocumentFiles($response)
        {
            $user = Auth::user();
            $path = Storage::disk('public')->path('');
    
            if (file_exists($path . '/idv/') == false) {
                mkdir($path . '/idv/', 0755, true);
            }
    
            if (file_exists($path . '/idv/' . $user->id) == false) {
                mkdir($path . '/idv/' . $user->id, 0775, true);
            }
    
    
            if (isset($response['documentary_verification']['documents']) && !empty($response['documentary_verification']['documents'])) {
                $lastKey = array_keys($response['documentary_verification']['documents']);
                $lastKey = array_values(array_reverse($lastKey))[0];
    
                if (file_exists($path . '/idv/' . $user->id . '/photo.jpg') == false) {
                    Storage::disk('public')->put('/idv/' . $user->id . '/photo.jpg', file_get_contents($response['documentary_verification']['documents'][$lastKey]['images']['face']));
                }
                if (file_exists($path . '/idv/' . $user->id . '/front_photo.jpg') == false) {
                    Storage::disk('public')->put('/idv/' . $user->id . '/front_photo.jpg', file_get_contents($response['documentary_verification']['documents'][$lastKey]['images']['original_front']));
                }
                if (file_exists($path . '/idv/' . $user->id . '/back_photo.jpg') == false) {
                    Storage::disk('public')->put('/idv/' . $user->id . '/back_photo.jpg', file_get_contents($response['documentary_verification']['documents'][$lastKey]['images']['original_back']));
                }
            }
        }
    
        public function retryVerificationProcess()
        {
            $user = Auth::user();
    
            $curl = curl_init();
    
            $payload = [
                "client_id" => env('PLAID_CLIENT_ID'),
                "secret" => env('PLAID_SECRET'),
                "template_id" => env('PLAID_IDENTITY_VERIFICATION_TEMPLATE_ID'),
                "strategy" => "reset",
                "client_user_id" => 'user-' . $user->id,
            ];
    
            curl_setopt_array($curl, array(
                CURLOPT_URL => $this->getEnvironmentURL() . 'identity_verification/retry',
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_ENCODING => '',
                CURLOPT_MAXREDIRS => 10,
                CURLOPT_TIMEOUT => 0,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
                CURLOPT_CUSTOMREQUEST => 'POST',
                CURLOPT_POSTFIELDS => json_encode($payload),
                CURLOPT_HTTPHEADER => array(
                    'Content-Type: application/json'
                ),
            ));
    
            $response = json_decode(curl_exec($curl), true);
    
            curl_close($curl);
    
            $user->plaidIdentityVerification()->update([
                'identity_verification_id' => $response['id'],
                'verification_link' => $response['shareable_url'],
            ]);
    
            return $response['shareable_url'];
        }
    }