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?
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'];
}
}