diff --git a/app/Http/Controllers/ClientPortal/EmailPreferencesController.php b/app/Http/Controllers/ClientPortal/EmailPreferencesController.php new file mode 100644 index 000000000000..6fd5063756aa --- /dev/null +++ b/app/Http/Controllers/ClientPortal/EmailPreferencesController.php @@ -0,0 +1,61 @@ +firstOrFail(); + + $data['receive_emails'] = $invitation->contact->is_locked ? false : true; + $data['company'] = $invitation->company; + + return $this->render('generic.email_preferences', $data); + } + + public function update(string $entity, string $invitation_key, Request $request): \Illuminate\Http\RedirectResponse + { + $class = "\\App\\Models\\" . ucfirst(Str::camel($entity)) . 'Invitation'; + $invitation = $class::withTrashed()->where('key', $invitation_key)->firstOrFail(); + + $invitation->contact->is_locked = $request->action === 'unsubscribe' ? true : false; + $invitation->contact->push(); + + if ($invitation->contact->is_locked && !Cache::has("unsubscribe_notitfication_suppression:{$invitation_key}")) { + $nmo = new NinjaMailerObject(); + $nmo->mailable = new NinjaMailer((new ClientUnsubscribedObject($invitation->contact, $invitation->contact->company, $invitation->contact->company->owner()->company_users()->first()->portalType() ?? true))->build()); + $nmo->company = $invitation->contact->company; + $nmo->to_user = $invitation->contact->company->owner(); + $nmo->settings = $invitation->contact->company->settings; + + NinjaMailerJob::dispatch($nmo); + + Cache::put("unsubscribe_notitfication_suppression:{$invitation_key}", true, 3600); + } + + return back()->with('message', ctrans('texts.updated_settings')); + } +} + diff --git a/app/Mail/Admin/ClientUnsubscribedObject.php b/app/Mail/Admin/ClientUnsubscribedObject.php new file mode 100644 index 000000000000..eb0190ab161f --- /dev/null +++ b/app/Mail/Admin/ClientUnsubscribedObject.php @@ -0,0 +1,58 @@ +company->getLocale()); + /* Set customized translations _NOW_ */ + $t->replace(Ninja::transformTranslations($this->company->settings)); + + $data = [ + 'title' => ctrans('texts.client_unsubscribed'), + 'content' => ctrans('texts.client_unsubscribed_help', ['client' => $this->contact->present()->name()]), + 'url' => $this->contact->getAdminLink($this->use_react_link), + 'button' => ctrans('texts.view_client'), + 'signature' => $this->company->settings->email_signature, + 'settings' => $this->company->settings, + 'logo' => $this->company->present()->logo(), + 'text_body' => "\n\n".ctrans('texts.client_unsubscribed_help', ['client' => $this->contact->present()->name()])."\n\n", + ]; + + $mail_obj = new \stdClass(); + $mail_obj->subject = ctrans('texts.client_unsubscribed'); + $mail_obj->data = $data; + $mail_obj->markdown = 'email.admin.generic'; + $mail_obj->tag = $this->company->company_key; + $mail_obj->text_view = 'email.template.text'; + + return $mail_obj; + } +} diff --git a/app/Mail/TemplateEmail.php b/app/Mail/TemplateEmail.php index 1530ce9a4f8a..f9f4b1562a21 100644 --- a/app/Mail/TemplateEmail.php +++ b/app/Mail/TemplateEmail.php @@ -18,6 +18,7 @@ use App\Services\PdfMaker\Designs\Utilities\DesignHelpers; use App\Utils\HtmlEngine; use App\Utils\Ninja; use Illuminate\Mail\Mailable; +use Illuminate\Support\Facades\URL; class TemplateEmail extends Mailable { @@ -138,6 +139,7 @@ class TemplateEmail extends Mailable 'whitelabel' => $this->client->user->account->isPaid() ? true : false, 'logo' => $this->company->present()->logo($settings), 'links' => $this->build_email->getAttachmentLinks(), + 'email_preferences' => URL::signedRoute('client.email_preferences', ['entity' => $this->invitation->getEntityString(), 'invitation_key' => $this->invitation->key]), ]); foreach ($this->build_email->getAttachments() as $file) { diff --git a/app/Models/ClientContact.php b/app/Models/ClientContact.php index 0acdfb09cf42..2f5a948feae8 100644 --- a/app/Models/ClientContact.php +++ b/app/Models/ClientContact.php @@ -11,22 +11,23 @@ namespace App\Models; -use App\Jobs\Mail\NinjaMailer; -use App\Jobs\Mail\NinjaMailerJob; -use App\Jobs\Mail\NinjaMailerObject; -use App\Mail\ClientContact\ClientContactResetPasswordObject; -use App\Models\Presenters\ClientContactPresenter; use App\Utils\Ninja; +use Illuminate\Support\Str; +use App\Jobs\Mail\NinjaMailer; use App\Utils\Traits\AppSetup; use App\Utils\Traits\MakesHash; -use Illuminate\Contracts\Translation\HasLocalePreference; -use Illuminate\Database\Eloquent\Factories\HasFactory; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\SoftDeletes; -use Illuminate\Foundation\Auth\User as Authenticatable; -use Illuminate\Notifications\Notifiable; +use App\Jobs\Mail\NinjaMailerJob; +use App\Jobs\Mail\NinjaMailerObject; use Illuminate\Support\Facades\Cache; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Notifications\Notifiable; use Laracasts\Presenter\PresentableTrait; +use Illuminate\Database\Eloquent\SoftDeletes; +use App\Models\Presenters\ClientContactPresenter; +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Foundation\Auth\User as Authenticatable; +use Illuminate\Contracts\Translation\HasLocalePreference; +use App\Mail\ClientContact\ClientContactResetPasswordObject; /** * Class ClientContact @@ -339,4 +340,16 @@ class ClientContact extends Authenticatable implements HasLocalePreference return ''; } } + + public function getAdminLink($use_react_link = false): string + { + return $use_react_link ? $this->getReactLink() : config('ninja.app_url'); + } + + private function getReactLink(): string + { + return config('ninja.react_url')."/#/clients/{$this->client->hashed_id}"; + } + + } diff --git a/app/Models/CreditInvitation.php b/app/Models/CreditInvitation.php index 545585bcc5e3..83656270280f 100644 --- a/app/Models/CreditInvitation.php +++ b/app/Models/CreditInvitation.php @@ -97,6 +97,11 @@ class CreditInvitation extends BaseModel return self::class; } + public function getEntityString(): string + { + return 'credit'; + } + public function entityType() { return Credit::class; diff --git a/app/Models/InvoiceInvitation.php b/app/Models/InvoiceInvitation.php index 2f5c6c08a1df..58c191fc4a41 100644 --- a/app/Models/InvoiceInvitation.php +++ b/app/Models/InvoiceInvitation.php @@ -97,6 +97,11 @@ class InvoiceInvitation extends BaseModel return self::class; } + public function getEntityString(): string + { + return 'invoice'; + } + public function entityType() { return Invoice::class; diff --git a/app/Models/PurchaseOrderInvitation.php b/app/Models/PurchaseOrderInvitation.php index a1702ea6b926..7584256f72c9 100644 --- a/app/Models/PurchaseOrderInvitation.php +++ b/app/Models/PurchaseOrderInvitation.php @@ -97,6 +97,11 @@ class PurchaseOrderInvitation extends BaseModel return self::class; } + public function getEntityString(): string + { + return 'purchase_order'; + } + public function entityType() { return PurchaseOrder::class; diff --git a/app/Models/QuoteInvitation.php b/app/Models/QuoteInvitation.php index 65aef2e2556a..e1c3c726973c 100644 --- a/app/Models/QuoteInvitation.php +++ b/app/Models/QuoteInvitation.php @@ -78,6 +78,11 @@ class QuoteInvitation extends BaseModel return self::class; } + public function getEntityString(): string + { + return 'quote'; + } + public function entityType() { return Quote::class; diff --git a/app/Models/RecurringInvoiceInvitation.php b/app/Models/RecurringInvoiceInvitation.php index 34928a155c1d..a5fe7868db4e 100644 --- a/app/Models/RecurringInvoiceInvitation.php +++ b/app/Models/RecurringInvoiceInvitation.php @@ -91,6 +91,12 @@ class RecurringInvoiceInvitation extends BaseModel return self::class; } + + public function getEntityString(): string + { + return 'recurring_invoice'; + } + public function entityType() { return RecurringInvoice::class; diff --git a/app/Services/Email/EmailMailable.php b/app/Services/Email/EmailMailable.php index 2599d7d30161..ada43bab730d 100644 --- a/app/Services/Email/EmailMailable.php +++ b/app/Services/Email/EmailMailable.php @@ -77,6 +77,9 @@ class EmailMailable extends Mailable 'company' => $this->email_object->company, 'greeting' => '', 'links' => array_merge($this->email_object->links, $links->toArray()), + 'email_preferences' => $this->email_object->invitation + ? URL::signedRoute('client.email_preferences', ['entity' => $this->email_object->invitation->getEntityString(), 'invitation_key' => $this->email_object->invitation->key]) + : false, ] ); } diff --git a/lang/en/texts.php b/lang/en/texts.php index b7df2cdddbbd..ceb0e221993e 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5213,6 +5213,12 @@ $lang = array( 'nordigen_requisition_body' => 'Access to bank account feeds has expired as set in End User Agreement.

Please log into Invoice Ninja and re-authenticate with your banks to continue receiving transactions.', 'participant' => 'Participant', 'participant_name' => 'Participant name', + 'client_unsubscribed' => 'Client unsubscribed from emails.', + 'client_unsubscribed_help' => 'Client :client has unsubscribed from your e-mails. The client needs to consent to receive future emails from you.', + 'resubscribe' => 'Resubscribe', + 'subscribe' => 'Subscribe', + 'subscribe_help' => 'You are currently subscribed and will continue to receive email communications.', + 'unsubscribe_help' => 'You are currently not subscribed, and therefore, will not receive emails at this time.', ); return $lang; diff --git a/resources/views/email/template/client.blade.php b/resources/views/email/template/client.blade.php index 4d9462b2b370..2986b35345fb 100644 --- a/resources/views/email/template/client.blade.php +++ b/resources/views/email/template/client.blade.php @@ -163,7 +163,7 @@ {{ $slot ?? '' }} {!! $body ?? '' !!} - +
@@ -232,6 +232,21 @@
+ + @isset($email_preferences) + + +
+ +

+ {{ ctrans('texts.unsubscribe') }} +

+
+
+ + + @endisset diff --git a/resources/views/email/template/dark.blade.php b/resources/views/email/template/dark.blade.php index c96087dd12f5..faaf2b30acce 100644 --- a/resources/views/email/template/dark.blade.php +++ b/resources/views/email/template/dark.blade.php @@ -14,4 +14,12 @@

@endif +@isset($email_preferences) +

+ + {{ ctrans('texts.email_preferences') }} + +

+@endif + @endcomponent diff --git a/resources/views/email/template/light.blade.php b/resources/views/email/template/light.blade.php index 43b495cf4452..9a82f9c9e21a 100644 --- a/resources/views/email/template/light.blade.php +++ b/resources/views/email/template/light.blade.php @@ -14,4 +14,12 @@

@endif +@isset($email_preferences) +

+ + {{ ctrans('texts.email_preferences') }} + +

+@endif + @endcomponent diff --git a/resources/views/email/template/plain.blade.php b/resources/views/email/template/plain.blade.php index 270144bf7931..1ae5fe70b167 100644 --- a/resources/views/email/template/plain.blade.php +++ b/resources/views/email/template/plain.blade.php @@ -44,6 +44,14 @@

@endif @endisset + +@if(isset($email_preferences)) +

+ {{ ctrans('texts.email_preferences') }} +

+@endif + + @if(isset($unsubscribe_link))

{{ ctrans('texts.unsubscribe') }}

@endif \ No newline at end of file diff --git a/resources/views/portal/ninja2020/generic/email_preferences.blade.php b/resources/views/portal/ninja2020/generic/email_preferences.blade.php new file mode 100644 index 000000000000..af73164212da --- /dev/null +++ b/resources/views/portal/ninja2020/generic/email_preferences.blade.php @@ -0,0 +1,43 @@ +@extends('portal.ninja2020.layout.clean') @section('meta_title', +ctrans('texts.preferences')) @section('body') +
+
+
+ {{ $company->present()->name() }} +

+ {{ ctrans('texts.email_preferences') }} +

+ +
+ @csrf @method('put') + + @if($receive_emails) +

{{ ctrans('texts.subscribe_help') }}

+ + + @else +

{{ ctrans('texts.unsubscribe_help') }}

+ + + @endif +
+
+
+
+@stop diff --git a/routes/client.php b/routes/client.php index 870b3514abdb..192f9f20424c 100644 --- a/routes/client.php +++ b/routes/client.php @@ -1,5 +1,6 @@ ['invite_db'], 'prefix' => 'client', 'as' => 'clie Route::get('{entity}/{invitation_key}/download', [App\Http\Controllers\ClientPortal\InvitationController::class, 'routerForDownload'])->middleware('token_auth'); Route::get('pay/{invitation_key}', [App\Http\Controllers\ClientPortal\InvitationController::class, 'payInvoice'])->name('pay.invoice'); + Route::get('email_preferences/{entity}/{invitation_key}', [EmailPreferencesController::class, 'index'])->name('email_preferences')->middleware('signed'); + Route::put('email_preferences/{entity}/{invitation_key}', [EmailPreferencesController::class, 'update']); + Route::get('unsubscribe/{entity}/{invitation_key}', [App\Http\Controllers\ClientPortal\InvitationController::class, 'unsubscribe'])->name('unsubscribe'); }); @@ -159,3 +163,5 @@ Route::fallback(function () { abort(404); })->middleware('throttle:404'); + +// Fix me: Move into invite_db middleware group.