mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Add Plaid support
This commit is contained in:
parent
92552dc94f
commit
d78b874838
@ -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');
|
||||
|
@ -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)) {
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,13 +293,33 @@ class PaymentService extends BaseService
|
||||
}
|
||||
|
||||
if ($accountGateway->gateway->id == GATEWAY_STRIPE) {
|
||||
if (!empty($details['plaidPublicToken'])) {
|
||||
$plaidResult = $this->getPlaidToken($accountGateway, $details['plaidPublicToken'], $details['plaidAccountId']);
|
||||
|
||||
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'];
|
||||
}
|
||||
|
||||
$tokenResponse = $gateway->createCard($details)->send();
|
||||
|
||||
if ($tokenResponse->isSuccessful()) {
|
||||
$sourceReference = $tokenResponse->getCardReference();
|
||||
if (!$customerReference) {
|
||||
$customerReference = $tokenResponse->getCustomerReference();
|
||||
}
|
||||
|
||||
if (!$sourceReference) {
|
||||
$responseData = $tokenResponse->getData();
|
||||
if ($responseData['object'] == 'bank_account' || $responseData['object'] == 'card') {
|
||||
if (!empty($responseData['object']) && ($responseData['object'] == 'bank_account' || $responseData['object'] == 'card')) {
|
||||
$sourceReference = $responseData['id'];
|
||||
}
|
||||
}
|
||||
@ -297,7 +328,14 @@ class PaymentService extends BaseService
|
||||
// This customer was just created; find the card
|
||||
$data = $tokenResponse->getData();
|
||||
if (!empty($data['default_source'])) {
|
||||
$sourceReferebce = $data['default_source'];
|
||||
$sourceReference = $data['default_source'];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$data = $tokenResponse->getData();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
12
public/images/plaid-logo.svg
Normal file
12
public/images/plaid-logo.svg
Normal 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 |
12
public/images/plaid-logowhite.svg
Normal file
12
public/images/plaid-logowhite.svg
Normal 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 |
@ -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;
|
||||
|
@ -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,6 +411,24 @@
|
||||
|
||||
|
||||
@if($paymentType == PAYMENT_TYPE_STRIPE_ACH)
|
||||
@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>
|
||||
@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">
|
||||
@ -421,10 +486,18 @@
|
||||
->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
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user