Braintree: New payment flow (#77)

* pass livewirePaymentView & processPaymentView thru base driver

* add paymentData to the interface

* braintree
This commit is contained in:
Benjamin Beganović 2024-08-09 01:08:54 +02:00 committed by GitHub
parent 6351e209d2
commit 59666dc5db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 315 additions and 37 deletions

View File

@ -20,11 +20,12 @@ use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\BraintreePaymentDriver;
use App\PaymentDrivers\Common\LivewireMethodInterface;
use App\PaymentDrivers\Common\MethodInterface;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
class ACH implements MethodInterface
class ACH implements MethodInterface, LivewireMethodInterface
{
use MakesHash;
@ -97,10 +98,7 @@ class ACH implements MethodInterface
public function paymentView(array $data)
{
$data['gateway'] = $this->braintree;
$data['currency'] = $this->braintree->client->getCurrencyCode();
$data['payment_method_id'] = GatewayType::BANK_TRANSFER;
$data['amount'] = $this->braintree->payment_hash->data->amount_with_fee;
$data = $this->paymentData($data);
return render('gateways.braintree.ach.pay', $data);
}
@ -181,4 +179,24 @@ class ACH implements MethodInterface
throw new PaymentFailed($response->transaction->additionalProcessorResponse, $response->transaction->processorResponseCode);
}
/**
* @inheritDoc
*/
public function livewirePaymentView(array $data): string
{
return 'gateways.braintree.ach.pay_livewire';
}
/**
* @inheritDoc
*/
public function paymentData(array $data): array
{
$data['gateway'] = $this->braintree;
$data['currency'] = $this->braintree->client->getCurrencyCode();
$data['payment_method_id'] = GatewayType::BANK_TRANSFER;
$data['amount'] = $this->braintree->payment_hash->data->amount_with_fee;
return $data;
}
}

View File

@ -21,8 +21,9 @@ use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\BraintreePaymentDriver;
use App\PaymentDrivers\Common\LivewireMethodInterface;
class CreditCard
class CreditCard implements LivewireMethodInterface
{
/**
* @var BraintreePaymentDriver
@ -76,17 +77,7 @@ class CreditCard
public function paymentView(array $data)
{
$data['gateway'] = $this->braintree;
$data['client_token'] = $this->braintree->gateway->clientToken()->generate();
$data['threeds'] = $this->threeDParameters($data);
$data['threeds_enable'] = $this->braintree->company_gateway->getConfigField('threeds') ? "true" : "false";
if ($this->braintree->company_gateway->getConfigField('merchantAccountId')) {
/** https://developer.paypal.com/braintree/docs/reference/request/client-token/generate#merchant_account_id */
$data['client_token'] = $this->braintree->gateway->clientToken()->generate([
'merchantAccountId' => $this->braintree->company_gateway->getConfigField('merchantAccountId'),
]);
}
$data = $this->paymentData($data);
return render('gateways.braintree.credit_card.pay', $data);
}
@ -278,4 +269,32 @@ class CreditCard
return $this->braintree->processInternallyFailedPayment($this->braintree, $e);
}
}
/**
* @inheritDoc
*/
public function livewirePaymentView(array $data): string
{
return 'gateways.braintree.credit_card.pay_livewire';
}
/**
* @inheritDoc
*/
public function paymentData(array $data): array
{
$data['gateway'] = $this->braintree;
$data['client_token'] = $this->braintree->gateway->clientToken()->generate();
$data['threeds'] = $this->threeDParameters($data);
$data['threeds_enable'] = $this->braintree->company_gateway->getConfigField('threeds') ? "true" : "false";
if ($this->braintree->company_gateway->getConfigField('merchantAccountId')) {
/** https://developer.paypal.com/braintree/docs/reference/request/client-token/generate#merchant_account_id */
$data['client_token'] = $this->braintree->gateway->clientToken()->generate([
'merchantAccountId' => $this->braintree->company_gateway->getConfigField('merchantAccountId'),
]);
}
return $data;
}
}

View File

