Merge pull request #6842 from beganovich/v5-728

Checkout.com: Standalone credit card authorization
This commit is contained in:
David Bomba 2021-10-15 07:01:54 +11:00 committed by GitHub
commit 384642acde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 273 additions and 120 deletions

View File

@ -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;
@ -25,7 +28,7 @@ use Checkout\Models\Payments\TokenSource;
use Illuminate\Contracts\View\Factory;
use Illuminate\View\View;
class CreditCard
class CreditCard implements MethodInterface
{
use Utilities;
use MakesHash;
@ -38,6 +41,8 @@ class CreditCard
public function __construct(CheckoutComPaymentDriver $checkout)
{
$this->checkout = $checkout;
$this->checkout->init();
}
/**
@ -54,15 +59,50 @@ class CreditCard
}
/**
* Checkout.com supports doesn't support direct authorization of the credit card.
* Token can be saved after the first (successful) purchase.
* Handle authorization for credit card.
*
* @param mixed $data
* @return void
* @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 +120,6 @@ class CreditCard
public function paymentResponse(PaymentResponseRequest $request)
{
$this->checkout->init();
$state = [
'server_response' => json_decode($request->gateway_response),
'value' => $request->value,
@ -133,7 +171,6 @@ class CreditCard
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();
@ -161,7 +198,6 @@ class CreditCard
$response = $this->checkout->gateway->payments()->request($payment);
if ($response->status == 'Authorized') {
return $this->processSuccessfulPayment($response);
}
@ -181,7 +217,6 @@ class CreditCard
return $this->processUnsuccessfulPayment($response);
}
} catch (CheckoutHttpException $e) {
$this->checkout->unWindGatewayFees($this->checkout->payment_hash);
return $this->checkout->processInternallyFailedPayment($this->checkout, $e);
}

View File

@ -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<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}(new(function(){function e(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.button=document.querySelector("#pay-button")}var t,r,o;return t=e,(r=[{key:"init",value:function(){this.frames=Frames.init(document.querySelector("meta[name=public-key]").content)}},{key:"handle",value:function(){var e=this;this.init(),Frames.addEventHandler(Frames.Events.CARD_VALIDATION_CHANGED,(function(t){e.button.disabled=!Frames.isCardValid()})),Frames.addEventHandler(Frames.Events.CARD_TOKENIZED,(function(e){document.querySelector('input[name="gateway_response"]').value=JSON.stringify(e),document.getElementById("server_response").submit()})),document.querySelector("#authorization-form").addEventListener("submit",(function(t){e.button.disabled=!0,t.preventDefault(),Frames.submitCard()}))}}])&&n(t.prototype,r),o&&n(t,o),e}())).handle()}});

View File

@ -0,0 +1,9 @@
/**
* 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
*/

View File

@ -5,6 +5,7 @@
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1",
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7",
"/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js?id=f7f4ecfb1771951b91e7",
"/js/clients/payment_methods/authorize-checkout-card.js": "/js/clients/payment_methods/authorize-checkout-card.js?id=d23f32f956a8c8927339",
"/js/clients/payment_methods/braintree-ach.js": "/js/clients/payment_methods/braintree-ach.js?id=9fb7941baba1f9645ed9",
"/js/clients/payment_methods/wepay-bank-account.js": "/js/clients/payment_methods/wepay-bank-account.js?id=8fea0be371d430064a89",
"/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=7c2cbef525868592f42e",

View File

@ -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();

View File

@ -1,7 +1,44 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'Credit card', 'card_title' => 'Credit card'])
@section('gateway_head')
<meta name="public-key" content="{{ $gateway->getPublishableKey() }}">
@include('portal.ninja2020.gateways.checkout.credit_card.includes.styles')
<script src="https://cdn.checkout.com/js/framesv2.min.js"></script>
@endsection
@section('gateway_content')
@component('portal.ninja2020.components.general.card-element-single', ['title' => 'Credit card', 'show_title' => false])
{{ __('texts.checkout_authorize_label') }}
<form action="{{ route('client.payment_methods.store', ['method' => App\Models\GatewayType::CREDIT_CARD]) }}"
method="post" id="server_response">
@csrf
<input type="hidden" name="payment_method_id" value="{{ \App\Models\GatewayType::CREDIT_CARD }}">
<input type="hidden" name="gateway_response" id="gateway_response">
</form>
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.method')])
{{ ctrans('texts.credit_card') }}
@endcomponent
@component('portal.ninja2020.components.general.card-element-single')
<div id="checkout--container">
<form class="xl:flex xl:justify-center" id="authorization-form" method="POST" action="#">
<div class="one-liner">
<div class="card-frame">
<!-- form will be added here -->
</div>
<!-- add submit button -->
<button id="pay-button" disabled>
{{ ctrans('texts.add_payment_method') }}
</button>
</div>
<p class="success-payment-message"></p>
</form>
</div>
@endcomponent
@endsection
@section('gateway_footer')
<script src="{{ asset('js/clients/payment_methods/authorize-checkout-card.js') }}"></script>
@endsection

