diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php
index 8801ec56d07a..374665e32506 100644
--- a/app/Http/Controllers/PaymentController.php
+++ b/app/Http/Controllers/PaymentController.php
@@ -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');
diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php
index 1c05920be6eb..477c8912d6ac 100644
--- a/app/Http/Controllers/PublicClientController.php
+++ b/app/Http/Controllers/PublicClientController.php
@@ -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)) {
diff --git a/app/Models/AccountGateway.php b/app/Models/AccountGateway.php
index 6236f1c15b57..13a990bb3ae4 100644
--- a/app/Models/AccountGateway.php
+++ b/app/Models/AccountGateway.php
@@ -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';
}
}
diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php
index 462bc918b943..c143b91dc5eb 100644
--- a/app/Services/PaymentService.php
+++ b/app/Services/PaymentService.php
@@ -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();
+ }
+ }
}
diff --git a/public/images/plaid-logo.svg b/public/images/plaid-logo.svg
new file mode 100644
index 000000000000..f18a73dfe18d
--- /dev/null
+++ b/public/images/plaid-logo.svg
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/public/images/plaid-logowhite.svg b/public/images/plaid-logowhite.svg
new file mode 100644
index 000000000000..d30d97299b13
--- /dev/null
+++ b/public/images/plaid-logowhite.svg
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php
index a6af30f5c03a..bdd334d5454b 100644
--- a/resources/lang/en/texts.php
+++ b/resources/lang/en/texts.php
@@ -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;
diff --git a/resources/views/payments/add_paymentmethod.blade.php b/resources/views/payments/add_paymentmethod.blade.php
index 1f1a08bc8b10..fe83a6a8413e 100644
--- a/resources/views/payments/add_paymentmethod.blade.php
+++ b/resources/views/payments/add_paymentmethod.blade.php
@@ -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($('').val(public_token));
+ $form.append($('').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();
+ }
+ };
@else
-
+ @if ($accountGateway->getPlaidEnabled())
+
{{ trans('texts.secured_by_plaid') }}
+
+ @endif
@stop
\ No newline at end of file
diff --git a/resources/views/payments/payment_css.blade.php b/resources/views/payments/payment_css.blade.php
index 538dcd886561..c1e92dfa7522 100644
--- a/resources/views/payments/payment_css.blade.php
+++ b/resources/views/payments/payment_css.blade.php
@@ -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%;
+}