From 8053de40d24a85a1ff1d0a4b523436969df83f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 14 Oct 2021 18:20:27 +0200 Subject: [PATCH 1/7] Extract CSS into separate file --- .../checkout/credit_card/authorize.blade.php | 41 ++++++- .../credit_card/includes/styles.blade.php | 101 ++++++++++++++++++ .../checkout/credit_card/pay.blade.php | 96 +---------------- 3 files changed, 141 insertions(+), 97 deletions(-) create mode 100644 resources/views/portal/ninja2020/gateways/checkout/credit_card/includes/styles.blade.php diff --git a/resources/views/portal/ninja2020/gateways/checkout/credit_card/authorize.blade.php b/resources/views/portal/ninja2020/gateways/checkout/credit_card/authorize.blade.php index 9bf42c2b22a2..61b758aebe14 100644 --- a/resources/views/portal/ninja2020/gateways/checkout/credit_card/authorize.blade.php +++ b/resources/views/portal/ninja2020/gateways/checkout/credit_card/authorize.blade.php @@ -1,7 +1,44 @@ @extends('portal.ninja2020.layout.payments', ['gateway_title' => 'Credit card', 'card_title' => 'Credit card']) +@section('gateway_head') + @include('portal.ninja2020.gateways.checkout.credit_card.includes.styles') + + +@endsection + @section('gateway_content') - @component('portal.ninja2020.components.general.card-element-single', ['title' => 'Credit card', 'show_title' => false]) - {{ __('texts.checkout_authorize_label') }} + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.method')]) + {{ ctrans('texts.credit_card') }} + @endcomponent + + @component('portal.ninja2020.components.general.card-element-single') +
+
+
+
+ +
+ + +
+

