Fixes for Stripe payments (#3542)

* Payment fixes:
- Added new "process.js" inside of webpack.mix.js
- BasePaymentDriver now accepts raw array, no explode
- StripePaymentDriver now accepts raw array, no explode
- Removed 'form-control' class from #card-element
- New credit_card for processing payment

* Production build of assets
This commit is contained in:
Benjamin Beganović 2020-03-25 14:08:37 +01:00 committed by GitHub
parent 1a0c20aa9d
commit e93bdffc0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 253 additions and 9 deletions

View File

@ -44,7 +44,7 @@ use Omnipay\Omnipay;
class BasePaymentDriver
{
use SystemLogTrait;
/* The company gateway instance*/
protected $company_gateway;
@ -56,7 +56,7 @@ class BasePaymentDriver
/* Gateway capabilities */
protected $refundable = false;
/* Token billing */
protected $token_billing = false;
@ -194,7 +194,7 @@ class BasePaymentDriver
public function processPaymentView(array $data)
{
}
public function processPaymentResponse($request)
{
}
@ -251,7 +251,7 @@ class BasePaymentDriver
/*
$this->purchaseResponse = (array)$response->getData();*/
}
public function completePurchase($data)
{
$this->gateway();
@ -269,14 +269,14 @@ class BasePaymentDriver
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->currency_id = $this->client->getSetting('currency_id');
$payment->date = Carbon::now();
return $payment;
}
public function attachInvoices(Payment $payment, $hashed_ids) : Payment
{
$invoices = Invoice::whereIn('id', $this->transformKeys(explode(",", $hashed_ids)))
$invoices = Invoice::whereIn('id', $this->transformKeys($hashed_ids))
->whereClientId($this->client->id)
->get();

View File

@ -264,7 +264,7 @@ class StripePaymentDriver extends BasePaymentDriver
$gateway_type_id = $request->input('payment_method_id');
$hashed_ids = $request->input('hashed_ids');
$invoices = Invoice::whereIn('id', $this->transformKeys(explode(",", $hashed_ids)))
$invoices = Invoice::whereIn('id', $this->transformKeys($hashed_ids))
->whereClientId($this->client->id)
->get();
/**

2
public/js/clients/payments/process.js vendored Normal file
View File

@ -0,0 +1,2 @@
/*! For license information please see process.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=6)}({6:function(e,t,n){e.exports=n("OXGg")},OXGg: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(t,n){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.key=t,this.usingToken=n}var t,r,o;return t=e,(r=[{key:"setupStripe",value:function(){return this.stripe=Stripe(this.key),this.elements=this.stripe.elements(),this}},{key:"createElement",value:function(){return this.cardElement=this.elements.create("card"),this}},{key:"mountCardElement",value:function(){return this.cardElement.mount("#card-element"),this}},{key:"completePaymentUsingToken",value:function(){var e=this,t=document.getElementById("pay-now-with-token");this.stripe.handleCardPayment(t.dataset.secret,{payment_method:t.dataset.token}).then((function(t){return t.error?e.handleFailure(t.error.message):e.handleSuccess(t)}))}},{key:"completePaymentWithoutToken",value:function(){var e=this,t=document.getElementById("pay-now"),n=document.getElementById("cardholder-name");this.stripe.handleCardPayment(t.dataset.secret,this.cardElement,{payment_method_data:{billing_details:{name:n.value}}}).then((function(t){return t.error?e.handleFailure(t.error.message):e.handleSuccess(t)}))}},{key:"handleSuccess",value:function(e){document.querySelector('input[name="gateway_response"]').value=JSON.stringify(e.paymentIntent);var t=document.querySelector('input[name="token-billing-checkbox"]');t&&(document.querySelector('input[name="store_card"]').value=t.checked),document.getElementById("server-response").submit()}},{key:"handleFailure",value:function(e){var t=document.getElementById("errors");t.textContent="",t.textContent=e,t.hidden=!1}},{key:"handle",value:function(){var e=this;this.setupStripe(),this.usingToken&&document.getElementById("pay-now-with-token").addEventListener("click",(function(){return e.completePaymentUsingToken()})),this.usingToken||(this.createElement().mountCardElement(),document.getElementById("pay-now").addEventListener("click",(function(){return e.completePaymentWithoutToken()})))}}])&&n(t.prototype,r),o&&n(t,o),e}())(document.querySelector('meta[name="stripe-publishable-key"]').content,document.querySelector('meta[name="using-token"]').content).handle()}});

View File

@ -0,0 +1,9 @@
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/

123
resources/js/clients/payments/process.js vendored Normal file
View File

@ -0,0 +1,123 @@
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
class ProcessStripePayment {
constructor(key, usingToken) {
this.key = key;
this.usingToken = usingToken;
}
setupStripe() {
this.stripe = Stripe(this.key);
this.elements = this.stripe.elements();
return this;
}
createElement() {
this.cardElement = this.elements.create("card");
return this;
}
mountCardElement() {
this.cardElement.mount("#card-element");
return this;
}
completePaymentUsingToken() {
let payNowButton = document.getElementById("pay-now-with-token");
this.stripe
.handleCardPayment(payNowButton.dataset.secret, {
payment_method: payNowButton.dataset.token
})
.then(result => {
if (result.error) {
return this.handleFailure(result.error.message);
}
return this.handleSuccess(result);
});
}
completePaymentWithoutToken() {
let payNowButton = document.getElementById("pay-now");
let cardHolderName = document.getElementById("cardholder-name");
this.stripe
.handleCardPayment(payNowButton.dataset.secret, this.cardElement, {
payment_method_data: {
billing_details: { name: cardHolderName.value }
}
})
.then(result => {
if (result.error) {
return this.handleFailure(result.error.message);
}
return this.handleSuccess(result);
});
}
handleSuccess(result) {
document.querySelector(
'input[name="gateway_response"]'
).value = JSON.stringify(result.paymentIntent);
let tokenBillingCheckbox = document.querySelector(
'input[name="token-billing-checkbox"]'
);
if (tokenBillingCheckbox) {
document.querySelector('input[name="store_card"]').value =
tokenBillingCheckbox.checked;
}
document.getElementById("server-response").submit();
}
handleFailure(message) {
let errors = document.getElementById("errors");
errors.textContent = "";
errors.textContent = message;
errors.hidden = false;
}
handle() {
this.setupStripe();
if (this.usingToken) {
document
.getElementById("pay-now-with-token")
.addEventListener("click", () => {
return this.completePaymentUsingToken();
});
}
if (!this.usingToken) {
this.createElement().mountCardElement();
document.getElementById("pay-now").addEventListener("click", () => {
return this.completePaymentWithoutToken();
});
}
}
}
const publishableKey = document.querySelector(
'meta[name="stripe-publishable-key"]'
).content;
const usingToken = document.querySelector('meta[name="using-token"]').content;
new ProcessStripePayment(publishableKey, usingToken).handle();

View File

@ -44,7 +44,7 @@
{{ ctrans('texts.credit_card') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<div id="card-element" class="form-control"></div>
<div id="card-element"></div>
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">

View File

@ -0,0 +1,106 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.pay_now'))
@push('head')
<meta name="stripe-publishable-key" content="{{ $gateway->getPublishableKey() }}">
<meta name="using-token" content="{{ boolval($token) }}">
@endpush
@section('header')
Insert breadcrumbs..
@endsection
@section('body')
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
@csrf
<input type="hidden" name="gateway_response">
<input type="hidden" name="store_card">
@foreach($invoices as $invoice)
<input type="hidden" name="hashed_ids[]" value="{{ $invoice->hashed_id }}">
@endforeach
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
</form>
<div class="container mx-auto">
<div class="grid grid-cols-6 gap-4">
<div class="col-span-6 md:col-start-2 md:col-span-4">
<div class="alert alert-failure mb-4" hidden id="errors"></div>
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.pay_now') }}
</h3>
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500" translate>
{{ ctrans('texts.complete_your_payment') }}
</p>
</div>
<div>
<dl>
@if($token)
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
{{ ctrans('texts.credit_card') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ strtoupper($token->meta->brand) }} - **** {{ $token->meta->last4 }}
</dd>
</div>
<div class="bg-white px-4 py-5 flex justify-end">
<button
type="button"
data-secret="{{ $intent->client_secret }}"
data-token="{{ $token->token }}"
id="pay-now-with-token"
class="button button-primary">
{{ ctrans('texts.pay_now') }}
</button>
</div>
@else
<div
class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
{{ ctrans('texts.name') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input" id="cardholder-name" type="text"
placeholder="{{ ctrans('texts.name') }}">
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.credit_card') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<div id="card-element"></div>
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.token_billing_checkbox') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input type="checkbox" class="form-check" name="token-billing-checkbox"/>
</dd>
</div>
<div class="bg-white px-4 py-5 flex justify-end">
<button
type="button"
id="pay-now"
data-secret="{{ $intent->client_secret }}"
class="button button-primary">
{{ ctrans('texts.pay_now') }}
</button>
</div>
@endif
</dl>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('footer')
<script src="https://js.stripe.com/v3/"></script>
<script src="{{ asset('js/clients/payments/process.js') }}"></script>
@endpush

4
webpack.mix.js vendored
View File

@ -23,6 +23,10 @@ mix.js("resources/js/app.js", "public/js")
.js(
"resources/js/clients/quotes/approve.js",
"public/js/clients/quotes/approve.js"
)
.js(
"resources/js/clients/payments/process.js",
"public/js/clients/payments/process.js"
);
mix.sass("resources/sass/app.scss", "public/css")