@ -10,8 +10,9 @@ use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\BraintreePaymentDriver;
use App\PaymentDrivers\Common\LivewireMethodInterface;
class PayPal
class PayPal implements LivewireMethodInterface
{
/**
* @var BraintreePaymentDriver
@ -45,8 +46,7 @@ class PayPal
*/
public function paymentView(array $data)
{
$data['gateway'] = $this->braintree;
$data['client_token'] = $this->braintree->gateway->clientToken()->generate();
$data = $this->paymentData($data);
return render('gateways.braintree.paypal.pay', $data);
}
@ -188,4 +188,23 @@ class PayPal
return $this->braintree->processInternallyFailedPayment($this->braintree, $e);
}
}
/**
* @inheritDoc
*/
public function livewirePaymentView(array $data): string
{
return 'gateways.braintree.paypal.pay_livewire';
}
/**
* @inheritDoc
*/
public function paymentData(array $data): array
{
$data['gateway'] = $this->braintree;
$data['client_token'] = $this->braintree->gateway->clientToken()->generate();
return $data;
}
}

View File

@ -1,9 +0,0 @@
/**
* 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://www.elastic.co/licensing/elastic-license
*/class i{initBraintreeDataCollector(){window.braintree.client.create({authorization:document.querySelector("meta[name=client-token]").content},function(e,n){window.braintree.dataCollector.create({client:n,paypal:!0},function(t,a){t||(document.querySelector("input[name=client-data]").value=a.deviceData)})})}mountBraintreePaymentWidget(){window.braintree.dropin.create({authorization:document.querySelector("meta[name=client-token]").content,container:"#dropin-container",threeDSecure:document.querySelector("input[name=threeds_enable]").value.toLowerCase()==="true"},this.handleCallback)}handleCallback(e,n){if(e){console.error(e);return}let t=document.getElementById("pay-now");params=JSON.parse(document.querySelector("input[name=threeds]").value),t.addEventListener("click",()=>{n.requestPaymentMethod({threeDSecure:{challengeRequested:!0,amount:params.amount,email:params.email,billingAddress:{givenName:params.billingAddress.givenName,surname:params.billingAddress.surname,phoneNumber:params.billingAddress.phoneNumber,streetAddress:params.billingAddress.streetAddress,extendedAddress:params.billingAddress.extendedAddress,locality:params.billingAddress.locality,region:params.billingAddress.region,postalCode:params.billingAddress.postalCode,countryCodeAlpha2:params.billingAddress.countryCodeAlpha2}}},function(a,r){if(a){console.log(a),dropin.clearSelectedPaymentMethod(),alert("There was a problem verifying this card, please contact your merchant");return}if(document.querySelector("input[name=threeds_enable]").value==="true"&&!r.liabilityShifted){console.log("Liability did not shift",r),alert("There was a problem verifying this card, please contact your merchant");return}t.disabled=!0,t.querySelector("svg").classList.remove("hidden"),t.querySelector("span").classList.add("hidden"),document.querySelector("input[name=gateway_response]").value=JSON.stringify(r);let d=document.querySelector('input[name="token-billing-checkbox"]:checked');d&&(document.querySelector('input[name="store_card"]').value=d.value),document.getElementById("server-response").submit()})})}handle(){this.initBraintreeDataCollector(),this.mountBraintreePaymentWidget(),Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach(n=>n.addEventListener("click",t=>{document.getElementById("dropin-container").classList.add("hidden"),document.getElementById("save-card--container").style.display="none",document.querySelector("input[name=token]").value=t.target.dataset.token,document.getElementById("pay-now-with-token").classList.remove("hidden"),document.getElementById("pay-now").classList.add("hidden")})),document.getElementById("toggle-payment-with-credit-card").addEventListener("click",n=>{document.getElementById("dropin-container").classList.remove("hidden"),document.getElementById("save-card--container").style.display="grid",document.querySelector("input[name=token]").value="",document.getElementById("pay-now-with-token").classList.add("hidden"),document.getElementById("pay-now").classList.remove("hidden")});let e=document.getElementById("pay-now-with-token");e.addEventListener("click",n=>{e.disabled=!0,e.querySelector("svg").classList.remove("hidden"),e.querySelector("span").classList.add("hidden"),document.getElementById("server-response").submit()})}}new i().handle();

View File

@ -0,0 +1,9 @@
import{i as e,w as n}from"./wait-8f4ae121.js";/**
* 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://www.elastic.co/licensing/elastic-license
*/function t(){console.log("boot",document.querySelector("meta[name=client-token]"))}e()?t():n("#braintree-credit-card-payment","meta[name=client-token]").then(()=>t());

View File

