mirror of
				https://github.com/invoiceninja/invoiceninja.git
				synced 2025-11-03 22:27:31 -05: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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user