Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions app/Contracts/TransactionalNotification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Contracts;

use App\Listeners\SuppressMailNotificationListener;

/**
* Marker interface for notifications that must always be delivered,
* regardless of the user's email preferences or verification state.
*
* Account recovery, purchase receipts, and license/entitlement
* notifications fall into this category.
*
* @see SuppressMailNotificationListener
*/
interface TransactionalNotification {}
8 changes: 8 additions & 0 deletions app/Http/Controllers/Api/LicenseController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
use App\Jobs\CreateAnystackLicenseJob;
use App\Models\License;
use App\Models\User;
use App\Notifications\ClaimAccount;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Enum;

Expand Down Expand Up @@ -46,6 +48,12 @@ public function store(Request $request)
]
);

if ($user->wasRecentlyCreated) {
$user->notify(new ClaimAccount(
Password::broker()->createToken($user)
));
}

// Create the license via job
$subscription = Subscription::from($validated['subscription']);

Expand Down
14 changes: 11 additions & 3 deletions app/Http/Controllers/Auth/CustomerAuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,17 @@ public function resetPassword(Request $request): RedirectResponse
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, $password): void {
$user->forceFill([
'password' => $password,
]);
$attributes = ['password' => $password];

// Proving control of the inbox + setting a password is sufficient
// to consider the email verified. This also lets the same flow
// serve as the "claim your account" path for users created via
// checkout.
if (! $user->email_verified_at) {
$attributes['email_verified_at'] = now();
}

$user->forceFill($attributes);

$user->save();
}
Expand Down
11 changes: 9 additions & 2 deletions app/Listeners/SuppressMailNotificationListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace App\Listeners;

use App\Contracts\TransactionalNotification;
use App\Models\User;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Notifications\Events\NotificationSending;

Expand All @@ -18,8 +20,13 @@ public function handle(NotificationSending $event): bool
return true;
}

// System notifications like email verification should always be sent
if ($event->notification instanceof VerifyEmail) {
// Transactional notifications (account recovery, verification,
// purchase receipts, entitlement grants) must always be delivered.
// Framework notifications can't implement our marker, so they're
// listed explicitly.
if ($event->notification instanceof TransactionalNotification
|| $event->notification instanceof VerifyEmail
|| $event->notification instanceof ResetPassword) {
return true;
}

Expand Down
3 changes: 3 additions & 0 deletions app/Livewire/ClaimDonationLicense.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Jobs\CreateAnystackLicenseJob;
use App\Models\OpenCollectiveDonation;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
Expand Down Expand Up @@ -107,6 +108,8 @@ public function claim(): void
'name' => $this->name,
'password' => Hash::make($this->password),
]);

event(new Registered($user));
}

// Parse name for first/last
Expand Down
12 changes: 11 additions & 1 deletion app/Livewire/MobilePricing.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

use App\Enums\Subscription;
use App\Models\User;
use App\Notifications\ClaimAccount;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Laravel\Cashier\Cashier;
Expand Down Expand Up @@ -182,11 +184,19 @@ private function findOrCreateUser(string $email): User
'email' => 'required|email|max:255',
]);

return User::firstOrCreate([
$user = User::firstOrCreate([
'email' => $email,
], [
'password' => Hash::make(Str::random(72)),
]);

if ($user->wasRecentlyCreated) {
$user->notify(new ClaimAccount(
Password::broker()->createToken($user)
));
}

return $user;
}

private function successUrl(): string
Expand Down
14 changes: 7 additions & 7 deletions app/Livewire/OrderSuccess.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace App\Livewire;

use App\Enums\Subscription;
use App\Models\License;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Laravel\Cashier\Cashier;
use Livewire\Attributes\Layout;
Expand All @@ -17,10 +16,10 @@ class OrderSuccess extends Component
{
public ?string $email = null;

public ?string $licenseKey = null;

public ?Subscription $subscription = null;

public bool $isExistingUser = false;

public string $checkoutSessionId;

public function mount(string $checkoutSessionId): void
Expand Down Expand Up @@ -67,9 +66,10 @@ public function loadData(): void
return;
}

$this->email = $subscriptionRecord->user->email;
$this->licenseKey = License::query()
->whereBelongsTo($subscriptionItem)
->first()?->key;
$user = $subscriptionRecord->user;
$this->email = $user->email;
// Users created via checkout start with email_verified_at = null and
// are sent a ClaimAccount email. Verified users already have access.
$this->isExistingUser = ! is_null($user->email_verified_at);
}
}
3 changes: 2 additions & 1 deletion app/Notifications/BundleGranted.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Notifications;

use App\Contracts\TransactionalNotification;
use App\Models\Plugin;
use App\Models\PluginBundle;
use Illuminate\Bus\Queueable;
Expand All @@ -10,7 +11,7 @@
use Illuminate\Notifications\Notification;
use Illuminate\Support\Collection;

class BundleGranted extends Notification implements ShouldQueue
class BundleGranted extends Notification implements ShouldQueue, TransactionalNotification
{
use Queueable;

Expand Down
41 changes: 41 additions & 0 deletions app/Notifications/ClaimAccount.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace App\Notifications;

use App\Contracts\TransactionalNotification;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class ClaimAccount extends Notification implements ShouldQueue, TransactionalNotification
{
use Queueable;

public function __construct(public string $token) {}

/**
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail'];
}

public function toMail(object $notifiable): MailMessage
{
$url = route('password.reset', [
'token' => $this->token,
'email' => $notifiable->getEmailForPasswordReset(),
]);

return (new MailMessage)
->subject('Welcome to NativePHP — Claim Your Account')
->greeting('Welcome to NativePHP!')
->line('Thanks for your purchase. We\'ve created an account for you so you can access your licenses and downloads.')
->line('To finish setting up your account, please click the button below to verify your email address and set a password.')
->action('Claim Your Account', $url)
->line('This link will expire in '.config('auth.passwords.users.expire').' minutes. If it expires, you can request a new one from the password reset page.')
->salutation("Happy coding!\n\nThe NativePHP Team");
}
}
3 changes: 2 additions & 1 deletion app/Notifications/LicenseKeyGenerated.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

namespace App\Notifications;

use App\Contracts\TransactionalNotification;
use App\Enums\Subscription;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class LicenseKeyGenerated extends Notification implements ShouldQueue
class LicenseKeyGenerated extends Notification implements ShouldQueue, TransactionalNotification
{
use Queueable;

Expand Down
3 changes: 2 additions & 1 deletion app/Notifications/PluginGranted.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

namespace App\Notifications;

use App\Contracts\TransactionalNotification;
use App\Models\Plugin;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class PluginGranted extends Notification implements ShouldQueue
class PluginGranted extends Notification implements ShouldQueue, TransactionalNotification
{
use Queueable;

Expand Down
3 changes: 2 additions & 1 deletion app/Notifications/ProductGranted.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

namespace App\Notifications;

use App\Contracts\TransactionalNotification;
use App\Models\Product;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class ProductGranted extends Notification implements ShouldQueue
class ProductGranted extends Notification implements ShouldQueue, TransactionalNotification
{
use Queueable;

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading