1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-13 06:32:40 +01:00

Merge pull request #6924 from beganovich/browser-pay

Google Pay, Apple Pay & Microsoft Pay using Stripe
This commit is contained in:
David Bomba 2021-11-03 20:29:00 +11:00 committed by GitHub
commit 5e4e65f9a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 431 additions and 4 deletions

View File

@ -71,7 +71,7 @@ class BaseDriver extends AbstractPaymentDriver
public $payment_method;
/* PaymentHash */
public $payment_hash;
public PaymentHash $payment_hash;
/* Array of payment methods */
public static $methods = [];

View File

@ -0,0 +1,215 @@
<?php
/**
* 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
*/
namespace App\PaymentDrivers\Stripe;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\Common\MethodInterface;
use App\PaymentDrivers\StripePaymentDriver;
use App\Utils\Ninja;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Stripe\ApplePayDomain;
use Stripe\Exception\ApiErrorException;
use Stripe\PaymentIntent;
class BrowserPay implements MethodInterface
{
protected StripePaymentDriver $stripe;
public function __construct(StripePaymentDriver $stripe)
{
$this->stripe = $stripe;
$this->stripe->init();
$this->ensureApplePayDomainIsValidated();
}
/**
* Authorization page for browser pay.
*
* @param array $data
* @return RedirectResponse
*/
public function authorizeView(array $data): RedirectResponse
{
return redirect()->route('client.payment_methods.index');
}
/**
* Handle the authorization for browser pay.
*
* @param Request $request
* @return RedirectResponse
*/
public function authorizeResponse(Request $request): RedirectResponse
{
return redirect()->route('client.payment_methods.index');
}
public function paymentView(array $data): View
{
$payment_intent_data = [
'amount' => $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
'currency' => $this->stripe->client->getCurrencyCode(),
'customer' => $this->stripe->findOrCreateCustomer(),
'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')),
];
$data['gateway'] = $this->stripe;
$data['pi_client_secret'] = $this->stripe->createPaymentIntent($payment_intent_data)->client_secret;
$data['payment_request_data'] = [
'country' => $this->stripe->client->country->iso_3166_2,
'currency' => strtolower(
$this->stripe->client->getCurrencyCode()
),
'total' => [
'label' => $payment_intent_data['description'],
'amount' => $payment_intent_data['amount'],
],
'requestPayerName' => true,
'requestPayerEmail' => true
];
return render('gateways.stripe.browser_pay.pay', $data);
}
/**
* Handle payment response for browser pay.
*
* @param PaymentResponseRequest $request
* @return RedirectResponse|App\PaymentDrivers\Stripe\never
*/
public function paymentResponse(PaymentResponseRequest $request)
{
$gateway_response = json_decode($request->gateway_response);
$this->stripe->payment_hash
->withData('gateway_response', $gateway_response)
->withData('payment_intent', PaymentIntent::retrieve($gateway_response->id, $this->stripe->stripe_connect_auth));
if ($gateway_response->status === 'succeeded') {
return $this->processSuccessfulPayment();
}
return $this->processUnsuccessfulPayment();
}
/**
* Handle successful payment for browser pay.
*
* @return RedirectResponse
*/
protected function processSuccessfulPayment()
{
$gateway_response = $this->stripe->payment_hash->data->gateway_response;
$payment_intent = $this->stripe->payment_hash->data->payment_intent;
$this->stripe->logSuccessfulGatewayResponse(['response' => $gateway_response, 'data' => $this->stripe->payment_hash], SystemLog::TYPE_STRIPE);
$payment_method = $this->stripe->getStripePaymentMethod($gateway_response->payment_method);
$data = [
'payment_method' => $gateway_response->payment_method,
'payment_type' => PaymentType::parseCardType(strtolower($payment_method->card->brand)),
'amount' => $this->stripe->convertFromStripeAmount($gateway_response->amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
'transaction_reference' => optional($payment_intent->charges->data[0])->id,
'gateway_type_id' => GatewayType::APPLE_PAY,
];
$this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, ['amount' => $data['amount']]);
$this->stripe->payment_hash->save();
$payment = $this->stripe->createPayment($data, Payment::STATUS_COMPLETED);
SystemLogger::dispatch(
['response' => $gateway_response, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_STRIPE,
$this->stripe->client,
$this->stripe->client->company,
);
return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]);
}
/**
* Handle unsuccessful payment for browser pay.
*
* @return never
*/
protected function processUnsuccessfulPayment()
{
$server_response = $this->stripe->payment_hash->data->gateway_response;
$this->stripe->sendFailureMail($server_response->cancellation_reason);
$message = [
'server_response' => $server_response,
'data' => $this->stripe->payment_hash->data,
];
SystemLogger::dispatch(
$message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_STRIPE,
$this->stripe->client,
$this->stripe->client->company,
);
throw new PaymentFailed('Failed to process the payment.', 500);
}
/**
* Ensure Apple Pay domain is verified.
*
* @return void
* @throws ApiErrorException
*/
protected function ensureApplePayDomainIsValidated()
{
$config = $this->stripe->company_gateway->getConfig();
if (property_exists($config, 'apple_pay_domain_id')) {
return;
}
$domain = config('ninja.app_url');
if (Ninja::isHosted()) {
$domain = isset($this->stripe_driver->company_gateway->company->portal_domain)
? $this->stripe_driver->company_gateway->company->portal_domain
: $this->stripe_driver->company_gateway->company->domain();
}
$response = ApplePayDomain::create([
'domain_name' => $domain,
]);
$config->apple_pay_domain_id = $response->id;
$this->stripe->company_gateway->setConfig($config);
$this->stripe->company_gateway->save();
}
}