@ -1,4 +1,4 @@
/**
import{i as l,w as s}from"./wait-8f4ae121.js";/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
@ -6,4 +6,4 @@
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/class a{initBraintreeDataCollector(){window.braintree.client.create({authorization:document.querySelector("meta[name=client-token]").content},function(e,t){window.braintree.dataCollector.create({client:t,paypal:!0},function(n,o){n||(document.querySelector("input[name=client-data]").value=o.deviceData)})})}static getPaymentDetails(){return{flow:"vault"}}static handleErrorMessage(e){let t=document.getElementById("errors");t.innerText=e,t.hidden=!1}handlePaymentWithToken(){Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach(t=>t.addEventListener("click",n=>{document.getElementById("paypal-button").classList.add("hidden"),document.getElementById("save-card--container").style.display="none",document.querySelector("input[name=token]").value=n.target.dataset.token,document.getElementById("pay-now-with-token").classList.remove("hidden"),document.getElementById("pay-now").classList.add("hidden")}));let e=document.getElementById("pay-now-with-token");e.addEventListener("click",t=>{e.disabled=!0,e.querySelector("svg").classList.remove("hidden"),e.querySelector("span").classList.add("hidden"),document.getElementById("server-response").submit()})}handle(){this.initBraintreeDataCollector(),this.handlePaymentWithToken(),braintree.client.create({authorization:document.querySelector("meta[name=client-token]").content}).then(function(e){return braintree.paypalCheckout.create({client:e})}).then(function(e){return e.loadPayPalSDK({vault:!0}).then(function(t){return paypal.Buttons({fundingSource:paypal.FUNDING.PAYPAL,createBillingAgreement:function(){return t.createPayment(a.getPaymentDetails())},onApprove:function(n,o){return t.tokenizePayment(n).then(function(i){let r=document.querySelector('input[name="token-billing-checkbox"]:checked');r&&(document.querySelector('input[name="store_card"]').value=r.value),document.querySelector("input[name=gateway_response]").value=JSON.stringify(i),document.getElementById("server-response").submit()})},onCancel:function(n){},onError:function(n){console.log(n.message),a.handleErrorMessage(n.message)}}).render("#paypal-button")})}).catch(function(e){console.log(e.message),a.handleErrorMessage(e.message)})}}new a().handle();
*/class a{initBraintreeDataCollector(){window.braintree.client.create({authorization:document.querySelector("meta[name=client-token]").content},function(e,t){window.braintree.dataCollector.create({client:t,paypal:!0},function(n,o){n||(document.querySelector("input[name=client-data]").value=o.deviceData)})})}static getPaymentDetails(){return{flow:"vault"}}static handleErrorMessage(e){let t=document.getElementById("errors");t.innerText=e,t.hidden=!1}handlePaymentWithToken(){Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach(t=>t.addEventListener("click",n=>{document.getElementById("paypal-button").classList.add("hidden"),document.getElementById("save-card--container").style.display="none",document.querySelector("input[name=token]").value=n.target.dataset.token,document.getElementById("pay-now-with-token").classList.remove("hidden"),document.getElementById("pay-now").classList.add("hidden")}));let e=document.getElementById("pay-now-with-token");e.addEventListener("click",t=>{e.disabled=!0,e.querySelector("svg").classList.remove("hidden"),e.querySelector("span").classList.add("hidden"),document.getElementById("server-response").submit()})}handle(){this.initBraintreeDataCollector(),this.handlePaymentWithToken(),braintree.client.create({authorization:document.querySelector("meta[name=client-token]").content}).then(function(e){return braintree.paypalCheckout.create({client:e})}).then(function(e){return e.loadPayPalSDK({vault:!0}).then(function(t){return paypal.Buttons({fundingSource:paypal.FUNDING.PAYPAL,createBillingAgreement:function(){return t.createPayment(a.getPaymentDetails())},onApprove:function(n,o){return t.tokenizePayment(n).then(function(c){let r=document.querySelector('input[name="token-billing-checkbox"]:checked');r&&(document.querySelector('input[name="store_card"]').value=r.value),document.querySelector("input[name=gateway_response]").value=JSON.stringify(c),document.getElementById("server-response").submit()})},onCancel:function(n){},onError:function(n){console.log(n.message),a.handleErrorMessage(n.message)}}).render("#paypal-button")})}).catch(function(e){console.log(e.message),a.handleErrorMessage(e.message)})}}function i(){new a().handle()}l()?i():s("#braintree-paypal-payment").then(()=>i());

View File

