mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-12 14:12:44 +01:00
New payment flow (#64)
* remove context from invoice-pay * withsecurecontext trait * update usages * wip * wip * wip * wip * wip
This commit is contained in:
parent
f25469a288
commit
2a1947ea6e
@ -59,7 +59,6 @@ class Register extends Component
|
||||
|
||||
public function register(array $data)
|
||||
{
|
||||
|
||||
$service = new ClientRegisterService(
|
||||
company: $this->subscription->company,
|
||||
additional: $this->additional_fields,
|
||||
|
@ -10,32 +10,26 @@
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Livewire;
|
||||
namespace App\Livewire\Flow2;
|
||||
|
||||
use App\Utils\Number;
|
||||
use App\Models\Invoice;
|
||||
use Livewire\Component;
|
||||
use App\Utils\HtmlEngine;
|
||||
use App\Libraries\MultiDB;
|
||||
use Livewire\Attributes\On;
|
||||
use App\Livewire\Flow2\Terms;
|
||||
use App\Models\CompanyGateway;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Models\Invoice;
|
||||
use App\Utils\Number;
|
||||
use App\Utils\Traits\MakesDates;
|
||||
use App\Livewire\Flow2\Signature;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Utils\Traits\WithSecureContext;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Reactive;
|
||||
use App\Livewire\Flow2\PaymentMethod;
|
||||
use App\Livewire\Flow2\ProcessPayment;
|
||||
use App\Livewire\Flow2\RequiredFields;
|
||||
use App\Livewire\Flow2\UnderOverPayment;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class InvoicePay extends Component
|
||||
{
|
||||
use MakesDates;
|
||||
use MakesHash;
|
||||
use WithSecureContext;
|
||||
|
||||
private $mappings = [
|
||||
private $mappings = [
|
||||
'client_name' => 'name',
|
||||
'client_website' => 'website',
|
||||
'client_phone' => 'phone',
|
||||
@ -91,7 +85,7 @@ class InvoicePay extends Component
|
||||
public $settings;
|
||||
|
||||
public $terms_accepted = false;
|
||||
|
||||
|
||||
public $signature_accepted = false;
|
||||
|
||||
public $payment_method_accepted = false;
|
||||
@ -100,13 +94,10 @@ class InvoicePay extends Component
|
||||
|
||||
public $required_fields = false;
|
||||
|
||||
public array $context = [];
|
||||
|
||||
#[On('update.context')]
|
||||
public function handleContext(string $property, $value): self
|
||||
{
|
||||
|
||||
data_set($this->context, $property, $value);
|
||||
$this->setContext(property: $property, value: $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -116,7 +107,7 @@ class InvoicePay extends Component
|
||||
{
|
||||
nlog("Terms accepted");
|
||||
// $this->invite = \App\Models\InvoiceInvitation::withTrashed()->find($this->invitation_id)->withoutRelations();
|
||||
$this->terms_accepted =true;
|
||||
$this->terms_accepted = true;
|
||||
}
|
||||
|
||||
#[On('signature-captured')]
|
||||
@ -128,35 +119,33 @@ class InvoicePay extends Component
|
||||
$invite = \App\Models\InvoiceInvitation::withTrashed()->find($this->invitation_id)->withoutRelations();
|
||||
$invite->signature_base64 = $base64;
|
||||
$invite->signature_date = now()->addSeconds($invite->contact->client->timezone_offset());
|
||||
$this->context['signature'] = $base64;
|
||||
$this->setContext('signature', $base64); // $this->context['signature'] = $base64;
|
||||
$invite->save();
|
||||
|
||||
|
||||
}
|
||||
|
||||
#[On('payable-amount')]
|
||||
public function payableAmount($payable_amount)
|
||||
{
|
||||
$this->context['payable_invoices'][0]['amount'] = Number::parseFloat($payable_amount);
|
||||
$this->setContext('payable_invoices.0.amount', Number::parseFloat($payable_amount)); // $this->context['payable_invoices'][0]['amount'] = Number::parseFloat($payable_amount);
|
||||
$this->under_over_payment = false;
|
||||
}
|
||||
|
||||
#[On('payment-method-selected')]
|
||||
public function paymentMethodSelected($company_gateway_id, $gateway_type_id, $amount)
|
||||
{
|
||||
//@TODO only handles single invoice scenario
|
||||
$this->context['company_gateway_id'] = $company_gateway_id;
|
||||
$this->context['gateway_type_id'] = $gateway_type_id;
|
||||
$this->context['amount'] = $amount;
|
||||
$this->context['pre_payment'] = false;
|
||||
$this->context['is_recurring'] = false;
|
||||
$this->context['invitation_id'] = $this->invitation_id;
|
||||
{
|
||||
$this->setContext('company_gateway_id', $company_gateway_id);
|
||||
$this->setContext('gateway_type_id', $gateway_type_id);
|
||||
$this->setContext('amount', $amount);
|
||||
$this->setContext('pre_payment', false);
|
||||
$this->setContext('is_recurring', false);
|
||||
$this->setContext('invitation_id', $this->invitation_id);
|
||||
|
||||
$this->payment_method_accepted = true;
|
||||
|
||||
|
||||
$company_gateway = CompanyGateway::find($company_gateway_id);
|
||||
|
||||
$this->checkRequiredFields($company_gateway);
|
||||
|
||||
}
|
||||
|
||||
#[On('required-fields')]
|
||||
@ -167,33 +156,35 @@ class InvoicePay extends Component
|
||||
|
||||
private function checkRequiredFields(CompanyGateway $company_gateway)
|
||||
{
|
||||
|
||||
$fields = $company_gateway->driver()->getClientRequiredFields();
|
||||
$this->context['fields'] = $fields;
|
||||
|
||||
if($company_gateway->always_show_required_fields){
|
||||
$fields = $company_gateway->driver()->getClientRequiredFields();
|
||||
|
||||
$this->setContext('fields', $fields); // $this->context['fields'] = $fields;
|
||||
|
||||
if ($company_gateway->always_show_required_fields) {
|
||||
return $this->required_fields = true;
|
||||
}
|
||||
|
||||
$contact = $this->context['contact'];
|
||||
|
||||
$contact = $this->getContext()['contact'];
|
||||
|
||||
foreach ($fields as $index => $field) {
|
||||
$_field = $this->mappings[$field['name']];
|
||||
|
||||
if (\Illuminate\Support\Str::startsWith($field['name'], 'client_')) {
|
||||
if (empty($contact->client->{$_field})
|
||||
|| is_null($contact->client->{$_field})
|
||||
if (
|
||||
empty($contact->client->{$_field})
|
||||
|| is_null($contact->client->{$_field})
|
||||
) {
|
||||
|
||||
|
||||
return $this->required_fields = true;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (\Illuminate\Support\Str::startsWith($field['name'], 'contact_')) {
|
||||
if (empty($contact->{$_field}) || is_null($contact->{$_field}) || str_contains($contact->{$_field}, '@example.com')) {
|
||||
return $this->required_fields = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,53 +193,58 @@ class InvoicePay extends Component
|
||||
#[Computed()]
|
||||
public function component(): string
|
||||
{
|
||||
if(!$this->terms_accepted)
|
||||
if (!$this->terms_accepted) {
|
||||
return Terms::class;
|
||||
}
|
||||
|
||||
if(!$this->signature_accepted)
|
||||
if (!$this->signature_accepted) {
|
||||
return Signature::class;
|
||||
}
|
||||
|
||||
if($this->under_over_payment)
|
||||
if ($this->under_over_payment) {
|
||||
return UnderOverPayment::class;
|
||||
}
|
||||
|
||||
if(!$this->payment_method_accepted)
|
||||
if (!$this->payment_method_accepted) {
|
||||
return PaymentMethod::class;
|
||||
}
|
||||
|
||||
if($this->required_fields)
|
||||
if ($this->required_fields) {
|
||||
return RequiredFields::class;
|
||||
|
||||
}
|
||||
|
||||
return ProcessPayment::class;
|
||||
}
|
||||
|
||||
#[Computed()]
|
||||
public function componentUniqueId(): string
|
||||
{
|
||||
return "purchase-".md5(microtime());
|
||||
return "purchase-" . md5(microtime());
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
|
||||
$this->resetContext();
|
||||
|
||||
MultiDB::setDb($this->db);
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$invite = \App\Models\InvoiceInvitation::with('contact.client','company')->withTrashed()->find($this->invitation_id);
|
||||
$invite = \App\Models\InvoiceInvitation::with('contact.client', 'company')->withTrashed()->find($this->invitation_id);
|
||||
$client = $invite->contact->client;
|
||||
$settings = $client->getMergedSettings();
|
||||
$this->context['contact'] = $invite->contact;
|
||||
$this->context['settings'] = $settings;
|
||||
$this->context['db'] = $this->db;
|
||||
|
||||
$this->setContext('contact', $invite->contact); // $this->context['contact'] = $invite->contact;
|
||||
$this->setContext('settings', $settings); // $this->context['settings'] = $settings;
|
||||
$this->setContext('db', $this->db); // $this->context['db'] = $this->db;
|
||||
|
||||
$invoices = Invoice::find($this->transformKeys($this->invoices));
|
||||
$invoices = $invoices->filter(function ($i){
|
||||
|
||||
|
||||
$invoices = $invoices->filter(function ($i) {
|
||||
$i = $i->service()
|
||||
->markSent()
|
||||
->removeUnpaidGatewayFees()
|
||||
->save();
|
||||
|
||||
return $i->isPayable();
|
||||
|
||||
});
|
||||
|
||||
//under-over / payment
|
||||
@ -259,12 +255,12 @@ class InvoicePay extends Component
|
||||
$this->under_over_payment = $settings->client_portal_allow_over_payment || $settings->client_portal_allow_under_payment;
|
||||
$this->required_fields = false;
|
||||
|
||||
$this->context['variables'] = $this->variables;
|
||||
$this->context['invoices'] = $invoices;
|
||||
$this->context['settings'] = $settings;
|
||||
$this->context['invitation'] = $invite;
|
||||
$this->setContext('variables', $this->variables); // $this->context['variables'] = $this->variables;
|
||||
$this->setContext('invoices', $invoices); // $this->context['invoices'] = $invoices;
|
||||
$this->setContext('settings', $settings); // $this->context['settings'] = $settings;
|
||||
$this->setContext('invitation', $invite); // $this->context['invitation'] = $invite;
|
||||
|
||||
$this->context['payable_invoices'] = $invoices->map(function ($i){
|
||||
$payable_invoices = $invoices->map(function ($i) {
|
||||
return [
|
||||
'invoice_id' => $i->hashed_id,
|
||||
'amount' => $i->partial > 0 ? $i->partial : $i->balance,
|
||||
@ -273,13 +269,12 @@ class InvoicePay extends Component
|
||||
'date' => $i->translateDate($i->date, $i->client->date_format(), $i->client->locale())
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
|
||||
$this->setContext('payable_invoices', $payable_invoices);
|
||||
}
|
||||
|
||||
public function render()
|
||||
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
{
|
||||
return render('components.livewire.invoice-pay', [
|
||||
'context' => $this->context
|
||||
]);
|
||||
return render('flow2.invoice-pay');
|
||||
}
|
||||
}
|
||||
}
|
45
app/Livewire/Flow2/InvoiceSummary.php
Normal file
45
app/Livewire/Flow2/InvoiceSummary.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Livewire\Flow2;
|
||||
|
||||
use App\Utils\Traits\WithSecureContext;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class InvoiceSummary extends Component
|
||||
{
|
||||
use WithSecureContext;
|
||||
|
||||
public $invoice;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
//@TODO for a single invoice - show all details, for multi-invoices, only show the summaries
|
||||
$this->invoice = $this->getContext()['invitation']->invoice; // $this->context['invitation']->invoice;
|
||||
}
|
||||
|
||||
#[On(self::CONTEXT_UPDATE)]
|
||||
public function onContextUpdate(): void
|
||||
{
|
||||
// refactor logic for updating the price for eg if it changes with under/over pay
|
||||
|
||||
$this->invoice = $this->getContext()['invitation']->invoice;
|
||||
}
|
||||
|
||||
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
{
|
||||
return render('flow2.invoice-summary', [
|
||||
'invoice' => $this->invoice
|
||||
]);
|
||||
}
|
||||
}
|
@ -12,14 +12,15 @@
|
||||
|
||||
namespace App\Livewire\Flow2;
|
||||
|
||||
use App\Utils\Traits\WithSecureContext;
|
||||
use Livewire\Component;
|
||||
use App\Libraries\MultiDB;
|
||||
|
||||
class PaymentMethod extends Component
|
||||
{
|
||||
public $invoice;
|
||||
use WithSecureContext;
|
||||
|
||||
public $context;
|
||||
public $invoice;
|
||||
|
||||
public $variables;
|
||||
|
||||
@ -36,7 +37,7 @@ class PaymentMethod extends Component
|
||||
<svg class="animate-spin h-10 w-10 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</svg>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
@ -44,12 +45,12 @@ class PaymentMethod extends Component
|
||||
public function mount()
|
||||
{
|
||||
|
||||
$this->variables = $this->context['variables'];
|
||||
$this->amount = array_sum(array_column($this->context['payable_invoices'], 'amount'));
|
||||
$this->variables = $this->getContext()['variables'];
|
||||
$this->amount = array_sum(array_column($this->getContext()['payable_invoices'], 'amount'));
|
||||
|
||||
MultiDB::setDb($this->context['db']);
|
||||
MultiDB::setDb($this->getContext()['db']);
|
||||
|
||||
$this->methods = $this->context['invitation']->contact->client->service()->getPaymentMethods($this->amount);
|
||||
$this->methods = $this->getContext()['invitation']->contact->client->service()->getPaymentMethods($this->amount);
|
||||
|
||||
if(count($this->methods) == 1) {
|
||||
$this->dispatch('singlePaymentMethodFound', company_gateway_id: $this->methods[0]['company_gateway_id'], gateway_type_id: $this->methods[0]['gateway_type_id'], amount: $this->amount);
|
||||
@ -60,9 +61,8 @@ class PaymentMethod extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
//If there is only one payment method, skip display and push straight to the form!!
|
||||
return render('components.livewire.payment_method-flow2', ['methods' => $this->methods]);
|
||||
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
{
|
||||
return render('flow2.payment-method', ['methods' => $this->methods]);
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
namespace App\Livewire\Flow2;
|
||||
|
||||
use App\Exceptions\PaymentFailed;
|
||||
use App\Utils\Traits\WithSecureContext;
|
||||
use Livewire\Component;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\CompanyGateway;
|
||||
@ -22,8 +23,7 @@ use App\Services\ClientPortal\LivewireInstantPayment;
|
||||
|
||||
class ProcessPayment extends Component
|
||||
{
|
||||
|
||||
public $context;
|
||||
use WithSecureContext;
|
||||
|
||||
private string $component_view = '';
|
||||
|
||||
@ -33,17 +33,17 @@ class ProcessPayment extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
|
||||
MultiDB::setDb($this->context['db']);
|
||||
|
||||
$invitation = InvoiceInvitation::find($this->context['invitation_id']);
|
||||
MultiDB::setDb($this->getContext()['db']);
|
||||
|
||||
$invitation = InvoiceInvitation::find($this->getContext()['invitation_id']);
|
||||
|
||||
$data = [
|
||||
'company_gateway_id' => $this->context['company_gateway_id'],
|
||||
'payment_method_id' => $this->context['gateway_type_id'],
|
||||
'payable_invoices' => $this->context['payable_invoices'],
|
||||
'signature' => isset($this->context['signature']) ? $this->context['signature'] : false,
|
||||
'signature_ip' => isset($this->context['signature_ip']) ? $this->context['signature_ip'] : false,
|
||||
'company_gateway_id' => $this->getContext()['company_gateway_id'],
|
||||
'payment_method_id' => $this->getContext()['gateway_type_id'],
|
||||
'payable_invoices' => $this->getContext()['payable_invoices'],
|
||||
'signature' => isset($this->getContext()['signature']) ? $this->getContext()['signature'] : false,
|
||||
'signature_ip' => isset($this->getContext()['signature_ip']) ? $this->getContext()['signature_ip'] : false,
|
||||
'pre_payment' => false,
|
||||
'frequency_id' => false,
|
||||
'remaining_cycles' => false,
|
||||
@ -53,7 +53,7 @@ class ProcessPayment extends Component
|
||||
|
||||
$responder_data = (new LivewireInstantPayment($data))->run();
|
||||
|
||||
$company_gateway = CompanyGateway::find($this->context['company_gateway_id']);
|
||||
$company_gateway = CompanyGateway::find($this->getContext()['company_gateway_id']);
|
||||
|
||||
$this->component_view = '';
|
||||
|
||||
@ -111,17 +111,14 @@ class ProcessPayment extends Component
|
||||
|
||||
}
|
||||
|
||||
public function boot()
|
||||
public function render(): \Illuminate\Contracts\View\Factory|string|\Illuminate\View\View
|
||||
{
|
||||
if ($this->isLoading) {
|
||||
return <<<'HTML'
|
||||
<template></template>
|
||||
HTML;
|
||||
}
|
||||
|
||||
nlog($this->isLoading);
|
||||
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
|
||||
if(!$this->isLoading)
|
||||
return render('gateways.stripe.credit_card.livewire_pay', $this->payment_data_payload);
|
||||
return render('gateways.stripe.credit_card.livewire_pay', $this->payment_data_payload);
|
||||
}
|
||||
}
|
||||
|
@ -12,19 +12,115 @@
|
||||
|
||||
namespace App\Livewire\Flow2;
|
||||
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\CompanyGateway;
|
||||
use App\Services\Client\RFFService;
|
||||
use App\Utils\Traits\WithSecureContext;
|
||||
use Livewire\Component;
|
||||
|
||||
class RequiredFields extends Component
|
||||
{
|
||||
public $context;
|
||||
use WithSecureContext;
|
||||
|
||||
public function mount()
|
||||
public ?CompanyGateway $company_gateway;
|
||||
|
||||
public ?string $client_name;
|
||||
public ?string $contact_first_name;
|
||||
public ?string $contact_last_name;
|
||||
public ?string $contact_email;
|
||||
public ?string $client_phone;
|
||||
public ?string $client_address_line_1;
|
||||
public ?string $client_city;
|
||||
public ?string $client_state;
|
||||
public ?string $client_country_id;
|
||||
public ?string $client_postal_code;
|
||||
public ?string $client_shipping_address_line_1;
|
||||
public ?string $client_shipping_city;
|
||||
public ?string $client_shipping_state;
|
||||
public ?string $client_shipping_postal_code;
|
||||
public ?string $client_shipping_country_id;
|
||||
public ?string $client_custom_value1;
|
||||
public ?string $client_custom_value2;
|
||||
public ?string $client_custom_value3;
|
||||
public ?string $client_custom_value4;
|
||||
|
||||
/** @var array<int, string> */
|
||||
public array $fields = [];
|
||||
|
||||
public bool $is_loading = true;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
|
||||
MultiDB::setDB(
|
||||
$this->getContext()['db'],
|
||||
);
|
||||
|
||||
$this->fields = $this->getContext()['fields'];
|
||||
|
||||
$this->company_gateway = CompanyGateway::withTrashed()
|
||||
->with('company')
|
||||
->find($this->getContext()['company_gateway_id']);
|
||||
|
||||
$contact = auth()->user();
|
||||
|
||||
$this->client_name = $contact->client->name;
|
||||
$this->contact_first_name = $contact->first_name;
|
||||
$this->contact_last_name = $contact->last_name;
|
||||
$this->contact_email = $contact->email;
|
||||
$this->client_phone = $contact->client->phone;
|
||||
$this->client_address_line_1 = $contact->client->address1;
|
||||
$this->client_city = $contact->client->city;
|
||||
$this->client_state = $contact->client->state;
|
||||
$this->client_country_id = $contact->client->country_id;
|
||||
$this->client_postal_code = $contact->client->postal_code;
|
||||
$this->client_shipping_address_line_1 = $contact->client->shipping_address1;
|
||||
$this->client_shipping_city = $contact->client->shipping_city;
|
||||
$this->client_shipping_state = $contact->client->shipping_state;
|
||||
$this->client_shipping_postal_code = $contact->client->shipping_postal_code;
|
||||
$this->client_shipping_country_id = $contact->client->shipping_country_id;
|
||||
$this->client_custom_value1 = $contact->client->custom_value1;
|
||||
$this->client_custom_value2 = $contact->client->custom_value2;
|
||||
$this->client_custom_value3 = $contact->client->custom_value3;
|
||||
$this->client_custom_value4 = $contact->client->custom_value4;
|
||||
|
||||
$rff = new RFFService(
|
||||
fields: $this->getContext()['fields'],
|
||||
database: $this->getContext()['db'],
|
||||
company_gateway_id: $this->company_gateway->id,
|
||||
);
|
||||
|
||||
/** @var \App\Models\ClientContact $contact */
|
||||
$rff->check($contact);
|
||||
|
||||
if ($rff->unfilled_fields === 0) {
|
||||
$this->dispatch('required-fields');
|
||||
}
|
||||
|
||||
if ($rff->unfilled_fields > 0) {
|
||||
$this->is_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
public function handleSubmit(array $data)
|
||||
{
|
||||
return render('components.livewire.required-fields', ['contact' => $this->context['contact'], 'fields' => $this->context['fields']]);
|
||||
$rff = new RFFService(
|
||||
fields: $this->fields,
|
||||
database: $this->getContext()['db'],
|
||||
company_gateway_id: $this->company_gateway->id,
|
||||
);
|
||||
|
||||
$contact = auth()->user();
|
||||
|
||||
/** @var \App\Models\ClientContact $contact */
|
||||
$rff->handleSubmit($data, $contact, function () {
|
||||
$this->dispatch('required-fields');
|
||||
});
|
||||
}
|
||||
|
||||
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
{
|
||||
return render('flow2.required-fields', [
|
||||
'contact' => $this->getContext()['contact'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -12,20 +12,21 @@
|
||||
|
||||
namespace App\Livewire\Flow2;
|
||||
|
||||
use App\Utils\Traits\WithSecureContext;
|
||||
use Livewire\Component;
|
||||
|
||||
class Terms extends Component
|
||||
{
|
||||
public $invoice;
|
||||
use WithSecureContext;
|
||||
|
||||
public $context;
|
||||
public $invoice;
|
||||
|
||||
public $variables;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->invoice = $this->context['invoice'];
|
||||
$this->variables = $this->context['variables'];
|
||||
$this->invoice = $this->getContext()['invoice'];
|
||||
$this->variables = $this->getContext()['variables'];
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
@ -13,12 +13,13 @@
|
||||
namespace App\Livewire\Flow2;
|
||||
|
||||
use App\Utils\Number;
|
||||
use App\Utils\Traits\WithSecureContext;
|
||||
use Livewire\Component;
|
||||
|
||||
class UnderOverPayment extends Component
|
||||
{
|
||||
|
||||
public $context;
|
||||
use WithSecureContext;
|
||||
|
||||
public $payableAmount;
|
||||
|
||||
@ -32,17 +33,17 @@ class UnderOverPayment extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
|
||||
$this->invoice_amount = array_sum(array_column($this->context['payable_invoices'], 'amount'));
|
||||
$this->currency = $this->context['invitation']->contact->client->currency();
|
||||
$this->payableInvoices = $this->context['payable_invoices'];
|
||||
|
||||
$this->invoice_amount = array_sum(array_column($this->getContext()['payable_invoices'], 'amount'));
|
||||
$this->currency = $this->getContext()['invitation']->contact->client->currency();
|
||||
$this->payableInvoices = $this->getContext()['payable_invoices'];
|
||||
}
|
||||
|
||||
public function checkValue(array $payableInvoices)
|
||||
{
|
||||
$this->errors = '';
|
||||
|
||||
$settings = $this->context['settings'];
|
||||
$settings = $this->getContext()['settings'];
|
||||
$input_amount = 0;
|
||||
|
||||
foreach($payableInvoices as $key=>$invoice){
|
||||
@ -66,15 +67,13 @@ class UnderOverPayment extends Component
|
||||
}
|
||||
|
||||
if(!$this->errors){
|
||||
$this->context['payable_invoices'] = $payableInvoices;
|
||||
$this->getContext()['payable_invoices'] = $payableInvoices;
|
||||
$this->dispatch('payable-amount', payable_amount: $input_amount );
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
{
|
||||
return render('components.livewire.under-over-payments',[
|
||||
'settings' => $this->context['settings'],
|
||||
]);
|
||||
return render('flow2.under-over-payments');
|
||||
}
|
||||
}
|
||||
|
@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class InvoiceSummary extends Component
|
||||
{
|
||||
public $context;
|
||||
|
||||
public $invoice;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
//@TODO for a single invoice - show all details, for multi-invoices, only show the summaries
|
||||
$this->invoice = $this->context['invitation']->invoice;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return render('components.livewire.invoice-summary',[
|
||||
'invoice' => $this->invoice
|
||||
]);
|
||||
}
|
||||
}
|
188
app/Services/Client/RFFService.php
Normal file
188
app/Services/Client/RFFService.php
Normal file
@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Services\Client;
|
||||
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\ClientContact;
|
||||
use App\Models\CompanyGateway;
|
||||
use Illuminate\Support\Str;
|
||||
use Validator;
|
||||
|
||||
class RFFService
|
||||
{
|
||||
public array $mappings = [
|
||||
'client_name' => 'name',
|
||||
'client_website' => 'website',
|
||||
'client_phone' => 'phone',
|
||||
|
||||
'client_address_line_1' => 'address1',
|
||||
'client_address_line_2' => 'address2',
|
||||
'client_city' => 'city',
|
||||
'client_state' => 'state',
|
||||
'client_postal_code' => 'postal_code',
|
||||
'client_country_id' => 'country_id',
|
||||
|
||||
'client_shipping_address_line_1' => 'shipping_address1',
|
||||
'client_shipping_address_line_2' => 'shipping_address2',
|
||||
'client_shipping_city' => 'shipping_city',
|
||||
'client_shipping_state' => 'shipping_state',
|
||||
'client_shipping_postal_code' => 'shipping_postal_code',
|
||||
'client_shipping_country_id' => 'shipping_country_id',
|
||||
|
||||
'client_custom_value1' => 'custom_value1',
|
||||
'client_custom_value2' => 'custom_value2',
|
||||
'client_custom_value3' => 'custom_value3',
|
||||
'client_custom_value4' => 'custom_value4',
|
||||
|
||||
'contact_first_name' => 'first_name',
|
||||
'contact_last_name' => 'last_name',
|
||||
'contact_email' => 'email',
|
||||
// 'contact_phone' => 'phone',
|
||||
];
|
||||
|
||||
public int $unfilled_fields = 0;
|
||||
|
||||
public function __construct(
|
||||
public array $fields,
|
||||
public string $database,
|
||||
public string $company_gateway_id,
|
||||
) {
|
||||
}
|
||||
|
||||
public function check(ClientContact $contact): void
|
||||
{
|
||||
$_contact = $contact;
|
||||
|
||||
foreach ($this->fields as $index => $field) {
|
||||
$_field = $this->mappings[$field['name']];
|
||||
|
||||
if (Str::startsWith($field['name'], 'client_')) {
|
||||
if (
|
||||
empty($_contact->client->{$_field})
|
||||
|| is_null($_contact->client->{$_field})
|
||||
) {
|
||||
// $this->show_form = true;
|
||||
$this->unfilled_fields++;
|
||||
} else {
|
||||
$this->fields[$index]['filled'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Str::startsWith($field['name'], 'contact_')) {
|
||||
if (empty($_contact->{$_field}) || is_null($_contact->{$_field}) || str_contains($_contact->{$_field}, '@example.com')) {
|
||||
$this->unfilled_fields++;
|
||||
} else {
|
||||
$this->fields[$index]['filled'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function handleSubmit(array $data, ClientContact $contact, callable $callback): bool
|
||||
{
|
||||
MultiDB::setDb($this->database);
|
||||
|
||||
$rules = [];
|
||||
|
||||
collect($this->fields)->map(function ($field) use (&$rules) {
|
||||
if (!array_key_exists('filled', $field)) {
|
||||
$rules[$field['name']] = array_key_exists('validation_rules', $field)
|
||||
? $field['validation_rules']
|
||||
: 'required';
|
||||
}
|
||||
});
|
||||
|
||||
$validator = Validator::make($data, $rules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
session()->flash('validation_errors', $validator->getMessageBag()->getMessages());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->update($data, $contact)) {
|
||||
$callback();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function update(array $data, ClientContact $_contact): bool
|
||||
{
|
||||
$client = [];
|
||||
$contact = [];
|
||||
|
||||
MultiDB::setDb($this->database);
|
||||
|
||||
foreach ($data as $field => $value) {
|
||||
if (Str::startsWith($field, 'client_')) {
|
||||
$client[$this->mappings[$field]] = $value;
|
||||
}
|
||||
|
||||
if (Str::startsWith($field, 'contact_')) {
|
||||
$contact[$this->mappings[$field]] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$_contact->first_name = $data['contact_first_name'] ?? '';
|
||||
$_contact->last_name = $data['contact_last_name'] ?? '';
|
||||
$_contact->client->name = $data['client_name'] ?? '';
|
||||
$_contact->email = $data['contact_email'] ?? '';
|
||||
$_contact->client->phone = $data['client_phone'] ?? '';
|
||||
$_contact->client->address1 = $data['client_address_line_1'] ?? '';
|
||||
$_contact->client->city = $data['client_city'] ?? '';
|
||||
$_contact->client->state = $data['client_state'] ?? '';
|
||||
$_contact->client->country_id = $data['client_country_id'] ?? '';
|
||||
$_contact->client->postal_code = $data['client_postal_code'] ?? '';
|
||||
$_contact->client->shipping_address1 = $data['client_shipping_address_line_1'] ?? '';
|
||||
$_contact->client->shipping_city = $data['client_shipping_city'] ?? '';
|
||||
$_contact->client->shipping_state = $data['client_shipping_state'] ?? '';
|
||||
$_contact->client->shipping_postal_code = $data['client_shipping_postal_code'] ?? '';
|
||||
$_contact->client->shipping_country_id = $data['client_shipping_country_id'] ?? '';
|
||||
$_contact->client->custom_value1 = $data['client_custom_value1'] ?? '';
|
||||
$_contact->client->custom_value2 = $data['client_custom_value2'] ?? '';
|
||||
$_contact->client->custom_value3 = $data['client_custom_value3'] ?? '';
|
||||
$_contact->client->custom_value4 = $data['client_custom_value4'] ?? '';
|
||||
$_contact->push();
|
||||
|
||||
|
||||
$_contact
|
||||
->fill($contact)
|
||||
->push();
|
||||
|
||||
$_contact->client
|
||||
->fill($client)
|
||||
->push();
|
||||
|
||||
if ($_contact) {
|
||||
/** @var \App\Models\CompanyGateway $cg */
|
||||
$cg = CompanyGateway::find(
|
||||
$this->company_gateway_id,
|
||||
);
|
||||
|
||||
if ($cg && $cg->update_details) {
|
||||
$payment_gateway = $cg->driver($_contact->client)->init();
|
||||
|
||||
if (method_exists($payment_gateway, "updateCustomer")) {
|
||||
$payment_gateway->updateCustomer();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
47
app/Utils/Traits/WithSecureContext.php
Normal file
47
app/Utils/Traits/WithSecureContext.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Utils\Traits;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
trait WithSecureContext
|
||||
{
|
||||
public const CONTEXT_UPDATE = 'secureContext.updated';
|
||||
|
||||
/**
|
||||
* @throws \Psr\Container\ContainerExceptionInterface
|
||||
* @throws \Psr\Container\NotFoundExceptionInterface
|
||||
*/
|
||||
public function getContext(): mixed
|
||||
{
|
||||
return session()->get('secureContext.invoice-pay');
|
||||
}
|
||||
|
||||
public function setContext(string $property, $value): array
|
||||
{
|
||||
$clone = session()->pull('secureContext.invoice-pay', default: []);
|
||||
|
||||
data_set($clone, $property, $value);
|
||||
|
||||
session()->put('secureContext.invoice-pay', $clone);
|
||||
|
||||
$this->dispatch(self::CONTEXT_UPDATE);
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function resetContext(): void
|
||||
{
|
||||
session()->forget('secureContext.invoice-pay');
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
9
public/build/assets/stripe-credit-card-c690d3d4.js
vendored
Normal file
9
public/build/assets/stripe-credit-card-c690d3d4.js
vendored
Normal 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://www.elastic.co/licensing/elastic-license
|
||||
*/class l{constructor(e,t,n,o){this.key=e,this.secret=t,this.onlyAuthorization=n,this.stripeConnect=o}setupStripe(){return this.stripeConnect?this.stripe=Stripe(this.key,{stripeAccount:this.stripeConnect}):this.stripe=Stripe(this.key),this.elements=this.stripe.elements(),this}createElement(){var e;return this.cardElement=this.elements.create("card",{hidePostalCode:((e=document.querySelector("meta[name=stripe-require-postal-code]"))==null?void 0:e.content)==="0",value:{postalCode:document.querySelector("meta[name=client-postal-code]").content},hideIcon:!1}),this}mountCardElement(){return this.cardElement.mount("#card-element"),this}completePaymentUsingToken(){let e=document.querySelector("input[name=token]").value,t=document.getElementById("pay-now");this.payNowButton=t,this.payNowButton.disabled=!0,this.payNowButton.querySelector("svg").classList.remove("hidden"),this.payNowButton.querySelector("span").classList.add("hidden"),this.stripe.handleCardPayment(this.secret,{payment_method:e}).then(n=>n.error?this.handleFailure(n.error.message):this.handleSuccess(n))}completePaymentWithoutToken(){let e=document.getElementById("pay-now");this.payNowButton=e,this.payNowButton.disabled=!0,this.payNowButton.querySelector("svg").classList.remove("hidden"),this.payNowButton.querySelector("span").classList.add("hidden");let t=document.getElementById("cardholder-name");this.stripe.handleCardPayment(this.secret,this.cardElement,{payment_method_data:{billing_details:{name:t.value}}}).then(n=>n.error?this.handleFailure(n.error.message):this.handleSuccess(n))}handleSuccess(e){document.querySelector('input[name="gateway_response"]').value=JSON.stringify(e.paymentIntent);let t=document.querySelector('input[name="token-billing-checkbox"]:checked');t&&(document.querySelector('input[name="store_card"]').value=t.value),document.getElementById("server-response").submit()}handleFailure(e){let t=document.getElementById("errors");t.textContent="",t.textContent=e,t.hidden=!1,this.payNowButton.disabled=!1,this.payNowButton.querySelector("svg").classList.add("hidden"),this.payNowButton.querySelector("span").classList.remove("hidden")}handleAuthorization(){let e=document.getElementById("cardholder-name"),t=document.getElementById("authorize-card");this.payNowButton=t,this.payNowButton.disabled=!0,this.payNowButton.querySelector("svg").classList.remove("hidden"),this.payNowButton.querySelector("span").classList.add("hidden"),this.stripe.handleCardSetup(this.secret,this.cardElement,{payment_method_data:{billing_details:{name:e.value}}}).then(n=>n.error?this.handleFailure(n.error.message):this.handleSuccessfulAuthorization(n))}handleSuccessfulAuthorization(e){document.getElementById("gateway_response").value=JSON.stringify(e.setupIntent),document.getElementById("server_response").submit()}handle(){this.setupStripe(),this.onlyAuthorization?(this.createElement().mountCardElement(),document.getElementById("authorize-card").addEventListener("click",()=>this.handleAuthorization())):(Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach(e=>e.addEventListener("click",t=>{document.getElementById("stripe--payment-container").classList.add("hidden"),document.getElementById("save-card--container").style.display="none",document.querySelector("input[name=token]").value=t.target.dataset.token})),document.getElementById("toggle-payment-with-credit-card").addEventListener("click",e=>{document.getElementById("stripe--payment-container").classList.remove("hidden"),document.getElementById("save-card--container").style.display="grid",document.querySelector("input[name=token]").value=""}),this.createElement().mountCardElement(),document.getElementById("pay-now").addEventListener("click",()=>{try{return document.querySelector("input[name=token]").value?this.completePaymentUsingToken():this.completePaymentWithoutToken()}catch(e){console.log(e.message)}}))}}Livewire.hook("component.init",()=>{var a,i,s,d;console.log("running now");const r=((a=document.querySelector('meta[name="stripe-publishable-key"]'))==null?void 0:a.content)??"",e=((i=document.querySelector('meta[name="stripe-secret"]'))==null?void 0:i.content)??"",t=((s=document.querySelector('meta[name="only-authorization"]'))==null?void 0:s.content)??"",n=((d=document.querySelector('meta[name="stripe-account-id"]'))==null?void 0:d.content)??"";let o=new l(r,e,t,n);o.handle(),document.addEventListener("livewire:init",()=>{Livewire.on("passed-required-fields-check",()=>o.handle())})});
|
@ -240,7 +240,7 @@
|
||||
"src": "resources/js/setup/setup.js"
|
||||
},
|
||||
"resources/sass/app.scss": {
|
||||
"file": "assets/app-8544e4cc.css",
|
||||
"file": "assets/app-608daae2.css",
|
||||
"isEntry": true,
|
||||
"src": "resources/sass/app.scss"
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2">
|
||||
<div class="p-2">
|
||||
@livewire('invoice-summary',['context' => $context])
|
||||
</div>
|
||||
|
||||
<div class="p-2">
|
||||
@livewire($this->component,['context' => $context], key($this->componentUniqueId()))
|
||||
</div>
|
||||
|
||||
</div>
|
@ -1,66 +0,0 @@
|
||||
<div class="flex flex-col space-y-4 p-4" x-data="{ isLoading: @entangle('isLoading') }">
|
||||
|
||||
<div x-show="isLoading" class="flex items-center justify-center min-h-screen">
|
||||
<svg class="animate-spin h-10 w-10 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@foreach($methods as $index => $method)
|
||||
|
||||
<button
|
||||
class="button button-primary bg-primary payment-method flex items-center justify-center relative py-4"
|
||||
@click="$wire.dispatch('payment-method-selected', { company_gateway_id: {{ $method['company_gateway_id'] }}, gateway_type_id: {{ $method['gateway_type_id'] }}, amount: {{ $amount }} })">
|
||||
<svg class="animate-spin h-5 w-5 text-white hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span>{{ $method['label'] }}</span>
|
||||
</button>
|
||||
|
||||
@endforeach
|
||||
|
||||
|
||||
@script
|
||||
<script>
|
||||
|
||||
Livewire.on('loadingCompleted', () => {
|
||||
isLoading = false;
|
||||
});
|
||||
|
||||
Livewire.on('singlePaymentMethodFound', (event) => {
|
||||
$wire.dispatch('payment-method-selected', {company_gateway_id: event.company_gateway_id, gateway_type_id: event.gateway_type_id, amount: event.amount })
|
||||
});
|
||||
|
||||
const buttons = document.querySelectorAll('.payment-method');
|
||||
|
||||
buttons.forEach(button => {
|
||||
button.addEventListener('click', (event) => {
|
||||
// Hide all buttons except the clicked one
|
||||
buttons.forEach(btn => {
|
||||
if (btn !== event.currentTarget) {
|
||||
btn.style.display = 'none';
|
||||
} else {
|
||||
// Disable the clicked button
|
||||
btn.disabled = true;
|
||||
|
||||
// Show the spinner by removing the 'hidden' class
|
||||
const spinner = btn.querySelector('svg');
|
||||
if (spinner) {
|
||||
spinner.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const span = btn.querySelector('span');
|
||||
if (span) {
|
||||
span.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
@endscript
|
||||
</div>
|
@ -1,37 +0,0 @@
|
||||
<div x-data="{ fields: @entangle('fields'), contact: @entangle('contact') }" class="px-4 py-5 bg-white sm:gap-4 sm:px-6">
|
||||
@foreach($fields as $field)
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => $field['label']])
|
||||
@if($field['name'] == 'client_country_id' || $field['name'] == 'client_shipping_country_id')
|
||||
<select id="client_country" class="input w-full form-select bg-white" name="{{ $field['name'] }}" wire:model="{{ $field['name'] }}">
|
||||
<option value="none"></option>
|
||||
|
||||
@foreach($countries as $country)
|
||||
<option value="{{ $country->id }}">
|
||||
{{ $country->iso_3166_2 }} ({{ $country->name }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@else
|
||||
<input class="input w-full" type="{{ $field['type'] ?? 'text' }}" name="{{ $field['name'] }}" wire:model="{{ $field['name'] }}">
|
||||
@endif
|
||||
|
||||
@if(session()->has('validation_errors') && array_key_exists($field['name'], session('validation_errors')))
|
||||
<p class="mt-2 text-gray-900 border-red-300 px-2 py-1 bg-gray-100">{{ session('validation_errors')[$field['name']][0] }}</p>
|
||||
@endif
|
||||
@endcomponent
|
||||
|
||||
@endforeach
|
||||
|
||||
<div class="bg-white px-4 py-5 flex w-full justify-end">
|
||||
<button
|
||||
class="button button-primary bg-primary payment-method flex items-center justify-center relative py-4"
|
||||
@click="$wire.dispatch('required-fields')">
|
||||
<svg class="animate-spin h-5 w-5 text-white hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span>{{ ctrans('texts.next') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
@ -1,45 +0,0 @@
|
||||
<div x-data="{ payableInvoices: @entangle('payableInvoices'), errors: @entangle('errors') }" class="px-4 py-5 bg-white sm:gap-4 sm:px-6">
|
||||
|
||||
<dt class="text-sm font-medium leading-5 text-gray-500 mb-3">
|
||||
{{ ctrans('texts.payment_amount') }}
|
||||
</dt>
|
||||
<dd class="text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2 flex flex-col">
|
||||
|
||||
|
||||
<template x-for="(invoice, index) in payableInvoices" :key="index">
|
||||
|
||||
<div class="flex items-center mb-2">
|
||||
<label>
|
||||
<span x-text="'{{ ctrans('texts.invoice') }} ' + invoice.number" class="mt-2"></span>
|
||||
<span class="pr-2">{{ $currency->code }} ({{ $currency->symbol }})</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input mt-0 mr-4 relative"
|
||||
name="payable_invoices[]"
|
||||
x-model="payableInvoices[index].formatted_amount"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="errors.length > 0">
|
||||
<div x-text="errors" class="alert alert-failure mb-4"></div>
|
||||
</template>
|
||||
|
||||
@if($settings->client_portal_allow_under_payment)
|
||||
<span class="mt-1 text-sm text-gray-800">{{ ctrans('texts.minimum_payment') }}: {{ $settings->client_portal_under_payment_minimum }}</span>
|
||||
@endif
|
||||
</dd>
|
||||
|
||||
<div class="bg-white px-4 py-5 flex w-full justify-end">
|
||||
<button
|
||||
class="button button-primary bg-primary payment-method flex items-center justify-center relative py-4"
|
||||
wire:click="checkValue(payableInvoices)">
|
||||
<svg class="animate-spin h-5 w-5 text-white hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span>{{ ctrans('texts.next') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,9 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2">
|
||||
<div class="p-2">
|
||||
@livewire('flow2.invoice-summary')
|
||||
</div>
|
||||
|
||||
<div class="p-2">
|
||||
@livewire($this->component, [], key($this->componentUniqueId()))
|
||||
</div>
|
||||
</div>
|
@ -1,4 +1,4 @@
|
||||
<div style="w-full">
|
||||
<div class="w-full">
|
||||
<div class="rounded-lg border bg-card bg-white text-card-foreground shadow-sm overflow-hidden" x-chunk="An order details card with order details, shipping information, customer information and payment information.">
|
||||
<div class="space-y-1.5 p-6 flex flex-row items-start bg-muted/50">
|
||||
<div class="grid gap-0.5">
|
||||
@ -14,7 +14,7 @@
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ ctrans('texts.date') }}: {{ $invoice->translateDate($invoice->date, $invoice->client->date_format(), $invoice->client->locale()) }}</p>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="p-6 text-sm">
|
||||
<div class="grid gap-3">
|
||||
@ -64,4 +64,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,63 @@
|
||||
<div class="flex flex-col p-4 rounded-lg border bg-card text-card-foreground shadow-sm overflow-hidden px-4 py-5 bg-white sm:gap-4 sm:px-6"
|
||||
x-data="{ isLoading: @entangle('isLoading') }">
|
||||
|
||||
<svg x-show="isLoading" wire:loading class="animate-spin h-5 w-5 text-primary" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
</svg>
|
||||
|
||||
<p class="font-semibold tracking-tight group flex items-center gap-2 text-lg">{{ ctrans('texts.payment_methods') }}
|
||||
</p>
|
||||
|
||||
<div class="my-3 flex flex-col space-y-3">
|
||||
@foreach($methods as $index => $method)
|
||||
<button wire:loading.remove
|
||||
class="flex px-4 py-3 border rounded-lg lg:-mb-1 hover:shadow-sm transition duration-300"
|
||||
@click="$wire.dispatch('payment-method-selected', { company_gateway_id: {{ $method['company_gateway_id'] }}, gateway_type_id: {{ $method['gateway_type_id'] }}, amount: {{ $amount }} })">
|
||||
<span>{{ $method['label'] }}</span>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@script
|
||||
<script>
|
||||
Livewire.on('loadingCompleted', () => {
|
||||
isLoading = false;
|
||||
});
|
||||
|
||||
Livewire.on('singlePaymentMethodFound', (event) => {
|
||||
$wire.dispatch('payment-method-selected', { company_gateway_id: event.company_gateway_id, gateway_type_id: event.gateway_type_id, amount: event.amount })
|
||||
});
|
||||
|
||||
const buttons = document.querySelectorAll('.payment-method');
|
||||
|
||||
buttons.forEach(button => {
|
||||
button.addEventListener('click', (event) => {
|
||||
// Hide all buttons except the clicked one
|
||||
buttons.forEach(btn => {
|
||||
if (btn !== event.currentTarget) {
|
||||
btn.style.display = 'none';
|
||||
} else {
|
||||
// Disable the clicked button
|
||||
btn.disabled = true;
|
||||
|
||||
// Show the spinner by removing the 'hidden' class
|
||||
const spinner = btn.querySelector('svg');
|
||||
if (spinner) {
|
||||
spinner.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const span = btn.querySelector('span');
|
||||
if (span) {
|
||||
span.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endscript
|
||||
</div>
|
@ -0,0 +1,61 @@
|
||||
<div x-data="{ fields: @entangle('fields'), contact: @entangle('contact') }"
|
||||
class="rounded-lg border bg-card text-card-foreground shadow-sm overflow-hidden px-4 py-5 bg-white sm:gap-4 sm:px-6">
|
||||
|
||||
<p class="font-semibold tracking-tight group flex items-center gap-2 text-lg mb-3">
|
||||
{{ ctrans('texts.required_fields') }}
|
||||
</p>
|
||||
|
||||
@if($is_loading)
|
||||
<svg class="animate-spin h-5 w-5 text-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
</svg>
|
||||
@else
|
||||
<form id="required-client-info-form"
|
||||
x-on:submit.prevent="$wire.handleSubmit(Object.fromEntries(new FormData(document.getElementById('required-client-info-form'))))"
|
||||
class="-ml-4 lg:-ml-5">
|
||||
@foreach($fields as $field)
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => $field['label']])
|
||||
@if($field['name'] == 'client_country_id' || $field['name'] == 'client_shipping_country_id')
|
||||
<select id="client_country" class="input w-full form-select bg-white" name="{{ $field['name'] }}"
|
||||
wire:model="{{ $field['name'] }}">
|
||||
<option value="none"></option>
|
||||
|
||||
@foreach($countries as $country)
|
||||
<option value="{{ $country->id }}">
|
||||
{{ $country->iso_3166_2 }} ({{ $country->name }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@else
|
||||
<input class="input w-full" type="{{ $field['type'] ?? 'text' }}" name="{{ $field['name'] }}"
|
||||
wire:model="{{ $field['name'] }}">
|
||||
@endif
|
||||
|
||||
@if(session()->has('validation_errors') && array_key_exists($field['name'], session('validation_errors')))
|
||||
<p class="mt-2 text-gray-900 border-red-300 px-2 py-1 bg-gray-100">
|
||||
{{ session('validation_errors')[$field['name']][0] }}
|
||||
</p>
|
||||
@endif
|
||||
@endcomponent
|
||||
|
||||
@endforeach
|
||||
|
||||
<div class="bg-white px-4 py-5 flex items-center w-full justify-end space-x-3">
|
||||
<svg wire:loading class="animate-spin h-5 w-5 text-primary" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
</svg>
|
||||
|
||||
<button wire:loading.attr="disabled" class="button button-primary bg-primary">
|
||||
<span>{{ ctrans('texts.next') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
@ -0,0 +1,45 @@
|
||||
<div x-data="{ payableInvoices: @entangle('payableInvoices'), errors: @entangle('errors') }"
|
||||
class="rounded-lg border bg-card text-card-foreground shadow-sm overflow-hidden px-4 py-5 bg-white sm:gap-4 sm:px-6">
|
||||
|
||||
<p class="font-semibold tracking-tight group flex items-center gap-2 text-lg mb-3">
|
||||
{{ ctrans('texts.payment_amount') }}
|
||||
</p>
|
||||
|
||||
<dd class="text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2 flex flex-col">
|
||||
<template x-for="(invoice, index) in payableInvoices" :key="index">
|
||||
|
||||
<div class="flex items-center mb-2">
|
||||
<label>
|
||||
<span x-text="'{{ ctrans('texts.invoice') }} ' + invoice.number" class="mt-2"></span>
|
||||
<span class="pr-2">{{ $currency->code }} ({{ $currency->symbol }})</span>
|
||||
<input type="text" class="input mt-0 mr-4 relative" name="payable_invoices[]"
|
||||
x-model="payableInvoices[index].formatted_amount" />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="errors.length > 0">
|
||||
<div x-text="errors" class="alert alert-failure mb-4"></div>
|
||||
</template>
|
||||
|
||||
@if($settings->client_portal_allow_under_payment)
|
||||
<span class="mt-1 text-sm text-gray-800">{{ ctrans('texts.minimum_payment') }}:
|
||||
{{ $settings->client_portal_under_payment_minimum }}</span>
|
||||
@endif
|
||||
</dd>
|
||||
|
||||
<div class="bg-white px-4 py-5 flex items-center w-full justify-end space-x-3">
|
||||
<svg wire:loading class="animate-spin h-5 w-5 text-primary" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
</svg>
|
||||
|
||||
<button wire:loading.attr="disabled" wire:click="checkValue(payableInvoices)"
|
||||
class="button button-primary bg-primary">
|
||||
<span>{{ ctrans('texts.next') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
@ -1,4 +1,4 @@
|
||||
<div class="bg-white">
|
||||
<div class="rounded-lg border bg-card text-card-foreground shadow-sm overflow-hidden py-5 bg-white sm:gap-4">
|
||||
@if($stripe_account_id)
|
||||
<meta name="stripe-account-id" content="{{ $stripe_account_id }}">
|
||||
<meta name="stripe-publishable-key" content="{{ config('ninja.ninja_stripe_publishable_key') }}">
|
||||
|
@ -17,7 +17,7 @@
|
||||
@endif
|
||||
|
||||
@if($invoice->isPayable())
|
||||
@livewire('invoice-pay', ['invoices' => $invoices, 'invitation_id' => $invitation->id, 'db' => $invoice->company->db, 'variables' => $variables])
|
||||
@livewire('flow2.invoice-pay', ['invoices' => $invoices, 'invitation_id' => $invitation->id, 'db' => $invoice->company->db, 'variables' => $variables])
|
||||
@endif
|
||||
|
||||
@include('portal.ninja2020.components.entity-documents', ['entity' => $invoice])
|
||||
|
Loading…
Reference in New Issue
Block a user