View File

@ -40,6 +40,7 @@ use App\PaymentDrivers\Stripe\EPS;
use App\PaymentDrivers\Stripe\Bancontact;
use App\PaymentDrivers\Stripe\BECS;
use App\PaymentDrivers\Stripe\ACSS;
use App\PaymentDrivers\Stripe\BrowserPay;
use App\PaymentDrivers\Stripe\UpdatePaymentMethods;
use App\PaymentDrivers\Stripe\Utilities;
use App\Utils\Traits\MakesHash;
@ -82,7 +83,7 @@ class StripePaymentDriver extends BaseDriver
GatewayType::BANK_TRANSFER => ACH::class,
GatewayType::ALIPAY => Alipay::class,
GatewayType::SOFORT => SOFORT::class,
GatewayType::APPLE_PAY => ApplePay::class,
GatewayType::APPLE_PAY => BrowserPay::class,
GatewayType::SEPA => SEPA::class,
GatewayType::PRZELEWY24 => PRZELEWY24::class,
GatewayType::GIROPAY => GIROPAY::class,
@ -139,7 +140,7 @@ class StripePaymentDriver extends BaseDriver
{
$types = [
// GatewayType::CRYPTO,
GatewayType::CREDIT_CARD
GatewayType::CREDIT_CARD,
];
if ($this->client
@ -218,6 +219,14 @@ class StripePaymentDriver extends BaseDriver
&& in_array($this->client->country->iso_3166_3, ["CAN", "USA"]))
$types[] = GatewayType::ACSS;
if (
$this->client
&& isset($this->client->country)
&& in_array($this->client->country->iso_3166_2, ['AE', 'AT', 'AU', 'BE', 'BG', 'BR', 'CA', 'CH', 'CI', 'CR', 'CY', 'CZ', 'DE', 'DK', 'DO', 'EE', 'ES', 'FI', 'FR', 'GB', 'GI', 'GR', 'GT', 'HK', 'HU', 'ID', 'IE', 'IN', 'IT', 'JP', 'LI', 'LT', 'LU', 'LV', 'MT', 'MX', 'MY', 'NL', 'NO', 'NZ', 'PE', 'PH', 'PL', 'PT', 'RO', 'SE', 'SG', 'SI', 'SK', 'SN', 'TH', 'TT', 'US', 'UY'])
) {
$types[] = GatewayType::APPLE_PAY;
}
return $types;
}

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
.card-js input.card-number{padding-right:48px}.card-js .card-number-wrapper .card-type-icon{height:23px;width:32px;position:absolute;display:block;right:8px;top:7px;background:url(https://cardjs.co.uk/img/cards.png) 0 23px no-repeat;pointer-events:none;opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-ms-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.card-js .card-number-wrapper .show{opacity:1}.card-js .card-number-wrapper .card-type-icon.visa{background-position:0 0}.card-js .card-number-wrapper .card-type-icon.master-card{background-position:-32px 0}.card-js .card-number-wrapper .card-type-icon.american-express{background-position:-64px 0}.card-js .card-number-wrapper .card-type-icon.discover{background-position:-96px 0}.card-js .card-number-wrapper .card-type-icon.diners{background-position:-128px 0}.card-js .card-number-wrapper .card-type-icon.jcb{background-position:-160px 0}.card-js .cvc-container{width:50%;float:right}.card-js .cvc-wrapper{box-sizing:border-box;margin-left:5px}.card-js .cvc-wrapper .cvc{display:block;width:100%}.card-js .expiry-container{width:50%;float:left}.card-js .expiry-wrapper{box-sizing:border-box;margin-right:5px}.card-js .expiry-wrapper .expiry{display:block;width:100%}.card-js .expiry-wrapper .expiry-month{border-top-right-radius:0;border-bottom-right-radius:0;padding-left:30px}.card-js .expiry-wrapper .expiry-year{border-top-left-radius:0;border-bottom-left-radius:0;border-left:0}.card-js .expiry-wrapper .expiry-month,.card-js .expiry-wrapper .expiry-year{display:inline-block}.card-js .expiry-wrapper .expiry{padding-left:38px}.card-js .icon{position:absolute;display:block;width:24px;height:17px;left:8px;top:10px;pointer-events:none}.card-js .icon.right{right:8px;left:auto}.card-js .icon.popup{cursor:pointer;pointer-events:auto}.card-js .icon .svg{fill:#888}.card-js .icon.popup .svg{fill:#aaa!important}.card-js .card-number-wrapper,.card-js .name-wrapper{margin-bottom:15px;width:100%}.card-js .card-number-wrapper,.card-js .cvc-wrapper,.card-js .expiry-wrapper,.card-js .name-wrapper{-webkit-box-shadow:0 1px 0 rgba(255,255,255,.7),inset 0 1px 0 rgba(255,255,255,.7);-moz-box-shadow:0 1px 0 rgba(255,255,255,.7),inset 0 1px 0 rgba(255,255,255,.7);-ms-box-shadow:0 1px 0 rgba(255,255,255,.7),inset 0 1px 0 rgba(255,255,255,.7);-o-box-shadow:0 1px 0 rgba(255,255,255,.7),inset 0 1px 0 rgba(255,255,255,.7);box-shadow:0 1px 0 rgba(255,255,255,.7),inset 0 1px 0 rgba(255,255,255,.7);position:relative}.card-js .card-number-wrapper,.card-js .cvc-container,.card-js .expiry-container,.card-js .name-wrapper{display:inline-block}.card-js::after{content:' ';display:table;clear:both}.card-js input,.card-js select{color:#676767;font-size:15px;font-weight:300;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;height:36px;border:1px solid #d9d9d9;border-radius:4px;box-shadow:none;background-color:#FDFDFD;box-sizing:border-box;padding:0;-webkit-transition:border-color .15s linear,box-shadow .15s linear;-moz-transition:border-color .15s linear,box-shadow .15s linear;-ms-transition:border-color .15s linear,box-shadow .15s linear;-o-transition:border-color .15s linear,box-shadow .15s linear;transition:border-color .15s linear,box-shadow .15s linear}.card-js select{-moz-appearance:none;text-indent:.01px;text-overflow:''}.card-js input[disabled],.card-js select[disabled]{background-color:#eee;color:#555}.card-js select option[hidden]{color:#ABA9A9}.card-js input:focus,.card-js select:focus{background-color:#fff;outline:0;border-color:#66afe9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.card-js input[readonly=readonly]:not([disabled]),.card-js input[readonly]:not([disabled]){background-color:#fff;cursor:pointer}.card-js .has-error input,.card-js .has-error input:focus{border-color:#F64B2F;box-shadow:none}.card-js input.card-number,.card-js input.cvc,.card-js input.name{padding-left:38px;width:100%}.card-js.stripe .icon .svg{fill:#559A28}
.card-js input.card-number{padding-right:48px}.card-js .card-number-wrapper .card-type-icon{height:23px;width:32px;position:absolute;display:block;right:8px;top:7px;background:url(https://cardjs.co.uk/img/cards.png) 0 23px no-repeat;pointer-events:none;opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-ms-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.card-js .card-number-wrapper .show{opacity:1}.card-js .card-number-wrapper .card-type-icon.visa{background-position:0 0}.card-js .card-number-wrapper .card-type-icon.master-card{background-position:-32px 0}.card-js .card-number-wrapper .card-type-icon.american-express{background-position:-64px 0}.card-js .card-number-wrapper .card-type-icon.discover{background-position:-96px 0}.card-js .card-number-wrapper .card-type-icon.diners{background-position:-128px 0}.card-js .card-number-wrapper .card-type-icon.jcb{background-position:-160px 0}.card-js .cvc-container{width:50%;float:right}.card-js .cvc-wrapper{box-sizing:border-box;margin-left:5px}.card-js .cvc-wrapper .cvc{display:block;width:100%}.card-js .expiry-container{width:50%;float:left}.card-js .expiry-wrapper{box-sizing:border-box;margin-right:5px}.card-js .expiry-wrapper .expiry{display:block;width:100%}.card-js .expiry-wrapper .expiry-month{border-top-right-radius:0;border-bottom-right-radius:0;padding-left:30px}.card-js .expiry-wrapper .expiry-year{border-top-left-radius:0;border-bottom-left-radius:0;border-left:0}.card-js .expiry-wrapper .expiry-month,.card-js .expiry-wrapper .expiry-year{display:inline-block}.card-js .expiry-wrapper .expiry{padding-left:38px}.card-js .icon{position:absolute;display:block;width:24px;height:17px;left:8px;top:10px;pointer-events:none}.card-js .icon.right{right:8px;left:auto}.card-js .icon.popup{cursor:pointer;pointer-events:auto}.card-js .icon .svg{fill:#888}.card-js .icon.popup .svg{fill:#aaa!important}.card-js .card-number-wrapper,.card-js .name-wrapper{margin-bottom:15px;width:100%}.card-js .card-number-wrapper,.card-js .cvc-wrapper,.card-js .expiry-wrapper,.card-js .name-wrapper{-webkit-box-shadow:0 1px 0 rgba(255,255,255,.7),inset 0 1px 0 rgba(255,255,255,.7);-moz-box-shadow:0 1px 0 rgba(255,255,255,.7),inset 0 1px 0 rgba(255,255,255,.7);-ms-box-shadow:0 1px 0 rgba(255,255,255,.7),inset 0 1px 0 rgba(255,255,255,.7);-o-box-shadow:0 1px 0 rgba(255,255,255,.7),inset 0 1px 0 rgba(255,255,255,.7);box-shadow:0 1px 0 rgba(255,255,255,.7),inset 0 1px 0 rgba(255,255,255,.7);position:relative}.card-js .card-number-wrapper,.card-js .cvc-container,.card-js .expiry-container,.card-js .name-wrapper{display:inline-block}.card-js::after{content:' ';display:table;clear:both}.card-js input,.card-js select{color:#676767;font-size:15px;font-weight:300;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;height:36px;border:1px solid #d9d9d9;border-radius:4px;box-shadow:none;background-color:#fdfdfd;box-sizing:border-box;padding:0;-webkit-transition:border-color .15s linear,box-shadow .15s linear;-moz-transition:border-color .15s linear,box-shadow .15s linear;-ms-transition:border-color .15s linear,box-shadow .15s linear;-o-transition:border-color .15s linear,box-shadow .15s linear;transition:border-color .15s linear,box-shadow .15s linear}.card-js select{-moz-appearance:none;text-indent:.01px;text-overflow:''}.card-js input[disabled],.card-js select[disabled]{background-color:#eee;color:#555}.card-js select option[hidden]{color:#aba9a9}.card-js input:focus,.card-js select:focus{background-color:#fff;outline:0;border-color:#66afe9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.card-js input[readonly=readonly]:not([disabled]),.card-js input[readonly]:not([disabled]){background-color:#fff;cursor:pointer}.card-js .has-error input,.card-js .has-error input:focus{border-color:#f64b2f;box-shadow:none}.card-js input.card-number,.card-js input.cvc,.card-js input.name{padding-left:38px;width:100%}.card-js.stripe .icon .svg{fill:#559A28}

View File

@ -0,0 +1,2 @@
/*! For license information please see stripe-browserpay.js.LICENSE.txt */
(()=>{function e(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 t(){var e;!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,t),this.clientSecret=null===(e=document.querySelector("meta[name=stripe-pi-client-secret]"))||void 0===e?void 0:e.content}var n,r,o;return n=t,(r=[{key:"init",value:function(){var e,t,n={};return document.querySelector("meta[name=stripe-account-id]")&&(n.apiVersion="2020-08-27",n.stripeAccount=null===(t=document.querySelector("meta[name=stripe-account-id]"))||void 0===t?void 0:t.content),this.stripe=Stripe(null===(e=document.querySelector("meta[name=stripe-publishable-key]"))||void 0===e?void 0:e.content,n),this.elements=this.stripe.elements(),this}},{key:"createPaymentRequest",value:function(){return this.paymentRequest=this.stripe.paymentRequest(JSON.parse(document.querySelector("meta[name=payment-request-data").content)),this}},{key:"createPaymentRequestButton",value:function(){this.paymentRequestButton=this.elements.create("paymentRequestButton",{paymentRequest:this.paymentRequest})}},{key:"handlePaymentRequestEvents",value:function(e,t){document.querySelector("#errors").hidden=!0,this.paymentRequest.on("paymentmethod",(function(n){e.confirmCardPayment(t,{payment_method:n.paymentMethod.id},{handleActions:!1}).then((function(r){r.error?(n.complete("fail"),document.querySelector("#errors").innerText=r.error.message,document.querySelector("#errors").hidden=!1):(n.complete("success"),"requires_action"===r.paymentIntent.status?e.confirmCardPayment(t).then((function(e){e.error?(n.complete("fail"),document.querySelector("#errors").innerText=e.error.message,document.querySelector("#errors").hidden=!1):(document.querySelector('input[name="gateway_response"]').value=JSON.stringify(e.paymentIntent),document.getElementById("server-response").submit())})):(document.querySelector('input[name="gateway_response"]').value=JSON.stringify(r.paymentIntent),document.getElementById("server-response").submit()))}))}))}},{key:"handle",value:function(){var e=this;this.init().createPaymentRequest().createPaymentRequestButton(),this.paymentRequest.canMakePayment().then((function(t){var n;if(t)return e.paymentRequestButton.mount("#payment-request-button");document.querySelector("#errors").innerHTML=JSON.parse(null===(n=document.querySelector("meta[name=no-available-methods]"))||void 0===n?void 0:n.content),document.querySelector("#errors").hidden=!1})),this.handlePaymentRequestEvents(this.stripe,this.clientSecret)}}])&&e(n.prototype,r),o&&e(n,o),t}())).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

@ -36,6 +36,7 @@
"/js/clients/payments/stripe-eps.js": "/js/clients/payments/stripe-eps.js?id=1ed972f879869de66c8a",
"/js/clients/payments/stripe-ideal.js": "/js/clients/payments/stripe-ideal.js?id=73ce56676f9252b0cecf",
"/js/clients/payments/stripe-przelewy24.js": "/js/clients/payments/stripe-przelewy24.js?id=f3a14f78bec8209c30ba",
"/js/clients/payments/stripe-browserpay.js": "/js/clients/payments/stripe-browserpay.js?id=71e49866d66a6d85b88a",
"/css/app.css": "/css/app.css?id=6d7f6103a3a7738d363b",
"/css/card-js.min.css": "/css/card-js.min.css?id=62afeb675235451543ad"
}

View File

@ -0,0 +1,145 @@
/**
* 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 StripeBrowserPay {
constructor() {
this.clientSecret = document.querySelector(
'meta[name=stripe-pi-client-secret]'
)?.content;
}
init() {
let config = {};
if (document.querySelector('meta[name=stripe-account-id]')) {
config.apiVersion = '2020-08-27';
config.stripeAccount = document.querySelector(
'meta[name=stripe-account-id]'
)?.content;
}
this.stripe = Stripe(
document.querySelector('meta[name=stripe-publishable-key]')
?.content,
config
);
this.elements = this.stripe.elements();
return this;
}
createPaymentRequest() {
this.paymentRequest = this.stripe.paymentRequest(
JSON.parse(
document.querySelector('meta[name=payment-request-data').content
)
);
return this;
}
createPaymentRequestButton() {
this.paymentRequestButton = this.elements.create(
'paymentRequestButton',
{
paymentRequest: this.paymentRequest,
}
);
}
handlePaymentRequestEvents(stripe, clientSecret) {
document.querySelector('#errors').hidden = true;
this.paymentRequest.on('paymentmethod', function (ev) {
stripe
.confirmCardPayment(
clientSecret,
{ payment_method: ev.paymentMethod.id },
{ handleActions: false }
)
.then(function (confirmResult) {
if (confirmResult.error) {
ev.complete('fail');
document.querySelector('#errors').innerText =
confirmResult.error.message;
document.querySelector('#errors').hidden = false;
} else {
ev.complete('success');
if (
confirmResult.paymentIntent.status ===
'requires_action'
) {
stripe
.confirmCardPayment(clientSecret)
.then(function (result) {
if (result.error) {
ev.complete('fail');
document.querySelector(
'#errors'
).innerText = result.error.message;
document.querySelector(
'#errors'
).hidden = false;
} else {
document.querySelector(
'input[name="gateway_response"]'
).value = JSON.stringify(
result.paymentIntent
);
document
.getElementById('server-response')
.submit();
}
});
} else {
document.querySelector(
'input[name="gateway_response"]'
).value = JSON.stringify(
confirmResult.paymentIntent
);
document.getElementById('server-response').submit();
}
}
});
});
}
handle() {
this.init().createPaymentRequest().createPaymentRequestButton();
this.paymentRequest.canMakePayment().then((result) => {
if (result) {
return this.paymentRequestButton.mount(
'#payment-request-button'
);
}
document.querySelector('#errors').innerHTML = JSON.parse(
document.querySelector('meta[name=no-available-methods]')
?.content
);
document.querySelector('#errors').hidden = false;
});
this.handlePaymentRequestEvents(this.stripe, this.clientSecret);
}
}
new StripeBrowserPay().handle();

View File

@ -4336,6 +4336,8 @@ $LANG = array(
'acss' => 'Pre-authorized debit payments',
'invalid_amount' => 'Invalid amount. Number/Decimal values only.',
'client_payment_failure_body' => 'Payment for Invoice :invoice for amount :amount failed.',
'browser_pay' => 'Google Pay, Apple Pay, Microsoft Pay',
'no_available_methods' => 'We can\'t find any credit cards on your device. <a href="https://invoiceninja.github.io/docs/payments#apple-pay-google-pay-microsoft-pay" target="_blank" class="underline">Read more about this.</a>'
);
return $LANG;

View File

@ -0,0 +1,39 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.browser_pay'), 'card_title' => ctrans('texts.browser_pay')])
@section('gateway_head')
@if($gateway->company_gateway->getConfigField('account_id'))
<meta name="stripe-account-id" content="{{ $gateway->company_gateway->getConfigField('account_id') }}">
<meta name="stripe-publishable-key" content="{{ config('ninja.ninja_stripe_publishable_key') }}">
@else
<meta name="stripe-publishable-key" content="{{ $gateway->getPublishableKey() }}">
@endif
<meta name="stripe-pi-client-secret" content="{{ $pi_client_secret }}">
<meta name="no-available-methods" content="{{ json_encode(ctrans('texts.no_available_methods')) }}">
<meta name="payment-request-data" content="{{ json_encode($payment_request_data) }}">
@endsection
@section('gateway_content')
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
@csrf
<input type="hidden" name="gateway_response">
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
<input type="hidden" name="store_card">
<input type="hidden" name="token">
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@include('portal.ninja2020.gateways.includes.payment_details')
@component('portal.ninja2020.components.general.card-element-single')
<div id="payment-request-button"></div>
@endcomponent
@endsection
@section('gateway_footer')
<script src="https://js.stripe.com/v3/"></script>
<script src="{{ asset('js/clients/payments/stripe-browserpay.js') }}"></script>
@endsection

4
webpack.mix.js vendored
View File

@ -146,6 +146,10 @@ mix.js("resources/js/app.js", "public/js")
"resources/js/clients/payments/stripe-przelewy24.js",
"public/js/clients/payments/stripe-przelewy24.js"
)
.js(
"resources/js/clients/payments/stripe-browserpay.js",
"public/js/clients/payments/stripe-browserpay.js"
)
mix.copyDirectory('node_modules/card-js/card-js.min.css', 'public/css/card-js.min.css');