@ -75,12 +75,18 @@
"src": "resources/js/clients/payments/authorize-credit-card-payment.js"
},
"resources/js/clients/payments/braintree-credit-card.js": {
"file": "assets/braintree-credit-card-1c3f3108.js",
"file": "assets/braintree-credit-card-2ba935f9.js",
"imports": [
"_wait-8f4ae121.js"
],
"isEntry": true,
"src": "resources/js/clients/payments/braintree-credit-card.js"
},
"resources/js/clients/payments/braintree-paypal.js": {
"file": "assets/braintree-paypal-45391805.js",
"file": "assets/braintree-paypal-16e2f577.js",
"imports": [
"_wait-8f4ae121.js"
],
"isEntry": true,
"src": "resources/js/clients/payments/braintree-paypal.js"
},

View File

@ -8,6 +8,7 @@
* @license https://www.elastic.co/licensing/elastic-license
*/
import { wait, instant } from '../wait';
class BraintreeCreditCard {
initBraintreeDataCollector() {
@ -43,8 +44,7 @@ class BraintreeCreditCard {
}
let payNow = document.getElementById('pay-now');
params = JSON.parse(document.querySelector('input[name=threeds]').value);
let params = JSON.parse(document.querySelector('input[name=threeds]').value);
payNow.addEventListener('click', () => {
dropinInstance.requestPaymentMethod({
@ -138,4 +138,8 @@ class BraintreeCreditCard {
}
}
new BraintreeCreditCard().handle();
function boot() {
new BraintreeCreditCard().handle();
}
instant() ? boot() : wait('#braintree-credit-card-payment', 'meta[name=client-token]').then(() => boot());

View File

@ -8,6 +8,8 @@
* @license https://www.elastic.co/licensing/elastic-license
*/
import { wait, instant } from '../wait';
class BraintreePayPal {
initBraintreeDataCollector() {
window.braintree.client.create({
@ -119,4 +121,8 @@ class BraintreePayPal {
}
}
new BraintreePayPal().handle();
function boot() {
new BraintreePayPal().handle();
}
instant() ? boot() : wait('#braintree-paypal-payment').then(() => boot());

View File

@ -0,0 +1,58 @@
<div class="rounded-lg border bg-card text-card-foreground shadow-sm overflow-hidden py-5 bg-white sm:gap-4"
id="braintree-ach-payment">
@if(count($tokens) > 0)
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@include('portal.ninja2020.gateways.includes.payment_details')
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
@csrf
<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="source" value="">
<input type="hidden" name="amount" value="{{ $amount }}">
<input type="hidden" name="currency" value="{{ $currency }}">
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
</form>
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
@if(count($tokens) > 0)
@foreach($tokens as $token)
<label class="mr-4">
<input
type="radio"
data-token="{{ $token->hashed_id }}"
name="payment-type"
class="form-radio cursor-pointer toggle-payment-with-token"/>
<span class="ml-1 cursor-pointer">{{ ctrans('texts.bank_transfer') }} (*{{ $token->meta->last4 }})</span>
</label>
@endforeach
@endisset
@endcomponent
@include('portal.ninja2020.gateways.includes.pay_now')
@else
@component('portal.ninja2020.components.general.card-element-single', ['title' => 'ACH', 'show_title' => false])
<span class="block">{{ ctrans('texts.bank_account_not_linked') }}</span>
<a class="hover:underline text-primary mt-2 block"
href="{{ route('client.payment_methods.index') }}">{{ ctrans('texts.add_payment_method') }}</a>
@endcomponent
@endif
</div>
@script
<script defer>
Array
.from(document.getElementsByClassName('toggle-payment-with-token'))
.forEach((element) => element.addEventListener('click', (element) => {
document.querySelector('input[name=source]').value = element.target.dataset.token;
}));
document.getElementById('pay-now').addEventListener('click', function (e) {
e.target.parentElement.disabled = true;
document.getElementById('server-response').submit();
});
</script>
@endscript

View File

@ -2,6 +2,7 @@
@section('gateway_head')
<meta name="client-token" content="{{ $client_token ?? '' }}"/>
<meta name="instant-payment" content="yes" />
<script src='https://js.braintreegateway.com/web/dropin/1.33.4/js/dropin.min.js'></script>
{{-- <script src="https://js.braintreegateway.com/web/3.76.2/js/client.min.js"></script> --}}

View File

@ -0,0 +1,79 @@
<div class="rounded-lg border bg-card text-card-foreground shadow-sm overflow-hidden py-5 bg-white sm:gap-4"
id="braintree-credit-card-payment">
<meta name="client-token" content="{{ $client_token ?? '' }}" />
<style>
[data-braintree-id="toggle"] {
display: none;
}
</style>
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
@csrf
<input type="hidden" name="gateway_response">
<input type="hidden" name="store_card">
<input type="hidden" name="threeds_enable" value="{!! $threeds_enable !!}">
<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="token">
<input type="hidden" name="client-data">
<input type="hidden" name="threeds" value="{{ json_encode($threeds) }}">
</form>
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.payment_type')])
{{ ctrans('texts.credit_card') }}
@endcomponent
@include('portal.ninja2020.gateways.includes.payment_details')
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
@if(count($tokens) > 0)
@foreach($tokens as $token)
<label class="mr-4">
<input
type="radio"
data-token="{{ $token->token }}"
name="payment-type"
class="form-radio cursor-pointer toggle-payment-with-token"/>
<span class="ml-1 cursor-pointer">**** {{ $token->meta?->last4 }}</span>
</label>
@endforeach
@endisset
<label>
<input
type="radio"
id="toggle-payment-with-credit-card"
class="form-radio cursor-pointer"
name="payment-type"
checked/>
<span class="ml-1 cursor-pointer">{{ __('texts.new_card') }}</span>
</label>
@endcomponent
@include('portal.ninja2020.gateways.includes.save_card')
@component('portal.ninja2020.components.general.card-element-single')
<div id="dropin-container"></div>
@endcomponent
@include('portal.ninja2020.gateways.includes.pay_now')
@include('portal.ninja2020.gateways.includes.pay_now', ['id' => 'pay-now-with-token', 'class' => 'hidden'])
<div id="threeds"></div>
</div>
@assets
<script src='https://js.braintreegateway.com/web/dropin/1.33.4/js/dropin.min.js'></script>
{{--
<script src="https://js.braintreegateway.com/web/3.76.2/js/client.min.js"></script> --}}
<script src="https://js.braintreegateway.com/web/3.87.0/js/data-collector.min.js"></script>
<!-- Load the client component. -->
<script src='https://js.braintreegateway.com/web/3.87.0/js/client.min.js'></script>
@vite('resources/js/clients/payments/braintree-credit-card.js')
@endassets

View File

@ -2,6 +2,7 @@
@section('gateway_head')
<meta name="client-token" content="{{ $client_token ?? '' }}"/>
<meta name="instant-payment" content="yes" />
<script src="https://js.braintreegateway.com/web/3.76.2/js/client.min.js"></script>
<script src="https://js.braintreegateway.com/web/3.76.2/js/paypal-checkout.min.js"></script>

View File

@ -0,0 +1,67 @@
<div class="rounded-lg border bg-card text-card-foreground shadow-sm overflow-hidden py-5 bg-white sm:gap-4"
id="braintree-paypal-payment">
<meta name="client-token" content="{{ $client_token ?? '' }}" />
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
@csrf
<input type="hidden" name="gateway_response">
<input type="hidden" name="store_card">
<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="token">
<input type="hidden" name="client-data">
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.payment_type')])
{{ ctrans('texts.paypal') }}
@endcomponent
@include('portal.ninja2020.gateways.includes.payment_details')
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
@if(count($tokens) > 0)
@foreach($tokens as $token)
<label class="mr-4 block mt-2">
<input
type="radio"
data-token="{{ $token->token }}"
name="payment-type"
class="form-radio cursor-pointer toggle-payment-with-token"/>
<span class="ml-1 cursor-pointer">{{ property_exists($token->meta, 'email') ? $token->meta?->email : 'no email provided'}}</span>
</label>
@endforeach
@endisset
<label class="block mt-2">
<input
type="radio"
id="toggle-payment-with-credit-card"
class="form-radio cursor-pointer"
name="payment-type"
checked/>
<span class="ml-1 cursor-pointer">{{ __('texts.new_account') }}</span>
</label>
@endcomponent
@include('portal.ninja2020.gateways.includes.save_card')
@component('portal.ninja2020.components.general.card-element-single')
<div id="paypal-button"></div>
@endcomponent
@include('portal.ninja2020.gateways.includes.pay_now', ['id' => 'pay-now-with-token', 'class' => 'hidden'])
</div>
@assets
<script src="https://js.braintreegateway.com/web/3.76.2/js/client.min.js"></script>
<script src="https://js.braintreegateway.com/web/3.76.2/js/paypal-checkout.min.js"></script>
<script src="https://js.braintreegateway.com/web/3.76.2/js/data-collector.min.js"></script>
@vite('resources/js/clients/payments/braintree-paypal.js')
@endassets