feature(send push notification): send push notification to broker's followers
This commit is contained in:
parent
82715973dc
commit
0d0818baf3
@ -73,3 +73,6 @@ OTP_LIFESPAN=10
|
|||||||
|
|
||||||
VAPID_PUBLIC_KEY=
|
VAPID_PUBLIC_KEY=
|
||||||
VAPID_PRIVATE_KEY=
|
VAPID_PRIVATE_KEY=
|
||||||
|
|
||||||
|
# Same as the VAPID_PUBLIC_KEY
|
||||||
|
VITE_VAPID_PUBLIC_KEY=BOBjjU2E-h8pDCV13yPwvMDR_WZwEhFmQY90gr16oJ5L1mpJ5qc7-0WzXcD1Z9D0Ozz0cLZxTe0_7nnDK3VFMP4
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
use App\Models\Broker;
|
use App\Models\Broker;
|
||||||
use App\Models\Customer;
|
use App\Models\Customer;
|
||||||
use App\Models\Deal;
|
use App\Models\Deal;
|
||||||
use App\Models\Follow;
|
|
||||||
use App\Notifications\NewDealNotification;
|
use App\Notifications\NewDealNotification;
|
||||||
|
|
||||||
final readonly class SendDealCreatedNotificationCustomerAction
|
final readonly class SendDealCreatedNotificationCustomerAction
|
||||||
@ -17,10 +16,12 @@ public function execute(Deal $deal): void
|
|||||||
*/
|
*/
|
||||||
$broker = $deal->broker->type;
|
$broker = $deal->broker->type;
|
||||||
$followers = $broker->followers()->with('user')->get();
|
$followers = $broker->followers()->with('user')->get();
|
||||||
$followers->map(function (Customer $customer) use ($deal) {
|
$followers->map(function (Customer $follower) use ($deal) {
|
||||||
\Log::info('dump', [$customer]);
|
$user = $follower->user;
|
||||||
\Log::info("Sending notification to {$customer->user->name}");
|
|
||||||
$customer->user->notify(new NewDealNotification($deal));
|
\Log::info("Sending notification to {$follower->user->name}", [$deal, $follower->user]);
|
||||||
|
|
||||||
|
$user->notifyNow(new NewDealNotification($deal));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,11 +19,12 @@ public function index()
|
|||||||
public function approve(Deal $deal, SendDealCreatedNotificationCustomerAction $notificationCustomerAction)
|
public function approve(Deal $deal, SendDealCreatedNotificationCustomerAction $notificationCustomerAction)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
\DB::transaction(function () use ($notificationCustomerAction, $deal) {
|
\DB::transaction(function () use ($deal) {
|
||||||
$deal->active = true;
|
$deal->active = true;
|
||||||
$deal->save();
|
$deal->save();
|
||||||
$notificationCustomerAction->execute($deal);
|
|
||||||
});
|
});
|
||||||
|
$notificationCustomerAction->execute($deal);
|
||||||
|
|
||||||
return back()->with('success', 'Deal activated successfully.');
|
return back()->with('success', 'Deal activated successfully.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
Log::error('Deal activation Failed: ', [$e->getMessage(), $e->getTrace()]);
|
Log::error('Deal activation Failed: ', [$e->getMessage(), $e->getTrace()]);
|
||||||
|
|||||||
@ -3,14 +3,15 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Http\Requests\PushSubscriptionRequest;
|
use App\Http\Requests\PushSubscriptionRequest;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Container\Attributes\CurrentUser;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
class PushSubscriptionController extends Controller
|
class PushSubscriptionController extends Controller
|
||||||
{
|
{
|
||||||
public function __invoke(PushSubscriptionRequest $request): JsonResponse
|
public function __invoke(#[CurrentUser] User $user, PushSubscriptionRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
$data = $request->validated();
|
$data = $request->validated();
|
||||||
$user = $request->user();
|
|
||||||
$user->updatePushSubscription($data['endpoint'], $data['keys']['p256dh'], $data['keys']['auth']);
|
$user->updatePushSubscription($data['endpoint'], $data['keys']['p256dh'], $data['keys']['auth']);
|
||||||
|
|
||||||
return response()->json(['message' => 'Push subscription updated successfully.']);
|
return response()->json(['message' => 'Push subscription updated successfully.']);
|
||||||
|
|||||||
@ -22,7 +22,7 @@ public function authorize(): bool
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'endpoint' => 'required|string|max:255',
|
'endpoint' => 'required|string|max:500',
|
||||||
'keys.auth' => 'required|string|max:255',
|
'keys.auth' => 'required|string|max:255',
|
||||||
'keys.p256dh' => 'required|string|max:255',
|
'keys.p256dh' => 'required|string|max:255',
|
||||||
];
|
];
|
||||||
|
|||||||
@ -27,7 +27,7 @@ public function toArray(Request $request): array
|
|||||||
'totalRedirection' => $this->total_redirection,
|
'totalRedirection' => $this->total_redirection,
|
||||||
'isLiked' => $this->is_liked,
|
'isLiked' => $this->is_liked,
|
||||||
'isFavorite' => $this->is_favorite,
|
'isFavorite' => $this->is_favorite,
|
||||||
'isFollowed' => $this->is_followed
|
'isFollowed' => $this->is_followed,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use NotificationChannels\WebPush\HasPushSubscriptions;
|
use NotificationChannels\WebPush\HasPushSubscriptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property string $name
|
* @property string $name
|
||||||
@ -62,7 +63,7 @@
|
|||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable, HasPushSubscriptions;
|
use HasFactory, HasPushSubscriptions, Notifiable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
|
|||||||
@ -3,15 +3,13 @@
|
|||||||
namespace App\Notifications;
|
namespace App\Notifications;
|
||||||
|
|
||||||
use App\Models\Deal;
|
use App\Models\Deal;
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Notifications\Notification;
|
use Illuminate\Notifications\Notification;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use NotificationChannels\WebPush\WebPushChannel;
|
use NotificationChannels\WebPush\WebPushChannel;
|
||||||
use NotificationChannels\WebPush\WebPushMessage;
|
use NotificationChannels\WebPush\WebPushMessage;
|
||||||
|
|
||||||
class NewDealNotification extends Notification
|
class NewDealNotification extends Notification
|
||||||
{
|
{
|
||||||
use Queueable;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new notification instance.
|
* Create a new notification instance.
|
||||||
*/
|
*/
|
||||||
@ -32,9 +30,11 @@ public function via(object $notifiable): array
|
|||||||
|
|
||||||
public function toWebPush($notifiable, $notification): WebPushMessage
|
public function toWebPush($notifiable, $notification): WebPushMessage
|
||||||
{
|
{
|
||||||
|
\Log::info('Building WebPush for user: '.$notifiable->id);
|
||||||
|
|
||||||
return (new WebPushMessage)
|
return (new WebPushMessage)
|
||||||
->title("New deal from {$this->deal->broker->name}")
|
->title("New deal from {$this->deal->broker->name}")
|
||||||
->body("Check out this deal: {$this->deal->title}")
|
->body('Check out this deal:'.Str::limit($this->deal->title, 30, '...'))
|
||||||
->action('View deal', route('explore', ['show' => $this->deal->id]));
|
->action('View deal', route('explore', ['show' => $this->deal->id]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
config/webpush.php
Normal file
44
config/webpush.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are the keys for authentication (VAPID).
|
||||||
|
* These keys must be safely stored and should not change.
|
||||||
|
*/
|
||||||
|
'vapid' => [
|
||||||
|
'subject' => env('VAPID_SUBJECT'),
|
||||||
|
'public_key' => env('VAPID_PUBLIC_KEY'),
|
||||||
|
'private_key' => env('VAPID_PRIVATE_KEY'),
|
||||||
|
'pem_file' => env('VAPID_PEM_FILE'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is model that will be used to for push subscriptions.
|
||||||
|
*/
|
||||||
|
'model' => \NotificationChannels\WebPush\PushSubscription::class,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the name of the table that will be created by the migration and
|
||||||
|
* used by the PushSubscription model shipped with this package.
|
||||||
|
*/
|
||||||
|
'table_name' => env('WEBPUSH_DB_TABLE', 'push_subscriptions'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the database connection that will be used by the migration and
|
||||||
|
* the PushSubscription model shipped with this package.
|
||||||
|
*/
|
||||||
|
'database_connection' => env('WEBPUSH_DB_CONNECTION', env('DB_CONNECTION', 'mysql')),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Guzzle client options used by Minishlink\WebPush.
|
||||||
|
*/
|
||||||
|
'client_options' => [],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The automatic padding in bytes used by Minishlink\WebPush.
|
||||||
|
* Set to false to support Firefox Android with v1 endpoint.
|
||||||
|
*/
|
||||||
|
'automatic_padding' => env('WEBPUSH_AUTOMATIC_PADDING', true),
|
||||||
|
|
||||||
|
];
|
||||||
@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
return new class extends Migration
|
return new class extends Migration
|
||||||
{
|
{
|
||||||
|
|||||||
@ -71,6 +71,9 @@ export async function favorite(button) {
|
|||||||
*/
|
*/
|
||||||
export async function follow(button, brokerId) {
|
export async function follow(button, brokerId) {
|
||||||
let isFollowed = button.dataset.followed === 'true';
|
let isFollowed = button.dataset.followed === 'true';
|
||||||
|
if (!isFollowed) {
|
||||||
|
await initSw();
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
button.dataset.followed = isFollowed ? 'false' : 'true';
|
button.dataset.followed = isFollowed ? 'false' : 'true';
|
||||||
// Update other buttons
|
// Update other buttons
|
||||||
@ -83,9 +86,7 @@ export async function follow(button, brokerId) {
|
|||||||
let response = await axios.post(`/api/follow/${brokerId}`);
|
let response = await axios.post(`/api/follow/${brokerId}`);
|
||||||
showToast(response.data.message);
|
showToast(response.data.message);
|
||||||
|
|
||||||
if (!isFollowed) {
|
|
||||||
initSw();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
button.dataset.followed = isFollowed ? 'true' : 'false';
|
button.dataset.followed = isFollowed ? 'true' : 'false';
|
||||||
showToast('Something went wrong!')
|
showToast('Something went wrong!')
|
||||||
|
|||||||
@ -20,11 +20,16 @@ export const initSw = async () => {
|
|||||||
* Request permission and trigger subscription
|
* Request permission and trigger subscription
|
||||||
*/
|
*/
|
||||||
const initPush = async () => {
|
const initPush = async () => {
|
||||||
// Request permission
|
if (Notification.permission === 'denied') {
|
||||||
|
alert('You have blocked notifications. Please click the Lock icon in your address bar to enable them.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Request permission (this will only pop up if status is 'default')
|
||||||
const permission = await Notification.requestPermission();
|
const permission = await Notification.requestPermission();
|
||||||
|
|
||||||
if (permission !== 'granted') {
|
if (permission !== 'granted') {
|
||||||
console.warn('Notification permission denied.');
|
console.warn('Notification permission denied or dismissed.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,8 +44,11 @@ const subscribeUser = async () => {
|
|||||||
|
|
||||||
// Check if a subscription already exists
|
// Check if a subscription already exists
|
||||||
let pushSubscription = await registration.pushManager.getSubscription();
|
let pushSubscription = await registration.pushManager.getSubscription();
|
||||||
|
if (pushSubscription) {
|
||||||
|
await pushSubscription.unsubscribe();
|
||||||
|
console.log('Unsubscribed old record...');
|
||||||
|
}
|
||||||
|
|
||||||
if (!pushSubscription) {
|
|
||||||
const subscribeOptions = {
|
const subscribeOptions = {
|
||||||
userVisibleOnly: true,
|
userVisibleOnly: true,
|
||||||
applicationServerKey: urlBase64ToUint8Array(
|
applicationServerKey: urlBase64ToUint8Array(
|
||||||
@ -55,11 +63,9 @@ const subscribeUser = async () => {
|
|||||||
console.error('Failed to subscribe user:', err);
|
console.error('Failed to subscribe user:', err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.log('User is already subscribed.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send subscription to Laravel backend
|
|
||||||
|
// Send a subscription to Laravel backend
|
||||||
if (pushSubscription) {
|
if (pushSubscription) {
|
||||||
console.log('Received PushSubscription:', JSON.stringify(pushSubscription));
|
console.log('Received PushSubscription:', JSON.stringify(pushSubscription));
|
||||||
await storePushSubscription(pushSubscription);
|
await storePushSubscription(pushSubscription);
|
||||||
|
|||||||
@ -14,6 +14,20 @@
|
|||||||
Route::get('/', HomeController::class)->name('home');
|
Route::get('/', HomeController::class)->name('home');
|
||||||
Route::get('/explore', ExplorePageController::class)->name('explore');
|
Route::get('/explore', ExplorePageController::class)->name('explore');
|
||||||
Route::post('/contact', ContactController::class)->name('contact');
|
Route::post('/contact', ContactController::class)->name('contact');
|
||||||
|
|
||||||
|
Route::get('/test-openssl', function () {
|
||||||
|
$res = openssl_pkey_new([
|
||||||
|
'curve_name' => 'prime256v1',
|
||||||
|
'private_key_type' => OPENSSL_KEYTYPE_EC,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($res === false) {
|
||||||
|
return 'ERROR: '.openssl_error_string(); // likely "error:02001003:system library:fopen:No such process"
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'SUCCESS: OpenSSL is configured correctly.';
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This routes are accessed by JS XHR requests, and is loaded here cause
|
* This routes are accessed by JS XHR requests, and is loaded here cause
|
||||||
* we do not want to use sanctum for web requests
|
* we do not want to use sanctum for web requests
|
||||||
|
|||||||
0
storage/logs/.gitignore
vendored
Normal file → Executable file
0
storage/logs/.gitignore
vendored
Normal file → Executable file
Loading…
x
Reference in New Issue
Block a user