Add Plaid support

This commit is contained in:
Joshua Dwire 2016-04-30 22:45:51 -04:00
parent 92552dc94f
commit d78b874838
9 changed files with 330 additions and 74 deletions

View File

@ -391,7 +391,7 @@ class PaymentController extends BaseController
'last_name' => 'required',
];
if ( ! Input::get('stripeToken') && ! Input::get('payment_method_nonce')) {
if ( ! Input::get('stripeToken') && ! Input::get('payment_method_nonce') && !(Input::get('plaidPublicToken') && Input::get('plaidAccountId'))) {
$rules = array_merge(
$rules,
[
@ -447,11 +447,6 @@ class PaymentController extends BaseController
// check if we're creating/using a billing token
if ($accountGateway->gateway_id == GATEWAY_STRIPE) {
if ($token = Input::get('stripeToken')) {
$details['token'] = $token;
unset($details['card']);
}
if ($useToken) {
$details['customerReference'] = $client->getGatewayToken();
unset($details['token']);
@ -464,7 +459,7 @@ class PaymentController extends BaseController
$details['token'] = $token;
$details['customerReference'] = $customerReference;
if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && empty($usingPlaid) ) {
if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && empty(Input::get('plaidPublicToken')) ) {
// The user needs to complete verification
Session::flash('message', trans('texts.bank_account_verification_next_steps'));
return Redirect::to('/client/paymentmethods');

View File

@ -794,10 +794,16 @@ class PublicClientController extends BaseController
if ($sourceToken) {
$details = array('token' => $sourceToken);
} elseif (Input::get('plaidPublicToken')) {
$usingPlaid = true;
$details = array('plaidPublicToken' => Input::get('plaidPublicToken'), 'plaidAccountId' => Input::get('plaidAccountId'));
}
if (!empty($details)) {
$gateway = $this->paymentService->createGateway($accountGateway);
$sourceId = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id);
} else {
return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv'));
return Redirect::to('payment/'.$invitation->invitation_key)->withInput(Request::except('cvv'));
}
if(empty($sourceId)) {

View File

@ -117,7 +117,7 @@ class AccountGateway extends EntityModel
$stripe_key = $this->getPublishableStripeKey();
return substr(trim($stripe_key), 0, 8) == 'sk_test_' ? 'tartan' : 'production';
return substr(trim($stripe_key), 0, 8) == 'pk_test_' ? 'tartan' : 'production';
}
}

View File

@ -93,6 +93,17 @@ class PaymentService extends BaseService
$data['ButtonSource'] = 'InvoiceNinja_SP';
};
if($input && $accountGateway->isGateway(GATEWAY_STRIPE)) {
if (!empty($input['stripeToken'])) {
$data['token'] = $input['stripeToken'];
unset($details['card']);
} elseif (!empty($input['plaidPublicToken'])) {
$data['plaidPublicToken'] = $input['plaidPublicToken'];
$data['plaidAccountId'] = $input['plaidAccountId'];
unset($data['card']);
}
}
return $data;
}
@ -282,22 +293,49 @@ class PaymentService extends BaseService
}
if ($accountGateway->gateway->id == GATEWAY_STRIPE) {
$tokenResponse = $gateway->createCard($details)->send();
$sourceReference = $tokenResponse->getCardReference();
$customerReference = $tokenResponse->getCustomerReference();
if (!empty($details['plaidPublicToken'])) {
$plaidResult = $this->getPlaidToken($accountGateway, $details['plaidPublicToken'], $details['plaidAccountId']);
if (!$sourceReference) {
$responseData = $tokenResponse->getData();
if ($responseData['object'] == 'bank_account' || $responseData['object'] == 'card') {
$sourceReference = $responseData['id'];
if (is_string($plaidResult)) {
$this->lastError = $plaidResult;
return;
} elseif (!$plaidResult) {
$this->lastError = 'No token received from Plaid';
return;
}
unset($details['plaidPublicToken']);
unset($details['plaidAccountId']);
$details['token'] = $plaidResult['stripe_bank_account_token'];
}
if ($customerReference == $sourceReference) {
// This customer was just created; find the card
$tokenResponse = $gateway->createCard($details)->send();
if ($tokenResponse->isSuccessful()) {
$sourceReference = $tokenResponse->getCardReference();
if (!$customerReference) {
$customerReference = $tokenResponse->getCustomerReference();
}
if (!$sourceReference) {
$responseData = $tokenResponse->getData();
if (!empty($responseData['object']) && ($responseData['object'] == 'bank_account' || $responseData['object'] == 'card')) {
$sourceReference = $responseData['id'];
}
}
if ($customerReference == $sourceReference) {
// This customer was just created; find the card
$data = $tokenResponse->getData();
if (!empty($data['default_source'])) {
$sourceReference = $data['default_source'];
}
}
} else {
$data = $tokenResponse->getData();
if (!empty($data['default_source'])) {
$sourceReferebce = $data['default_source'];
if ($data && $data['error'] && $data['error']['type'] == 'invalid_request_error') {
$this->lastError = $data['error']['message'];
return;
}
}
} elseif ($accountGateway->gateway->id == GATEWAY_BRAINTREE) {
@ -312,7 +350,7 @@ class PaymentService extends BaseService
$details['customerId'] = $customerReference;
$tokenResponse = $gateway->createPaymentMethod($details)->send();
$cardReference = $tokenResponse->getData()->paymentMethod->token;
$sourceReference = $tokenResponse->getData()->paymentMethod->token;
}
}
@ -809,4 +847,45 @@ class PaymentService extends BaseService
return $e->getMessage();
}
}
private function getPlaidToken($accountGateway, $publicToken, $accountId) {
$clientId = $accountGateway->getPlaidClientId();
$secret = $accountGateway->getPlaidSecret();
if (!$clientId) {
return 'No client ID set';
}
if (!$secret) {
return 'No secret set';
}
try{
$subdomain = $accountGateway->getPlaidEnvironment() == 'production' ? 'api' : 'tartan';
$response = (new \GuzzleHttp\Client(['base_uri'=>"https://{$subdomain}.plaid.com"]))->request(
'POST',
'exchange_token',
[
'allow_redirects' => false,
'headers' => ['content-type' => 'application/x-www-form-urlencoded'],
'body' => http_build_query(array(
'client_id' => $clientId,
'secret' => $secret,
'public_token' => $publicToken,
'account_id' => $accountId,
))
]
);
return json_decode($response->getBody(), true);
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
$response = $e->getResponse();
$body = json_decode($response->getBody(), true);
if ($body && !empty($body['message'])) {
return $body['message'];
}
return $e->getMessage();
}
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" enable-background="new 0 0 30 30" xml:space="preserve">
<path id="XMLID_14_" fill="#275A76" d="M15.1,3.2l-3.2-3.3L2.5,2.3L0,11.6l3.2,3.3l-3.3,3.2l2.4,9.3l9.3,2.6l3.3-3.2l3.2,3.3
l9.3-2.4l2.6-9.3l-3.2-3.3l3.3-3.2l-2.4-9.3L18.4,0L15.1,3.2z M13.2,5l-3.1,3.1l-3.9-4l4.9-1.3L13.2,5z M15,16.9l3.1,3.1l-3.1,3.1
L11.8,20L15,16.9z M10,18.1l-3.1-3.1l3.1-3.1l3.1,3.1L10,18.1z M16.9,15l3.1-3.1l3.1,3.1L20,18.2L16.9,15z M15,13.1L11.9,10l3.1-3.1
l3.1,3.1L15,13.1z M2.9,10.9L4.3,6l3.9,4l-3.1,3.1L2.9,10.9z M5,16.8l3.1,3.1l-4,3.9l-1.3-4.9L5,16.8z M10.9,27.1L6,25.7l4-3.9
l3.1,3.1L10.9,27.1z M16.8,25l3.1-3.1l3.9,4l-4.9,1.3L16.8,25z M27.1,19.1L25.7,24l-3.9-4l3.1-3.1L27.1,19.1z M25,13.2l-3.1-3.1
l4-3.9l1.3,4.9L25,13.2z M24,4.3l-4,3.9l-3.1-3.1l2.2-2.2L24,4.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" enable-background="new 0 0 30 30" xml:space="preserve">
<path id="XMLID_14_" fill="#FFFFFF" d="M15.1,3.2l-3.2-3.3L2.5,2.3L0,11.6l3.2,3.3l-3.3,3.2l2.4,9.3l9.3,2.6l3.3-3.2l3.2,3.3
l9.3-2.4l2.6-9.3l-3.2-3.3l3.3-3.2l-2.4-9.3L18.4,0L15.1,3.2z M13.2,5l-3.1,3.1l-3.9-4l4.9-1.3L13.2,5z M15,16.9l3.1,3.1l-3.1,3.1
L11.8,20L15,16.9z M10,18.1l-3.1-3.1l3.1-3.1l3.1,3.1L10,18.1z M16.9,15l3.1-3.1l3.1,3.1L20,18.2L16.9,15z M15,13.1L11.9,10l3.1-3.1
l3.1,3.1L15,13.1z M2.9,10.9L4.3,6l3.9,4l-3.1,3.1L2.9,10.9z M5,16.8l3.1,3.1l-4,3.9l-1.3-4.9L5,16.8z M10.9,27.1L6,25.7l4-3.9
l3.1,3.1L10.9,27.1z M16.8,25l3.1-3.1l3.9,4l-4.9,1.3L16.8,25z M27.1,19.1L25.7,24l-3.9-4l3.1-3.1L27.1,19.1z M25,13.2l-3.1-3.1
l4-3.9l1.3,4.9L25,13.2z M24,4.3l-4,3.9l-3.1-3.1l2.2-2.2L24,4.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1259,6 +1259,10 @@ $LANG = array(
'payment_method_error' => 'There was an error adding your payment methd. Please try again later.',
'notification_invoice_payment_failed_subject' => 'Payment failed for Invoice :invoice',
'notification_invoice_payment_failed' => 'A payment made by client :client towards Invoice :invoice failed. The payment has been marked as failed and :amount has been added to the client\'s balance.',
'link_with_plaid' => 'Link Account Instantly with Plaid',
'link_manually' => 'Link Manually',
'secured_by_plaid' => 'Secured by Plaid',
'plaid_linked_status' => 'Your bank account at :bank'
);
return $LANG;

View File

@ -64,6 +64,8 @@
Stripe.setPublishableKey('{{ $accountGateway->getPublishableStripeKey() }}');
$(function() {
$('.payment-form').submit(function(event) {
if($('[name=plaidAccountId]').length)return;
var $form = $(this);
var data = {
@ -140,6 +142,31 @@
// Prevent the form from submitting with the default action
return false;
});
@if($accountGateway->getPlaidEnabled())
var plaidHandler = Plaid.create({
selectAccount: true,
env: '{{ $accountGateway->getPlaidEnvironment() }}',
clientName: {!! json_encode($account->getDisplayName()) !!},
key: '{{ $accountGateway->getPlaidPublicKey() }}',
product: 'auth',
onSuccess: plaidSuccessHandler,
onExit : function(){$('#secured_by_plaid').hide()}
});
$('#plaid_link_button').click(function(){plaidHandler.open();$('#secured_by_plaid').fadeIn()});
$('#plaid_unlink').click(function(e){
e.preventDefault();
$('#manual_container').fadeIn();
$('#plaid_linked').hide();
$('#plaid_link_button').show();
$('#pay_now_button').hide();
$('#add_account_button').show();
$('[name=plaidPublicToken]').remove();
$('[name=plaidAccountId]').remove();
$('[name=account_holder_type],#account_holder_name').attr('required','required');
})
@endif
});
function stripeResponseHandler(status, response) {
@ -164,6 +191,26 @@
$form.get(0).submit();
}
};
function plaidSuccessHandler(public_token, metadata) {
$('#secured_by_plaid').hide()
var $form = $('.payment-form');
$form.append($('<input type="hidden" name="plaidPublicToken"/>').val(public_token));
$form.append($('<input type="hidden" name="plaidAccountId"/>').val(metadata.account_id));
$('#plaid_linked_status').text('{{ trans('texts.plaid_linked_status') }}'.replace(':bank', metadata.institution.name));
$('#manual_container').fadeOut();
$('#plaid_linked').show();
$('#plaid_link_button').hide();
$('[name=account_holder_type],#account_holder_name').removeAttr('required');
var payNowBtn = $('#pay_now_button');
if(payNowBtn.length) {
payNowBtn.show();
$('#add_account_button').hide();
}
};
</script>
@else
<script type="text/javascript">
@ -364,67 +411,93 @@
@if($paymentType == PAYMENT_TYPE_STRIPE_ACH)
<p>{{ trans('texts.ach_verification_delay_help') }}</p>
<div class="row">
<div class="col-md-6">
<div class="radio">
{!! Former::radios('account_holder_type')->radios(array(
trans('texts.individual_account') => array('value' => 'individual'),
trans('texts.company_account') => array('value' => 'company'),
))->inline()->label(''); !!}
@if($accountGateway->getPlaidEnabled())
<div id="plaid_container">
<a class="btn btn-default btn-lg" id="plaid_link_button">
<img src="{{ URL::to('images/plaid-logo.svg') }}">
<img src="{{ URL::to('images/plaid-logowhite.svg') }}" class="hoverimg">
{{ trans('texts.link_with_plaid') }}
</a>
<div id="plaid_linked">
<div id="plaid_linked_status"></div>
<a href="#" id="plaid_unlink">{{ trans('texts.unlink') }}</a>
</div>
</div>
<div class="col-md-6">
{!! Former::text('account_holder_name')
->placeholder(trans('texts.account_holder_name'))
->label('') !!}
@endif
<div id="manual_container">
@if($accountGateway->getPlaidEnabled())
<div id="plaid_or"><span>{{ trans('texts.or') }}</span></div>
<h4>{{ trans('texts.link_manually') }}</h4>
@endif
<p>{{ trans('texts.ach_verification_delay_help') }}</p>
<div class="row">
<div class="col-md-6">
<div class="radio">
{!! Former::radios('account_holder_type')->radios(array(
trans('texts.individual_account') => array('value' => 'individual'),
trans('texts.company_account') => array('value' => 'company'),
))->inline()->label(''); !!}
</div>
</div>
<div class="col-md-6">
{!! Former::text('account_holder_name')
->placeholder(trans('texts.account_holder_name'))
->label('') !!}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
{!! Former::select('country')
->placeholder(trans('texts.country_id'))
->fromQuery($countries, 'name', 'iso_3166_2')
->addGroupClass('country-select')
->label('') !!}
<div class="row">
<div class="col-md-6">
{!! Former::select('country')
->placeholder(trans('texts.country_id'))
->fromQuery($countries, 'name', 'iso_3166_2')
->addGroupClass('country-select')
->label('') !!}
</div>
<div class="col-md-6">
{!! Former::select('currency')
->placeholder(trans('texts.currency_id'))
->fromQuery($currencies, 'name', 'code')
->addGroupClass('currency-select')
->label('') !!}
</div>
</div>
<div class="col-md-6">
{!! Former::select('currency')
->placeholder(trans('texts.currency_id'))
->fromQuery($currencies, 'name', 'code')
->addGroupClass('currency-select')
->label('') !!}
<div class="row">
<div class="col-md-6">
{!! Former::text('')
->id('routing_number')
->placeholder(trans('texts.routing_number'))
->label('') !!}
</div>
<div class="col-md-6">
<div id="bank_name"></div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
{!! Former::text('')
->id('routing_number')
->placeholder(trans('texts.routing_number'))
->label('') !!}
</div>
<div class="col-md-6">
<div id="bank_name"></div>
</div>
</div>
<div class="row">
<div class="col-md-6">
{!! Former::text('')
->id('account_number')
->placeholder(trans('texts.account_number'))
->label('') !!}
</div>
<div class="col-md-6">
{!! Former::text('')
->id('confirm_account_number')
->placeholder(trans('texts.confirm_account_number'))
->label('') !!}
<div class="row">
<div class="col-md-6">
{!! Former::text('')
->id('account_number')
->placeholder(trans('texts.account_number'))
->label('') !!}
</div>
<div class="col-md-6">
{!! Former::text('')
->id('confirm_account_number')
->placeholder(trans('texts.confirm_account_number'))
->label('') !!}
</div>
</div>
</div>
<center>
{!! Button::success(strtoupper(trans('texts.add_account')))
->submit()
->withAttributes(['id'=>'add_account_button'])
->large() !!}
@if($accountGateway->getPlaidEnabled() && !empty($amount))
{!! Button::success(strtoupper(trans('texts.pay_now') . ' - ' . $account->formatMoney($amount, $client, true) ))
->submit()
->withAttributes(['style'=>'display:none', 'id'=>'pay_now_button'])
->large() !!}
@endif
</center>
@else
<div class="row">
@ -607,5 +680,8 @@
});
</script>
@if ($accountGateway->getPlaidEnabled())
<a href="https://plaid.com/products/auth/" target="_blank" style="display:none" id="secured_by_plaid"><img src="{{ URL::to('images/plaid-logowhite.svg') }}">{{ trans('texts.secured_by_plaid') }}</a>
<script src="https://cdn.plaid.com/link/stable/link-initialize.js"></script>
@endif
@stop

View File

@ -113,8 +113,6 @@ header h3 em {
color: #eb8039;
}
.secure {
text-align: right;
float: right;
@ -136,6 +134,80 @@ header h3 em {
text-transform: uppercase;
}
#plaid_link_button img {
height:30px;
vertical-align:-7px;
margin-right:5px;
}
#plaid_link_button:hover img,
#plaid_link_button .hoverimg{
display:none;
}
#plaid_link_button:hover .hoverimg{
display:inline;
}
#plaid_link_button {
width:425px;
border-color:#2A5A74;
color:#2A5A74;
}
#plaid_link_button:hover {
width:425px;
background-color:#2A5A74;
color:#fff;
}
#plaid_or,
#plaid_container {
text-align:center
}
#plaid_or span{
background:#fff;
position:relative;
bottom:-11px;
font-size:125%;
padding:0 10px;
}
#plaid_or {
border-bottom:1px solid #000;
margin:10px 0 30px;
}
#secured_by_plaid{
position:fixed;
z-index:999999999;
bottom:5px;
left:5px;
color:#fff;
border:1px solid #fff;
padding:3px 7px 3px 3px;
border-radius:3px;
vertical-align:-5px;
text-decoration: none!important;
}
#secured_by_plaid img{
height:20px;
margin-right:5px;
}
#secured_by_plaid:hover{
background-color:#2A5A74;
}
#plaid_linked{
margin:40px 0;
display:none;
}
#plaid_linked_status {
margin-bottom:10px;
font-size:150%;
}
</style>