From d78b8748389a877771960eaf1f454364a874c257 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Sat, 30 Apr 2016 22:45:51 -0400 Subject: [PATCH] Add Plaid support --- app/Http/Controllers/PaymentController.php | 9 +- .../Controllers/PublicClientController.php | 8 +- app/Models/AccountGateway.php | 2 +- app/Services/PaymentService.php | 103 ++++++++-- public/images/plaid-logo.svg | 12 ++ public/images/plaid-logowhite.svg | 12 ++ resources/lang/en/texts.php | 4 + .../payments/add_paymentmethod.blade.php | 178 +++++++++++++----- .../views/payments/payment_css.blade.php | 76 +++++++- 9 files changed, 330 insertions(+), 74 deletions(-) create mode 100644 public/images/plaid-logo.svg create mode 100644 public/images/plaid-logowhite.svg 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()) + + + @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%; +}