View File

@ -0,0 +1,101 @@
<style>
*,
*::after,
*::before {
box-sizing: border-box
}
#payment-form {
width: 100%;
}
iframe {
width: 100%
}
.one-liner {
display: flex;
flex-direction: column
}
#pay-button {
border: none;
border-radius: 3px;
color: #FFF;
font-weight: 500;
height: 40px;
width: 100%;
background-color: #13395E;
box-shadow: 0 1px 3px 0 rgba(19, 57, 94, 0.4)
}
#pay-button:active {
background-color: #0B2A49;
box-shadow: 0 1px 3px 0 rgba(19, 57, 94, 0.4)
}
#pay-button:hover {
background-color: #15406B;
box-shadow: 0 2px 5px 0 rgba(19, 57, 94, 0.4)
}
#pay-button:disabled {
background-color: #697887;
box-shadow: none
}
#pay-button:not(:disabled) {
cursor: pointer
}
.card-frame {
border: solid 1px #13395E;
border-radius: 3px;
width: 100%;
margin-bottom: 8px;
height: 40px;
box-shadow: 0 1px 3px 0 rgba(19, 57, 94, 0.2)
}
.card-frame.frame--rendered {
opacity: 1
}
.card-frame.frame--rendered.frame--focus {
border: solid 1px #13395E;
box-shadow: 0 2px 5px 0 rgba(19, 57, 94, 0.15)
}
.card-frame.frame--rendered.frame--invalid {
border: solid 1px #D96830;
box-shadow: 0 2px 5px 0 rgba(217, 104, 48, 0.15)
}
.success-payment-message {
color: #13395E;
line-height: 1.4
}
.token {
color: #b35e14;
font-size: 0.9rem;
font-family: monospace
}
@media screen and (min-width: 31rem) {
.one-liner {
flex-direction: row
}
.card-frame {
width: 318px;
margin-bottom: 0
}
#pay-button {
width: 175px;
margin-left: 8px
}
}
</style>

View File

@ -7,101 +7,7 @@
<meta name="currency" content="{{ $currency }}">
<meta name="reference" content="{{ $payment_hash }}">
<style>*, *::after, *::before {
box-sizing: border-box
}
#payment-form {
width: 100%;
}
iframe {
width: 100%
}
.one-liner {
display: flex;
flex-direction: column
}
#pay-button {
border: none;
border-radius: 3px;
color: #FFF;
font-weight: 500;
height: 40px;
width: 100%;
background-color: #13395E;
box-shadow: 0 1px 3px 0 rgba(19, 57, 94, 0.4)
}
#pay-button:active {
background-color: #0B2A49;
box-shadow: 0 1px 3px 0 rgba(19, 57, 94, 0.4)
}
#pay-button:hover {
background-color: #15406B;
box-shadow: 0 2px 5px 0 rgba(19, 57, 94, 0.4)
}
#pay-button:disabled {
background-color: #697887;
box-shadow: none
}
#pay-button:not(:disabled) {
cursor: pointer
}
.card-frame {
border: solid 1px #13395E;
border-radius: 3px;
width: 100%;
margin-bottom: 8px;
height: 40px;
box-shadow: 0 1px 3px 0 rgba(19, 57, 94, 0.2)
}
.card-frame.frame--rendered {
opacity: 1
}
.card-frame.frame--rendered.frame--focus {
border: solid 1px #13395E;
box-shadow: 0 2px 5px 0 rgba(19, 57, 94, 0.15)
}
.card-frame.frame--rendered.frame--invalid {
border: solid 1px #D96830;
box-shadow: 0 2px 5px 0 rgba(217, 104, 48, 0.15)
}
.success-payment-message {
color: #13395E;
line-height: 1.4
}
.token {
color: #b35e14;
font-size: 0.9rem;
font-family: monospace
}
@media screen and (min-width: 31rem) {
.one-liner {
flex-direction: row
}
.card-frame {
width: 318px;
margin-bottom: 0
}
#pay-button {
width: 175px;
margin-left: 8px
}
}</style>
@include('portal.ninja2020.gateways.checkout.credit_card.includes.styles')
<script src="https://cdn.checkout.com/js/framesv2.min.js"></script>
@endsection

View File

@ -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);
});
}
}

4
webpack.mix.js vendored
View File

@ -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');