+
+
@endcomponent @endsection + +@section('gateway_footer') + +@endsection diff --git a/resources/views/portal/ninja2020/gateways/checkout/credit_card/includes/styles.blade.php b/resources/views/portal/ninja2020/gateways/checkout/credit_card/includes/styles.blade.php new file mode 100644 index 000000000000..d3a529340b49 --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/checkout/credit_card/includes/styles.blade.php @@ -0,0 +1,101 @@ + diff --git a/resources/views/portal/ninja2020/gateways/checkout/credit_card/pay.blade.php b/resources/views/portal/ninja2020/gateways/checkout/credit_card/pay.blade.php index 3a2a67fc400c..4ac991261daa 100644 --- a/resources/views/portal/ninja2020/gateways/checkout/credit_card/pay.blade.php +++ b/resources/views/portal/ninja2020/gateways/checkout/credit_card/pay.blade.php @@ -7,101 +7,7 @@ - + @include('portal.ninja2020.gateways.checkout.credit_card.includes.styles') @endsection From 5f2de60c9cf8dd30e46dfe98975a47d59e4e100d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 14 Oct 2021 18:43:41 +0200 Subject: [PATCH 2/7] Handle authorization on frontend --- .../checkout/credit_card/authorize.blade.php | 55 ++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/resources/views/portal/ninja2020/gateways/checkout/credit_card/authorize.blade.php b/resources/views/portal/ninja2020/gateways/checkout/credit_card/authorize.blade.php index 61b758aebe14..327ab1fbaedd 100644 --- a/resources/views/portal/ninja2020/gateways/checkout/credit_card/authorize.blade.php +++ b/resources/views/portal/ninja2020/gateways/checkout/credit_card/authorize.blade.php @@ -1,19 +1,29 @@ @extends('portal.ninja2020.layout.payments', ['gateway_title' => 'Credit card', 'card_title' => 'Credit card']) @section('gateway_head') + + @include('portal.ninja2020.gateways.checkout.credit_card.includes.styles') @endsection @section('gateway_content') +
+ @csrf + + + +
+ @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.method')]) {{ ctrans('texts.credit_card') }} @endcomponent @component('portal.ninja2020.components.general.card-element-single')
-
+
@@ -31,14 +41,43 @@ @section('gateway_footer') @endsection From 1b3a1092a61d25f397da05e0a726834ef8dd8ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 14 Oct 2021 18:43:47 +0200 Subject: [PATCH 3/7] Handle authorization on backend --- app/PaymentDrivers/CheckoutCom/CreditCard.php | 59 +++++++++++++++---- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/app/PaymentDrivers/CheckoutCom/CreditCard.php b/app/PaymentDrivers/CheckoutCom/CreditCard.php index 0a76f483750a..1a6de72311ab 100644 --- a/app/PaymentDrivers/CheckoutCom/CreditCard.php +++ b/app/PaymentDrivers/CheckoutCom/CreditCard.php @@ -14,9 +14,12 @@ namespace App\PaymentDrivers\CheckoutCom; use App\Exceptions\PaymentFailed; use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; +use App\Http\Requests\Request; use App\Jobs\Mail\PaymentFailureMailer; use App\Models\ClientGatewayToken; +use App\Models\GatewayType; use App\PaymentDrivers\CheckoutComPaymentDriver; +use App\PaymentDrivers\Common\MethodInterface; use App\Utils\Traits\MakesHash; use Checkout\Library\Exceptions\CheckoutHttpException; use Checkout\Models\Payments\IdSource; @@ -24,8 +27,9 @@ use Checkout\Models\Payments\Payment; use Checkout\Models\Payments\TokenSource; use Illuminate\Contracts\View\Factory; use Illuminate\View\View; +use Omnipay\Common\Message\RedirectResponseInterface; -class CreditCard +class CreditCard implements MethodInterface { use Utilities; use MakesHash; @@ -38,6 +42,8 @@ class CreditCard public function __construct(CheckoutComPaymentDriver $checkout) { $this->checkout = $checkout; + + $this->checkout->init(); } /** @@ -54,15 +60,50 @@ class CreditCard } /** - * Checkout.com supports doesn't support direct authorization of the credit card. - * Token can be saved after the first (successful) purchase. - * - * @param mixed $data - * @return void + * Handle authorization for credit card. + * + * @param Request $request + * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse */ - public function authorizeResponse($data) + public function authorizeResponse(Request $request) { - return; + $gateway_response = \json_decode($request->gateway_response); + + $method = new TokenSource( + $gateway_response->token + ); + + $payment = new Payment($method, 'USD'); + $payment->amount = 100; // $1 + $payment->reference = '$1 payment for authorization.'; + $payment->capture = false; + + try { + $response = $this->checkout->gateway->payments()->request($payment); + + if ($response->approved && $response->status === 'Authorized') { + $payment_meta = new \stdClass; + $payment_meta->exp_month = (string) $response->source['expiry_month']; + $payment_meta->exp_year = (string) $response->source['expiry_year']; + $payment_meta->brand = (string) $response->source['scheme']; + $payment_meta->last4 = (string) $response->source['last4']; + $payment_meta->type = (int) GatewayType::CREDIT_CARD; + + $data = [ + 'payment_meta' => $payment_meta, + 'token' => $response->source['id'], + 'payment_method_id' => GatewayType::CREDIT_CARD, + ]; + + $payment_method = $this->checkout->storeGatewayToken($data); + + return redirect()->route('client.payment_methods.show', $payment_method->hashed_id); + } + } catch(CheckoutHttpException $exception) { + throw new PaymentFailed( + $exception->getMessage() + ); + } } public function paymentView($data) @@ -80,8 +121,6 @@ class CreditCard public function paymentResponse(PaymentResponseRequest $request) { - $this->checkout->init(); - $state = [ 'server_response' => json_decode($request->gateway_response), 'value' => $request->value, From 9264b6b80ea880dbef16b8ec469cd61a792612bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 14 Oct 2021 18:44:07 +0200 Subject: [PATCH 4/7] Apply styles --- app/PaymentDrivers/CheckoutCom/CreditCard.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/PaymentDrivers/CheckoutCom/CreditCard.php b/app/PaymentDrivers/CheckoutCom/CreditCard.php index 1a6de72311ab..4859e2f1cb9f 100644 --- a/app/PaymentDrivers/CheckoutCom/CreditCard.php +++ b/app/PaymentDrivers/CheckoutCom/CreditCard.php @@ -27,7 +27,6 @@ use Checkout\Models\Payments\Payment; use Checkout\Models\Payments\TokenSource; use Illuminate\Contracts\View\Factory; use Illuminate\View\View; -use Omnipay\Common\Message\RedirectResponseInterface; class CreditCard implements MethodInterface { @@ -61,9 +60,9 @@ class CreditCard implements MethodInterface /** * Handle authorization for credit card. - * - * @param Request $request - * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse + * + * @param Request $request + * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse */ public function authorizeResponse(Request $request) { @@ -99,7 +98,7 @@ class CreditCard implements MethodInterface return redirect()->route('client.payment_methods.show', $payment_method->hashed_id); } - } catch(CheckoutHttpException $exception) { + } catch (CheckoutHttpException $exception) { throw new PaymentFailed( $exception->getMessage() ); @@ -172,7 +171,6 @@ class CreditCard implements MethodInterface private function completePayment($method, PaymentResponseRequest $request) { - $payment = new Payment($method, $this->checkout->payment_hash->data->currency); $payment->amount = $this->checkout->payment_hash->data->value; $payment->reference = $this->checkout->getDescription(); @@ -200,7 +198,6 @@ class CreditCard implements MethodInterface $response = $this->checkout->gateway->payments()->request($payment); if ($response->status == 'Authorized') { - return $this->processSuccessfulPayment($response); } @@ -220,7 +217,6 @@ class CreditCard implements MethodInterface return $this->processUnsuccessfulPayment($response); } } catch (CheckoutHttpException $e) { - $this->checkout->unWindGatewayFees($this->checkout->payment_hash); return $this->checkout->processInternallyFailedPayment($this->checkout, $e); } From 0db3f3dd573df227bcce8cd0840acac1d5ed7e71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 14 Oct 2021 18:45:48 +0200 Subject: [PATCH 5/7] Extract script into separate file --- .../authorize-checkout-card.js | 51 +++++++++++++++++++ .../checkout/credit_card/authorize.blade.php | 41 +-------------- webpack.mix.js | 4 ++ 3 files changed, 56 insertions(+), 40 deletions(-) create mode 100644 resources/js/clients/payment_methods/authorize-checkout-card.js diff --git a/resources/js/clients/payment_methods/authorize-checkout-card.js b/resources/js/clients/payment_methods/authorize-checkout-card.js new file mode 100644 index 000000000000..4fdcc926cd50 --- /dev/null +++ b/resources/js/clients/payment_methods/authorize-checkout-card.js @@ -0,0 +1,51 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://opensource.org/licenses/AAL + */ + +class CheckoutCreditCardAuthorization { + constructor() { + this.button = document.querySelector('#pay-button'); + } + + init() { + this.frames = Frames.init( + document.querySelector('meta[name=public-key]').content + ); + } + + handle() { + this.init(); + + Frames.addEventHandler( + Frames.Events.CARD_VALIDATION_CHANGED, + (event) => { + this.button.disabled = !Frames.isCardValid(); + } + ); + + Frames.addEventHandler(Frames.Events.CARD_TOKENIZED, (event) => { + document.querySelector( + 'input[name="gateway_response"]' + ).value = JSON.stringify(event); + + document.getElementById('server_response').submit(); + }); + + document + .querySelector('#authorization-form') + .addEventListener('submit', (event) => { + this.button.disabled = true; + + event.preventDefault(); + Frames.submitCard(); + }); + } +} + +new CheckoutCreditCardAuthorization().handle(); diff --git a/resources/views/portal/ninja2020/gateways/checkout/credit_card/authorize.blade.php b/resources/views/portal/ninja2020/gateways/checkout/credit_card/authorize.blade.php index 327ab1fbaedd..eb0f26e4d241 100644 --- a/resources/views/portal/ninja2020/gateways/checkout/credit_card/authorize.blade.php +++ b/resources/views/portal/ninja2020/gateways/checkout/credit_card/authorize.blade.php @@ -40,44 +40,5 @@ @endsection @section('gateway_footer') - + @endsection diff --git a/webpack.mix.js b/webpack.mix.js index 391a151007e9..34b60abfe3bb 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -114,6 +114,10 @@ mix.js("resources/js/app.js", "public/js") "resources/js/clients/payments/stripe-sepa.js", "public/js/clients/payments/stripe-sepa.js" ) + .js( + "resources/js/clients/payment_methods/authorize-checkout-card.js", + "public/js/clients/payment_methods/authorize-checkout-card.js" + ) mix.copyDirectory('node_modules/card-js/card-js.min.css', 'public/css/card-js.min.css'); From 81b06c25cddaf9c577c2b66dff0fec3a17feecfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 14 Oct 2021 18:50:01 +0200 Subject: [PATCH 6/7] Tests --- .../Gateways/CheckoutCom/CreditCardTest.php | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/tests/Browser/ClientPortal/Gateways/CheckoutCom/CreditCardTest.php b/tests/Browser/ClientPortal/Gateways/CheckoutCom/CreditCardTest.php index 691779f30241..b4a242719b56 100644 --- a/tests/Browser/ClientPortal/Gateways/CheckoutCom/CreditCardTest.php +++ b/tests/Browser/ClientPortal/Gateways/CheckoutCom/CreditCardTest.php @@ -38,17 +38,6 @@ class CreditCardTest extends DuskTestCase }); } - public function testAddingPaymentMethodShouldntBePossible() - { - $this->browse(function (Browser $browser) { - $browser - ->visitRoute('client.payment_methods.index') - ->press('Add Payment Method') - ->clickLink('Credit Card') - ->assertSee('Checkout.com can be can saved as payment method for future use, once you complete your first transaction. Don\'t forget to check "Store credit card details" during payment process.'); - }); - } - public function testPayWithNewCard() { $this->browse(function (Browser $browser) { @@ -117,4 +106,22 @@ class CreditCardTest extends DuskTestCase ->assertSee('Payment method has been successfully removed.'); }); } + + public function testAddingCreditCardStandalone() + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.payment_methods.index') + ->press('Add Payment Method') + ->clickLink('Credit Card') + ->withinFrame('iframe', function (Browser $browser) { + $browser + ->type('cardnumber', '4242424242424242') + ->type('exp-date', '04/22') + ->type('cvc', '100'); + }) + ->press('#pay-button') + ->waitForText('Details of payment method', 60); + }); + } } From 5d595222ac8669bfe86d76e4caf0813ee39e7376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 14 Oct 2021 18:50:04 +0200 Subject: [PATCH 7/7] Assets production build --- .../clients/payment_methods/authorize-checkout-card.js | 2 ++ .../authorize-checkout-card.js.LICENSE.txt | 9 +++++++++ public/mix-manifest.json | 1 + 3 files changed, 12 insertions(+) create mode 100644 public/js/clients/payment_methods/authorize-checkout-card.js create mode 100644 public/js/clients/payment_methods/authorize-checkout-card.js.LICENSE.txt diff --git a/public/js/clients/payment_methods/authorize-checkout-card.js b/public/js/clients/payment_methods/authorize-checkout-card.js new file mode 100644 index 000000000000..42b35a2db4d4 --- /dev/null +++ b/public/js/clients/payment_methods/authorize-checkout-card.js @@ -0,0 +1,2 @@ +/*! For license information please see authorize-checkout-card.js.LICENSE.txt */ +!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=29)}({29:function(e,t,n){e.exports=n("kduS")},kduS:function(e,t){function n(e,t){for(var n=0;n