feat: add persistent mobile notifications
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Actions;
|
||||
use App\Enums\IdentityVerificationStatus;
|
||||
use App\Models\IdentityVerificationRequest;
|
||||
use App\Models\User;
|
||||
use App\Notifications\CertificationReviewedNotification;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
@@ -60,6 +61,11 @@ class ReviewIdentityVerification
|
||||
|
||||
$this->deleteReviewedDocument($reviewedRequest);
|
||||
|
||||
$reviewedRequest->user->notify(
|
||||
(new CertificationReviewedNotification($reviewedRequest))
|
||||
->locale($reviewedRequest->user->preferredLocale()),
|
||||
);
|
||||
|
||||
return $reviewedRequest->fresh(['user', 'reviewer']);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,8 +55,9 @@ class SendEngagementReminderNotifications extends Command
|
||||
private function eligibleUsersQuery(): Builder
|
||||
{
|
||||
return User::query()
|
||||
->select(['id', 'locale', 'suspended_at'])
|
||||
->select(['id', 'locale', 'suspended_at', 'engagement_reminders_enabled'])
|
||||
->whereNull('suspended_at')
|
||||
->where('engagement_reminders_enabled', true)
|
||||
->whereHas('deviceTokens')
|
||||
->whereDoesntHave('mealPosts', fn (Builder $query): Builder => $query
|
||||
->where('created_at', '>=', now()->subDay()));
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Enums\FollowStatus;
|
||||
use App\Http\Requests\FollowRequest;
|
||||
use App\Http\Resources\FollowResource;
|
||||
use App\Models\User;
|
||||
use App\Notifications\NewFollowerNotification;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
@@ -27,7 +28,14 @@ class FollowController extends Controller
|
||||
// Default to Accepted as is_private doesn't exist in the DB yet
|
||||
$status = FollowStatus::Accepted;
|
||||
|
||||
$authUser->following()->syncWithPivotValues([$user->id], ['status' => $status], false);
|
||||
$changes = $authUser->following()->syncWithPivotValues([$user->id], ['status' => $status], false);
|
||||
|
||||
if ($changes['attached'] !== []) {
|
||||
$user->notify(
|
||||
(new NewFollowerNotification($authUser))
|
||||
->locale($user->preferredLocale()),
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => __('api.follows.followed'),
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Resources\NotificationResource;
|
||||
use App\Notifications\MobileNotification;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
|
||||
class NotificationController extends Controller
|
||||
{
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$perPage = max(1, min($request->integer('per_page', 20), 60));
|
||||
$notifications = $request->user()
|
||||
->notifications()
|
||||
->whereIn('type', MobileNotification::TYPES)
|
||||
->latest()
|
||||
->paginate($perPage);
|
||||
|
||||
return NotificationResource::collection($notifications)->additional([
|
||||
'unreadCount' => $this->unreadCountFor($request),
|
||||
]);
|
||||
}
|
||||
|
||||
public function unreadCount(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'count' => $this->unreadCountFor($request),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function markAsRead(Request $request, string $notification): NotificationResource
|
||||
{
|
||||
$databaseNotification = $request->user()
|
||||
->notifications()
|
||||
->whereIn('type', MobileNotification::TYPES)
|
||||
->findOrFail($notification);
|
||||
|
||||
$databaseNotification->markAsRead();
|
||||
|
||||
return NotificationResource::make($databaseNotification->fresh());
|
||||
}
|
||||
|
||||
public function markAllAsRead(Request $request): JsonResponse
|
||||
{
|
||||
$updated = $request->user()
|
||||
->unreadNotifications()
|
||||
->whereIn('type', MobileNotification::TYPES)
|
||||
->update(['read_at' => now()]);
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'updated' => $updated,
|
||||
],
|
||||
'message' => __('api.notifications.all_read'),
|
||||
]);
|
||||
}
|
||||
|
||||
private function unreadCountFor(Request $request): int
|
||||
{
|
||||
return $request->user()
|
||||
->unreadNotifications()
|
||||
->whereIn('type', MobileNotification::TYPES)
|
||||
->count();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\UpdateNotificationPreferencesRequest;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NotificationPreferenceController extends Controller
|
||||
{
|
||||
public function show(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => $this->preferences($request),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateNotificationPreferencesRequest $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
$validated = $request->validated();
|
||||
|
||||
$user->forceFill([
|
||||
'social_notifications_enabled' => $validated['socialNotificationsEnabled']
|
||||
?? $user->social_notifications_enabled,
|
||||
'engagement_reminders_enabled' => $validated['engagementRemindersEnabled']
|
||||
?? $user->engagement_reminders_enabled,
|
||||
])->save();
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->preferences($request),
|
||||
'message' => __('api.notifications.preferences_updated'),
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return array{socialNotificationsEnabled: bool, engagementRemindersEnabled: bool} */
|
||||
private function preferences(Request $request): array
|
||||
{
|
||||
return [
|
||||
'socialNotificationsEnabled' => (bool) $request->user()->social_notifications_enabled,
|
||||
'engagementRemindersEnabled' => (bool) $request->user()->engagement_reminders_enabled,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateNotificationPreferencesRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'socialNotificationsEnabled' => ['sometimes', 'required', 'boolean'],
|
||||
'engagementRemindersEnabled' => ['sometimes', 'required', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class NotificationResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$data = is_array($this->data) ? $this->data : [];
|
||||
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'type' => $data['type'] ?? $this->type,
|
||||
'category' => $data['category'] ?? 'account',
|
||||
'title' => $data['title'] ?? __('api.notifications.default_title'),
|
||||
'body' => $data['body'] ?? '',
|
||||
'url' => $data['url'] ?? null,
|
||||
'readAt' => $this->read_at?->toISOString(),
|
||||
'createdAt' => $this->created_at?->toISOString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@ namespace App\Listeners;
|
||||
use App\Enums\BillingProductPurpose;
|
||||
use App\Enums\PaymentTransactionStatus;
|
||||
use App\Models\BillingProduct;
|
||||
use App\Models\PaymentTransaction;
|
||||
use App\Models\User;
|
||||
use App\Notifications\PaymentFailedNotification;
|
||||
use App\Services\PaymentInvoiceEmailer;
|
||||
use App\Services\PaymentTransactionRecorder;
|
||||
use Laravel\Cashier\Cashier;
|
||||
@@ -112,7 +114,7 @@ class HandleStripeWebhook
|
||||
return;
|
||||
}
|
||||
|
||||
$this->transactions->recordCheckoutSession(
|
||||
$transaction = $this->transactions->recordCheckoutSession(
|
||||
user: $this->userFromCustomer($session['customer'] ?? null),
|
||||
product: $this->productFromMetadata($session['metadata'] ?? null),
|
||||
stripeCheckoutSessionId: $sessionId,
|
||||
@@ -126,6 +128,8 @@ class HandleStripeWebhook
|
||||
errorMessage: 'Checkout session expired before payment.',
|
||||
payload: $session,
|
||||
);
|
||||
|
||||
$this->notifyPaymentFailure($transaction);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,6 +214,8 @@ class HandleStripeWebhook
|
||||
}
|
||||
|
||||
$lines = $invoice['lines']['data'] ?? [null];
|
||||
$notificationUser = null;
|
||||
$shouldNotify = false;
|
||||
|
||||
foreach ($lines ?: [null] as $line) {
|
||||
$priceId = is_array($line) ? $this->linePriceId($line) : null;
|
||||
@@ -217,7 +223,7 @@ class HandleStripeWebhook
|
||||
? BillingProduct::query()->where('stripe_price_id', $priceId)->first()
|
||||
: null;
|
||||
|
||||
$this->transactions->recordInvoice(
|
||||
$transaction = $this->transactions->recordInvoice(
|
||||
user: $this->userFromCustomer($invoice['customer'] ?? null),
|
||||
product: $product,
|
||||
stripeInvoiceId: $invoiceId,
|
||||
@@ -237,7 +243,44 @@ class HandleStripeWebhook
|
||||
errorMessage: 'Invoice payment failed.',
|
||||
payload: $invoice,
|
||||
);
|
||||
|
||||
if ($this->shouldNotifyPaymentFailure($transaction)) {
|
||||
$notificationUser ??= $transaction->user;
|
||||
$shouldNotify = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($shouldNotify && $notificationUser) {
|
||||
$notificationUser->notify(
|
||||
(new PaymentFailedNotification)
|
||||
->locale($notificationUser->preferredLocale()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function notifyPaymentFailure(PaymentTransaction $transaction): void
|
||||
{
|
||||
if (! $this->shouldNotifyPaymentFailure($transaction)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $transaction->user;
|
||||
|
||||
if (! $user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user->notify(
|
||||
(new PaymentFailedNotification)
|
||||
->locale($user->preferredLocale()),
|
||||
);
|
||||
}
|
||||
|
||||
private function shouldNotifyPaymentFailure(PaymentTransaction $transaction): bool
|
||||
{
|
||||
return $transaction->wasRecentlyCreated
|
||||
|| $transaction->wasChanged('status')
|
||||
|| $transaction->wasChanged('user_id');
|
||||
}
|
||||
|
||||
private function userFromCustomer(mixed $customer): ?User
|
||||
|
||||
@@ -33,6 +33,11 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasLocale
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use Billable, HasApiTokens, HasFactory, HasUlids, Notifiable, SoftDeletes;
|
||||
|
||||
protected $attributes = [
|
||||
'social_notifications_enabled' => true,
|
||||
'engagement_reminders_enabled' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
@@ -61,6 +66,8 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasLocale
|
||||
'sex',
|
||||
'date_of_birth',
|
||||
'terms_accepted_at',
|
||||
'social_notifications_enabled',
|
||||
'engagement_reminders_enabled',
|
||||
'suspended_at',
|
||||
'suspended_by',
|
||||
'suspended_reason',
|
||||
@@ -99,6 +106,8 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasLocale
|
||||
'role' => UserRole::class,
|
||||
'date_of_birth' => 'immutable_date',
|
||||
'terms_accepted_at' => 'datetime',
|
||||
'social_notifications_enabled' => 'boolean',
|
||||
'engagement_reminders_enabled' => 'boolean',
|
||||
'suspended_at' => 'datetime',
|
||||
'trial_ends_at' => 'datetime',
|
||||
];
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Enums\IdentityVerificationStatus;
|
||||
use App\Models\IdentityVerificationRequest;
|
||||
use App\Services\MobileDeepLink;
|
||||
|
||||
class CertificationReviewedNotification extends MobileNotification
|
||||
{
|
||||
public function __construct(
|
||||
private readonly IdentityVerificationRequest $verificationRequest,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function category(): string
|
||||
{
|
||||
return self::CATEGORY_ACCOUNT;
|
||||
}
|
||||
|
||||
protected function payload(object $notifiable): array
|
||||
{
|
||||
$approved = $this->verificationRequest->status === IdentityVerificationStatus::APPROVED;
|
||||
|
||||
return [
|
||||
'type' => $approved ? 'certification_approved' : 'certification_rejected',
|
||||
'title' => $approved
|
||||
? __('api.notifications.certification_approved_title')
|
||||
: __('api.notifications.certification_rejected_title'),
|
||||
'body' => $approved
|
||||
? __('api.notifications.certification_approved_body')
|
||||
: __('api.notifications.certification_rejected_body', [
|
||||
'reason' => $this->verificationRequest->rejection_reason,
|
||||
]),
|
||||
'url' => MobileDeepLink::to('profile/settings/certification'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,10 @@
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Services\MobileDeepLink;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class EngagementReminderNotification extends Notification
|
||||
class EngagementReminderNotification extends MobileNotification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
private const MESSAGE_KEYS = [
|
||||
'publish_meal',
|
||||
'meal_photo',
|
||||
@@ -19,7 +15,10 @@ class EngagementReminderNotification extends Notification
|
||||
'workout_checkin',
|
||||
];
|
||||
|
||||
public function __construct(private readonly string $messageKey) {}
|
||||
public function __construct(private readonly string $messageKey)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public static function randomMessageKey(): string
|
||||
{
|
||||
@@ -34,38 +33,22 @@ class EngagementReminderNotification extends Notification
|
||||
return self::MESSAGE_KEYS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
protected function category(): string
|
||||
{
|
||||
return ['expo'];
|
||||
return self::CATEGORY_ENGAGEMENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* title: string,
|
||||
* body: string,
|
||||
* sound: string,
|
||||
* channelId: string,
|
||||
* data: array{type: string, message_key: string, url: string}
|
||||
* }
|
||||
*/
|
||||
public function toExpoPush(object $notifiable): array
|
||||
protected function payload(object $notifiable): array
|
||||
{
|
||||
$message = $this->message();
|
||||
|
||||
return [
|
||||
'type' => 'engagement_reminder',
|
||||
'title' => $message['title'],
|
||||
'body' => $message['body'],
|
||||
'sound' => 'default',
|
||||
'channelId' => 'default',
|
||||
'url' => MobileDeepLink::to($this->path()),
|
||||
'data' => [
|
||||
'type' => 'engagement_reminder',
|
||||
'message_key' => $this->messageKey,
|
||||
'url' => MobileDeepLink::to($this->path()),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,25 +4,21 @@ namespace App\Notifications;
|
||||
|
||||
use App\Models\PostReviews;
|
||||
use App\Services\MobileDeepLink;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MealPostCommentedNotification extends Notification
|
||||
class MealPostCommentedNotification extends MobileNotification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(private readonly PostReviews $postReview) {}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
public function __construct(private readonly PostReviews $postReview)
|
||||
{
|
||||
return ['expo'];
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function toExpoPush(object $notifiable): array
|
||||
protected function category(): string
|
||||
{
|
||||
return self::CATEGORY_SOCIAL;
|
||||
}
|
||||
|
||||
protected function payload(object $notifiable): array
|
||||
{
|
||||
$this->postReview->loadMissing(
|
||||
'mealPost:id,title,user_id',
|
||||
@@ -35,16 +31,13 @@ class MealPostCommentedNotification extends Notification
|
||||
$commenterName = $this->postReview->user?->name ?: __('api.notifications.someone');
|
||||
|
||||
return [
|
||||
'type' => 'meal_commented',
|
||||
'title' => __('api.notifications.meal_post_commented_title'),
|
||||
'body' => __('api.notifications.meal_post_commented_body', [
|
||||
'meal' => $mealTitle,
|
||||
'name' => $commenterName,
|
||||
]),
|
||||
'sound' => 'default',
|
||||
'channelId' => 'default',
|
||||
'data' => [
|
||||
'url' => MobileDeepLink::to("meals/{$mealPostId}"),
|
||||
],
|
||||
'url' => MobileDeepLink::to("meals/{$mealPostId}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
abstract class MobileNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public const array TYPES = [
|
||||
'certification_approved',
|
||||
'certification_rejected',
|
||||
'engagement_reminder',
|
||||
'meal_commented',
|
||||
'new_follower',
|
||||
'payment_failed',
|
||||
];
|
||||
|
||||
protected const string CATEGORY_ACCOUNT = 'account';
|
||||
|
||||
protected const string CATEGORY_ENGAGEMENT = 'engagement';
|
||||
|
||||
protected const string CATEGORY_SOCIAL = 'social';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->afterCommit();
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
if ($this->category() === self::CATEGORY_ENGAGEMENT
|
||||
&& $notifiable instanceof User
|
||||
&& ! $notifiable->engagement_reminders_enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$channels = ['database'];
|
||||
|
||||
if ($this->shouldSendPush($notifiable)) {
|
||||
$channels[] = 'expo';
|
||||
}
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function toDatabase(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
...$this->payload($notifiable),
|
||||
'category' => $this->category(),
|
||||
];
|
||||
}
|
||||
|
||||
public function databaseType(object $notifiable): string
|
||||
{
|
||||
return $this->payload($notifiable)['type'];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function toExpoPush(object $notifiable): array
|
||||
{
|
||||
$payload = $this->payload($notifiable);
|
||||
|
||||
return [
|
||||
'title' => $payload['title'],
|
||||
'body' => $payload['body'],
|
||||
'sound' => 'default',
|
||||
'channelId' => 'default',
|
||||
'data' => [
|
||||
'type' => $payload['type'],
|
||||
'url' => $payload['url'],
|
||||
...($this->id ? ['notificationId' => $this->id] : []),
|
||||
...($payload['data'] ?? []),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
abstract protected function category(): string;
|
||||
|
||||
/** @return array{type: string, title: string, body: string, url: string, data?: array<string, mixed>} */
|
||||
abstract protected function payload(object $notifiable): array;
|
||||
|
||||
private function shouldSendPush(object $notifiable): bool
|
||||
{
|
||||
if (! $notifiable instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->category() !== self::CATEGORY_SOCIAL
|
||||
|| $notifiable->social_notifications_enabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\MobileDeepLink;
|
||||
|
||||
class NewFollowerNotification extends MobileNotification
|
||||
{
|
||||
public function __construct(private readonly User $follower)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function category(): string
|
||||
{
|
||||
return self::CATEGORY_SOCIAL;
|
||||
}
|
||||
|
||||
protected function payload(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'new_follower',
|
||||
'title' => __('api.notifications.new_follower_title'),
|
||||
'body' => __('api.notifications.new_follower_body', [
|
||||
'name' => $this->follower->name ?: __('api.notifications.someone'),
|
||||
]),
|
||||
'url' => MobileDeepLink::to("users/{$this->follower->getKey()}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Services\MobileDeepLink;
|
||||
|
||||
class PaymentFailedNotification extends MobileNotification
|
||||
{
|
||||
protected function category(): string
|
||||
{
|
||||
return self::CATEGORY_ACCOUNT;
|
||||
}
|
||||
|
||||
protected function payload(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'payment_failed',
|
||||
'title' => __('api.notifications.payment_failed_title'),
|
||||
'body' => __('api.notifications.payment_failed_body'),
|
||||
'url' => MobileDeepLink::to('profile/settings/billing'),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user