{"id":33963919,"url":"https://github.com/humweb/notification-subscriptions","last_synced_at":"2025-12-12T22:51:50.724Z","repository":{"id":295341300,"uuid":"508233447","full_name":"humweb/notification-subscriptions","owner":"humweb","description":null,"archived":false,"fork":false,"pushed_at":"2025-10-13T20:02:23.000Z","size":162,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-10-14T12:07:55.879Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/humweb.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"docs/contributing.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2022-06-28T09:22:25.000Z","updated_at":"2025-08-21T23:44:34.000Z","dependencies_parsed_at":"2025-05-25T01:46:33.149Z","dependency_job_id":"044a6eed-62f0-4ec9-8954-7ffdc3a18831","html_url":"https://github.com/humweb/notification-subscriptions","commit_stats":null,"previous_names":["humweb/notification-subscriptions"],"tags_count":0,"template":false,"template_full_name":"humweb/package-skeleton-laravel","purl":"pkg:github/humweb/notification-subscriptions","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/humweb%2Fnotification-subscriptions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/humweb%2Fnotification-subscriptions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/humweb%2Fnotification-subscriptions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/humweb%2Fnotification-subscriptions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/humweb","download_url":"https://codeload.github.com/humweb/notification-subscriptions/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/humweb%2Fnotification-subscriptions/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":27694177,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-12T02:00:06.775Z","response_time":129,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-12-12T22:51:50.024Z","updated_at":"2025-12-12T22:51:50.712Z","avatar_url":"https://github.com/humweb.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Notification Subscriptions for Laravel\n\n[![Tests](https://github.com/humweb/notification-subscriptions/actions/workflows/run-tests.yml/badge.svg)](https://github.com/humweb/notification-subscriptions/actions/workflows/run-tests.yml)\n[![codecov](https://codecov.io/gh/humweb/notification-subscriptions/graph/badge.svg)](https://codecov.io/gh/humweb/notification-subscriptions)\n[![Code Style](https://github.com/humweb/notification-subscriptions/actions/workflows/fix-php-code-style-issues.yml/badge.svg)](https://github.com/humweb/notification-subscriptions/actions/workflows/fix-php-code-style-issues.yml)\n[![PHPStan](https://github.com/humweb/notification-subscriptions/actions/workflows/phpstan.yml/badge.svg)](https://github.com/humweb/notification-subscriptions/actions/workflows/phpstan.yml)\n\nNotification Subscriptions allows your users to subscribe to certain notifications in your application, with support for per-channel preferences and notification digests (daily/weekly summaries).\n\n**For full documentation, please see the [docs/index.md](./docs/index.md) page.**\n\n## Quick Links\n\n-   [Installation](./docs/installation.md)\n-   [Configuration](./docs/configuration.md)\n-   [Usage](./docs/usage/subscribable-trait.md)\n-   [Usage](./docs/usage/subscribable-trait.md)\n    -   [Digest System (with structured builder)](./docs/usage/digest-system.md)\n-   [License](./docs/license.md)\n\n## Installation\n\nYou can install the package via composer:\n\n```bash\ncomposer require humweb/notification-subscriptions\n```\n\nYou can publish and run the migrations with:\n\n```bash\nphp artisan vendor:publish --tag=\"notification-subscriptions-migrations\"\nphp artisan migrate\n```\n\nThis will create two tables:\n\n-   `notification_subscriptions`: Stores user subscriptions, including channel and digest preferences.\n-   `pending_notifications`: Temporarily stores notifications that are scheduled for digest delivery.\n\nYou can publish the config file with:\n\n```bash\nphp artisan vendor:publish --tag=\"notification-subscriptions-config\"\n```\n\nThis will create a `config/notification-subscriptions.php` file.\n\n## Setup\n\n### 1. Prepare Your User Model\n\nAdd the `Humweb\\\\Notifications\\\\Traits\\\\Subscribable` trait to your `User` model (or any model you want to make subscribable).\n\n```php\nnamespace App\\\\Models;\n\nuse Humweb\\\\Notifications\\\\Traits\\\\Subscribable;\nuse Illuminate\\\\Foundation\\\\Auth\\\\User as Authenticatable;\nuse Illuminate\\\\Notifications\\\\Notifiable; // Usually already present\n\nclass User extends Authenticatable\n{\n    use Notifiable, Subscribable; // Add Subscribable here\n\n    // ... rest of your User model\n}\n```\n\n### 2. Configure Notification Types\n\nOpen the `config/notification-subscriptions.php` file. This is where you define all the notification types users can subscribe to, their available channels, and digest settings.\n\n```php\n\u003c?php\n\nuse Humweb\\\\Notifications\\\\Models\\\\NotificationSubscription;\n// Define your User model path\n// use App\\\\Models\\\\User;\n\nreturn [\n    // Typically App\\\\Models\\\\User::class\n    'user_model' =\u003e \\\\Humweb\\\\Notifications\\\\Database\\\\Stubs\\\\User::class, // Example, change to your User model\n    'subscription_model' =\u003e NotificationSubscription::class,\n    'table_name' =\u003e 'notification_subscriptions',\n    'pending_notifications_table_name' =\u003e 'pending_notifications', // Table for digest items\n\n    // Default notification class for digests\n    'digest_notification_class' =\u003e \\\\Humweb\\\\Notifications\\\\Notifications\\\\UserNotificationDigest::class,\n\n    // Available digest intervals for users to choose from\n    'digest_intervals' =\u003e [\n        'immediate' =\u003e 'Immediate',\n        'daily' =\u003e 'Daily Digest',\n        'weekly' =\u003e 'Weekly Digest',\n    ],\n\n    'notifications' =\u003e [\n        'app:updates' =\u003e [\n            'label' =\u003e 'Application Updates',\n            'description' =\u003e 'Receive notifications about new features and important updates.',\n            'class' =\u003e App\\\\Notifications\\\\AppUpdatesNotification::class, // Optional: FQCN of your Laravel Notification\n            'channels' =\u003e [\n                ['name' =\u003e 'mail', 'label' =\u003e 'Email'],\n                ['name' =\u003e 'database', 'label' =\u003e 'Site Notification'],\n            ]\n        ],\n        'comment:created' =\u003e [\n            'label' =\u003e 'New Comments',\n            'description' =\u003e 'Get notified about new comments on your content.',\n            'class' =\u003e App\\\\Notifications\\\\NewComment::class, // Example\n            'channels' =\u003e [\n                ['name' =\u003e 'mail', 'label' =\u003e 'Email'],\n                ['name' =\u003e 'database', 'label' =\u003e 'Site Notification'],\n            ]\n        ],\n        // Add more notification types...\n    ],\n];\n```\n\n#### Key Configuration Options:\n\n-   **`user_model`**: The class name of your User model.\n-   **`subscription_model`**: The Eloquent model for storing subscriptions (defaults to `Humweb\\Notifications\\Models\\NotificationSubscription`).\n-   **`table_name`**: The database table for subscriptions (defaults to `notification_subscriptions`).\n-   **`pending_notifications_table_name`**: Database table for storing notifications pending digest (defaults to `pending_notifications`).\n-   **`digest_notification_class`**: The default Laravel Notification class to use for sending digests. You should create this class. It will receive the channel and a collection of pending notification data.\n-   **`digest_intervals`**: An associative array defining the available digest periods users can select (e.g., `['immediate' =\u003e 'Immediate', 'daily' =\u003e 'Daily Digest']`). Keys are used internally, values are for display.\n-   **`notifications`**: An array where each key is a unique string identifying a notification type (e.g., `app:updates`, `comment:created`).\n    -   **`label`**: A human-readable name for the notification.\n    -   **`description`**: A more detailed explanation.\n    -   **`class`**: (Optional) The FQCN of the corresponding Laravel Notification class. Useful for reference or dynamic dispatch.\n    -   **`channels`**: An array of available delivery channels. Each channel item should be an array with:\n        -   `name`: The technical identifier (e.g., 'mail', 'database').\n        -   `label`: A human-readable name for the UI.\n\n### 3. Prepare Your Notification Classes (Optional but Recommended)\n\nFor seamless integration, especially with digest preferences, your Laravel Notification classes can use two traits provided by this package:\n\n-   `Humweb\\\\Notifications\\\\Traits\\\\DispatchesNotifications`: Adds a static `dispatch()` method to your notification. This method automatically handles checking user subscriptions and either sends the notification immediately or queues it for a digest.\n-   `Humweb\\\\Notifications\\\\Traits\\\\ChecksSubscription`: Provides the `via()` method. When a notification is sent (either directly or via the `dispatch()` method from `DispatchesNotifications`), this `via()` method ensures it only goes out through channels the user is _immediately_ subscribed to. Non-immediate preferences are handled by the digest system.\n\n```php\nnamespace App\\\\Notifications;\n\nuse Humweb\\\\Notifications\\\\Contracts\\\\SubscribableNotification;\nuse Humweb\\\\Notifications\\\\Traits\\\\ChecksSubscription;\nuse Humweb\\\\Notifications\\\\Traits\\\\DispatchesNotifications;\nuse Illuminate\\\\Bus\\\\Queueable;\nuse Illuminate\\\\Notifications\\\\Notification;\n// use Illuminate\\\\Contracts\\\\Queue\\\\ShouldQueue; // If you want to queue it\n\nclass NewComment extends Notification implements SubscribableNotification //, ShouldQueue\n{\n    use Queueable, DispatchesNotifications, ChecksSubscription;\n\n    public $comment;\n\n    // Your notification constructor\n    public function __construct($comment)\n    {\n        $this-\u003ecomment = $comment;\n    }\n\n    // Required by SubscribableNotification\n    // Must match a key in your config/notification-subscriptions.php 'notifications' array\n    public static function subscriptionType(): string\n    {\n        return 'comment:created';\n    }\n\n    // Standard Laravel Notification methods\n    // The via() method is supplied by ChecksSubscription trait.\n    // It will automatically filter channels based on immediate user subscriptions.\n\n    public function toMail($notifiable)\n    {\n        return (new \\\\Illuminate\\\\Notifications\\\\Messages\\\\MailMessage)\n                    -\u003eline('A new comment was added on your post: ' . $this-\u003ecomment-\u003epost_title)\n                    -\u003eaction('View Comment', url('/posts/' . $this-\u003ecomment-\u003epost_id . '#comment-' . $this-\u003ecomment-\u003eid))\n                    -\u003eline('Thank you for using our application!');\n    }\n\n    public function toArray($notifiable)\n    {\n        return [\n            'comment_id' =\u003e $this-\u003ecomment-\u003eid,\n            'comment_body' =\u003e $this-\u003ecomment-\u003ebody,\n            // ... other data\n        ];\n    }\n}\n```\n\nImplement the `Humweb\\\\Notifications\\\\Contracts\\\\SubscribableNotification` interface, which requires a static `subscriptionType()` method. This method should return the string key that identifies this notification in your `notification-subscriptions.php` config file.\n\n### 4. Create a Digest Notification Class\n\nYou need to create a notification class that will be responsible for sending the digest. The class name is specified in `config/notification-subscriptions.php` under `digest_notification_class`.\n\nThis class will receive two arguments in its constructor: the channel it's being sent for, and a collection of pending notification data.\n\n```php\n// app/Notifications/UserNotificationDigest.php\nnamespace App\\\\Notifications;\n\nuse Illuminate\\\\Bus\\\\Queueable;\nuse Illuminate\\\\Notifications\\\\Notification;\nuse Illuminate\\\\Contracts\\\\Queue\\\\ShouldQueue;\nuse Illuminate\\\\Notifications\\\\Messages\\\\MailMessage;\nuse Illuminate\\\\Support\\\\Collection;\n\nclass UserNotificationDigest extends Notification implements ShouldQueue\n{\n    use Queueable;\n\n    public string $channel;\n    public Collection $pendingNotificationsData;\n\n    public function __construct(string $channel, Collection $pendingNotificationsData)\n    {\n        $this-\u003echannel = $channel;\n        $this-\u003ependingNotificationsData = $pendingNotificationsData;\n    }\n\n    public function via($notifiable): array\n    {\n        // Send the digest via the channel it was originally intended for\n        return [$this-\u003echannel];\n    }\n\n    public function toMail($notifiable): MailMessage\n    {\n        $mailMessage = (new MailMessage)\n            -\u003esubject('Your Notification Digest');\n\n        if ($this-\u003ependingNotificationsData-\u003eisEmpty()) {\n            $mailMessage-\u003eline('You have no new notifications in this digest period.');\n            return $mailMessage;\n        }\n\n        $mailMessage-\u003eline('Here is a summary of your notifications:');\n\n        foreach ($this-\u003ependingNotificationsData as $item) {\n            // Customize how each item in the digest is displayed\n            // $item['class'] is the original notification class\n            // $item['data'] contains the original constructor arguments for that notification\n            // $item['created_at'] is when the original notification was triggered\n            $mailMessage-\u003eline(\\\"--- ({$item['created_at']-\u003eformat('M d, H:i')}) ---\\\");\n            $mailMessage-\u003eline(\\\"Type: {$item['class']}\\\"); // Example\n            // You might want to load $item['data'] into the original notification class\n            // and call a -\u003etoDigestMail() method on it, or format data directly.\n            $dataString = implode(', ', array_map(fn($k, $v) =\u003e \\\"$k: \\\" . (is_object($v) || is_array($v) ? json_encode($v) : $v), array_keys($item['data']), $item['data']));\n            $mailMessage-\u003eline(\\\"Details: {$dataString}\\\");\n        }\n        return $mailMessage;\n    }\n\n    public function toArray($notifiable): array\n    {\n        return [\n            'message' =\u003e 'You have new notifications in your digest.',\n            'count' =\u003e $this-\u003ependingNotificationsData-\u003ecount(),\n            'items' =\u003e $this-\u003ependingNotificationsData-\u003emap(function ($item) {\n                return [\n                    'original_class' =\u003e $item['class'],\n                    'data' =\u003e $item['data'],\n                    'triggered_at' =\u003e $item['created_at']-\u003etoIso8601String(),\n                ];\n            })-\u003eall(),\n        ];\n    }\n}\n\n```\n\n### 5. Schedule the Digest Command\n\nThe package includes an Artisan command `notifications:send-digests` to process and send due digests. You should schedule this command to run periodically (e.g., every 5 or 15 minutes) in your `app/Console/Kernel.php` file:\n\n```php\n// app/Console/Kernel.php\nprotected function schedule(Schedule $schedule)\n{\n    // ...\n    $schedule-\u003ecommand('notifications:send-digests')-\u003eeveryFifteenMinutes();\n    // ...\n}\n```\n\n## Usage\n\n### Managing Subscriptions\n\nThe `Subscribable` trait adds several methods to your User model:\n\n#### Subscribing (with Digest Options)\n\nTo subscribe a user to a specific notification type, channel, and optionally specify digest preferences:\n\n```php\n$user = Auth::user();\n\n// Subscribe to 'app:updates' via 'mail', receive immediately (default)\n$user-\u003esubscribe('app:updates', 'mail');\n\n// Subscribe to 'comment:created' via 'database', receive daily at 9:00 AM\n$user-\u003esubscribe('comment:created', 'database', 'daily', '09:00:00');\n\n// Subscribe to 'newsletter:marketing' via 'mail', receive weekly on Mondays at 8:30 AM\n$user-\u003esubscribe('newsletter:marketing', 'mail', 'weekly', '08:30:00', 'monday');\n```\n\n**Parameters for `subscribe()`:**\n\n1.  `string $type`: The notification type key (e.g., `comment:created`).\n2.  `string $channel`: The channel name (e.g., `mail`, `database`).\n3.  `string $digestInterval = 'immediate'`: Optional. The digest preference.\n    -   `'immediate'`: Send as soon as it occurs.\n    -   `'daily'`: Include in a daily digest.\n    -   `'weekly'`: Include in a weekly digest.\n        (These keys should match those defined in `config('notification-subscriptions.digest_intervals')`).\n4.  `?string $digestAtTime = null`: Optional. For 'daily' or 'weekly' digests, the time of day (HH:MM:SS or HH:MM) to send the digest.\n5.  `?string $digestAtDay = null`: Optional. For 'weekly' digests, the day of the week (e.g., 'monday', 'tuesday') to send the digest.\n\nIf the user is already subscribed to that specific type and channel, their digest preferences will be updated.\n\n#### Unsubscribing from a Type and Channel\n\n```php\n$user-\u003eunsubscribe('app:updates', 'mail');\n```\n\n#### Checking Subscription Status\n\n```php\nif ($user-\u003eisSubscribedTo('app:updates', 'mail')) {\n    // User is subscribed (could be immediate or digest)\n}\n```\n\n#### Getting Full Subscription Details\n\nTo get the full details of a subscription, including digest preferences:\n\n```php\n$details = $user-\u003egetSubscriptionDetails('comment:created', 'database');\n\nif ($details) {\n    echo \"Interval: \" . $details-\u003edigest_interval;    // 'immediate', 'daily', 'weekly'\n    echo \"Time: \" . $details-\u003edigest_at_time;       // e.g., '09:00:00' or null\n    echo \"Day: \" . $details-\u003edigest_at_day;         // e.g., 'monday' or null\n    echo \"Last Digest Sent: \" . $details-\u003elast_digest_sent_at; // Carbon instance or null\n}\n```\n\nThis returns a `NotificationSubscription` model instance or `null`.\n\n#### Other Subscription Management Methods\n\n-   `$user-\u003egetSubscribedChannels(string $type)`: Get channel names for a type (any digest preference).\n-   `$user-\u003eunsubscribeFromType(string $type)`: Unsubscribe from all channels/digest settings for a type.\n-   `$user-\u003eunsubscribeFromAll()`: Unsubscribe from everything.\n-   `$user-\u003esubscriptions`: Eloquent relation to get all `NotificationSubscription` models.\n\n### Dispatching Notifications\n\nIf you've set up your Notification classes with the `DispatchesNotifications` and `ChecksSubscription` traits:\n\n```php\nuse App\\\\Notifications\\\\NewComment;\n\n$comment = // ... your comment model ...\n$userToNotify = // ... the user who should receive this (if subscribed) ...\n\n// This static dispatch method handles everything:\n// - Checks if users are subscribed to 'comment:created'\n// - If 'immediate' for a channel, sends via that channel (respecting via() from ChecksSubscription)\n// - If 'daily' or 'weekly', stores it in 'pending_notifications' table for the digest command\nNewComment::dispatch($comment);\n```\n\nThe `DispatchesNotifications::dispatch()` method will find all users subscribed to the notification's `subscriptionType()`. For each user:\n\n-   If they have an \"immediate\" subscription on any channel for this type, the notification will be sent immediately (the `ChecksSubscription::via()` method on your notification will ensure it only uses the specific immediate channels).\n-   If they have \"daily\" or \"weekly\" subscriptions, the notification details are stored in the `pending_notifications` table. The `notifications:send-digests` command will later process these.\n\nIf you are **not** using the `DispatchesNotifications` trait, you'll need to implement this logic yourself:\n\n1.  Identify users to notify.\n2.  For each user, check their subscription for the notification type and channel.\n3.  If \"immediate\", send it.\n4.  If \"digest\", store it in `pending_notifications` (see `Humweb\\Notifications\\Models\\PendingNotification` model).\n\n### Listing Available Notification Types \u0026 Channels\n\nRetrieve configured types and channels (e.g., for a settings UI):\n\n```php\nuse Humweb\\\\Notifications\\\\Facades\\\\NotificationSubscriptions;\n\n$types = NotificationSubscriptions::getSubscribableNotificationTypes();\n$availableDigestIntervals = NotificationSubscriptions::getDigestIntervals(); // Get configured digest intervals\n\n// $types will be an array like in your config\n// $availableDigestIntervals will be like ['immediate' =\u003e 'Immediate', ...]\n```\n\n## Frontend Example (Vue/Inertia)\n\nHere's an example of how you might build a notification settings page using Vue and Inertia.\n\n**Controller (`NotificationSubscriptionController.php` - example):**\n\nYou would typically create a controller to handle fetching settings and updating them.\n\n```php\n\u003c?php\n\nnamespace App\\\\Http\\\\Controllers;\n\nuse Illuminate\\\\Http\\\\Request;\nuse Illuminate\\\\Support\\\\Facades\\\\Auth;\nuse Humweb\\\\Notifications\\\\Facades\\\\NotificationSubscriptions as NotificationSettingsFacade;\nuse Inertia\\\\Inertia;\n\nclass NotificationSubscriptionController extends Controller\n{\n    public function index()\n    {\n        $user = Auth::user();\n        $notificationTypes = NotificationSettingsFacade::getSubscribableNotificationTypes();\n        $availableDigestIntervals = NotificationSettingsFacade::getDigestIntervals();\n\n        $settings = [];\n        foreach ($notificationTypes as $typeKey =\u003e $typeDetails) {\n            $channels = [];\n            foreach ($typeDetails['channels'] as $channelConfig) {\n                $subscription = $user-\u003egetSubscriptionDetails($typeKey, $channelConfig['name']);\n                $channels[] = [\n                    'name' =\u003e $channelConfig['name'],\n                    'label' =\u003e $channelConfig['label'],\n                    'subscribed' =\u003e (bool) $subscription,\n                    'digest_interval' =\u003e $subscription-\u003edigest_interval ?? 'immediate',\n                    'digest_at_time' =\u003e $subscription-\u003edigest_at_time ? substr($subscription-\u003edigest_at_time, 0, 5) : '09:00', // HH:MM\n                    'digest_at_day' =\u003e $subscription-\u003edigest_at_day ?? 'monday',\n                ];\n            }\n            $settings[$typeKey] = [\n                'label' =\u003e $typeDetails['label'],\n                'description' =\u003e $typeDetails['description'],\n                'channels' =\u003e $channels,\n            ];\n        }\n\n        return Inertia::render('Profile/NotificationSettings', [\n            'notificationSettings' =\u003e $settings,\n            'availableDigestIntervals' =\u003e $availableDigestIntervals,\n            // Example days of the week\n            'availableDaysOfWeek' =\u003e [\n                'monday' =\u003e 'Monday', 'tuesday' =\u003e 'Tuesday', 'wednesday' =\u003e 'Wednesday',\n                'thursday' =\u003e 'Thursday', 'friday' =\u003e 'Friday', 'saturday' =\u003e 'Saturday', 'sunday' =\u003e 'Sunday'\n            ],\n        ]);\n    }\n\n    public function store(Request $request)\n    {\n        $request-\u003evalidate([\n            'type' =\u003e 'required|string',\n            'channel' =\u003e 'required|string',\n            'subscribed' =\u003e 'required|boolean',\n            'digest_interval' =\u003e 'required|string|in:' . implode(',', array_keys(NotificationSettingsFacade::getDigestIntervals())),\n            'digest_at_time' =\u003e 'nullable|required_if:digest_interval,daily|required_if:digest_interval,weekly|date_format:H:i',\n            'digest_at_day' =\u003e 'nullable|required_if:digest_interval,weekly|string|in:monday,tuesday,wednesday,thursday,friday,saturday,sunday',\n        ]);\n\n        $user = Auth::user();\n\n        if ($request-\u003esubscribed) {\n            $user-\u003esubscribe(\n                $request-\u003etype,\n                $request-\u003echannel,\n                $request-\u003edigest_interval,\n                $request-\u003edigest_at_time ? $request-\u003edigest_at_time . ':00' : null, // Append seconds\n                $request-\u003edigest_at_day\n            );\n        } else {\n            $user-\u003eunsubscribe($request-\u003etype, $request-\u003echannel);\n        }\n\n        return back()-\u003ewith('success', 'Notification settings updated.');\n    }\n}\n```\n\n**Vue Component (`resources/js/Pages/Profile/NotificationSettings.vue` - example):**\n\n```html\n\u003ctemplate\u003e\n    \u003cdiv\u003e\n        \u003ch1\u003eNotification Settings\u003c/h1\u003e\n\n        \u003cdiv\n            v-for=\"(type, typeKey) in notificationSettings\"\n            :key=\"typeKey\"\n            class=\"mb-8 p-4 border rounded\"\n        \u003e\n            \u003ch2 class=\"text-xl font-semibold\"\u003e{{ type.label }}\u003c/h2\u003e\n            \u003cp class=\"text-sm text-gray-600 mb-3\"\u003e{{ type.description }}\u003c/p\u003e\n\n            \u003cdiv\n                v-for=\"channel in type.channels\"\n                :key=\"channel.name\"\n                class=\"mb-6 p-3 border-l-4 rounded\"\n            \u003e\n                \u003ch3 class=\"text-lg\"\u003e\n                    {{ channel.label }} ({{ channel.name }})\n                \u003c/h3\u003e\n\n                \u003cdiv class=\"mt-2\"\u003e\n                    \u003clabel class=\"flex items-center\"\u003e\n                        \u003cinput\n                            type=\"checkbox\"\n                            :checked=\"channel.subscribed\"\n                            @change=\"toggleSubscription(typeKey, channel)\"\n                            class=\"form-checkbox h-5 w-5 text-blue-600\"\n                        /\u003e\n                        \u003cspan class=\"ml-2 text-gray-700\"\u003eSubscribed\u003c/span\u003e\n                    \u003c/label\u003e\n                \u003c/div\u003e\n\n                \u003cdiv v-if=\"channel.subscribed\" class=\"mt-3 space-y-2 pl-4\"\u003e\n                    \u003cdiv\u003e\n                        \u003clabel\n                            :for=\"`interval-${typeKey}-${channel.name}`\"\n                            class=\"block text-sm font-medium text-gray-700\"\n                            \u003eDelivery Preference:\u003c/label\n                        \u003e\n                        \u003cselect\n                            :id=\"`interval-${typeKey}-${channel.name}`\"\n                            v.model=\"channel.digest_interval\"\n                            @change=\"updateSubscription(typeKey, channel)\"\n                            class=\"mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md\"\n                        \u003e\n                            \u003coption\n                                v-for=\"(label, key) in availableDigestIntervals\"\n                                :key=\"key\"\n                                :value=\"key\"\n                            \u003e\n                                {{ label }}\n                            \u003c/option\u003e\n                        \u003c/select\u003e\n                    \u003c/div\u003e\n\n                    \u003cdiv\n                        v-if=\"channel.digest_interval === 'daily' || channel.digest_interval === 'weekly'\"\n                    \u003e\n                        \u003clabel\n                            :for=\"`time-${typeKey}-${channel.name}`\"\n                            class=\"block text-sm font-medium text-gray-700\"\n                            \u003eTime:\u003c/label\n                        \u003e\n                        \u003cinput\n                            type=\"time\"\n                            :id=\"`time-${typeKey}-${channel.name}`\"\n                            v.model=\"channel.digest_at_time\"\n                            @change=\"updateSubscription(typeKey, channel)\"\n                            class=\"mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md\"\n                        /\u003e\n                    \u003c/div\u003e\n\n                    \u003cdiv v-if=\"channel.digest_interval === 'weekly'\"\u003e\n                        \u003clabel\n                            :for=\"`day-${typeKey}-${channel.name}`\"\n                            class=\"block text-sm font-medium text-gray-700\"\n                            \u003eDay of the Week:\u003c/label\n                        \u003e\n                        \u003cselect\n                            :id=\"`day-${typeKey}-${channel.name}`\"\n                            v.model=\"channel.digest_at_day\"\n                            @change=\"updateSubscription(typeKey, channel)\"\n                            class=\"mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md\"\n                        \u003e\n                            \u003coption\n                                v-for=\"(label, key) in availableDaysOfWeek\"\n                                :key=\"key\"\n                                :value=\"key\"\n                            \u003e\n                                {{ label }}\n                            \u003c/option\u003e\n                        \u003c/select\u003e\n                    \u003c/div\u003e\n                \u003c/div\u003e\n            \u003c/div\u003e\n        \u003c/div\u003e\n    \u003c/div\u003e\n\u003c/template\u003e\n\n\u003cscript setup\u003e\n    import { useForm } from \"@inertiajs/vue3\";\n    import { watch } from \"vue\";\n\n    const props = defineProps({\n        notificationSettings: Object,\n        availableDigestIntervals: Object,\n        availableDaysOfWeek: Object,\n    });\n\n    // Use a reactive form for settings to easily watch for changes.\n    // This is a simplified approach; for complex forms, consider structuring 'form.data' more directly\n    // or using multiple forms.\n    const form = useForm({\n        type: \"\",\n        channel: \"\",\n        subscribed: false,\n        digest_interval: \"immediate\",\n        digest_at_time: \"09:00\",\n        digest_at_day: \"monday\",\n    });\n\n    function updateSubscription(typeKey, channel) {\n        form.type = typeKey;\n        form.channel = channel.name;\n        form.subscribed = channel.subscribed; // Assumed to be true if we are updating digest prefs\n        form.digest_interval = channel.digest_interval;\n        form.digest_at_time = channel.digest_at_time;\n        form.digest_at_day = channel.digest_at_day;\n\n        // Normalize time for backend if it's just HH:MM\n        let timeToSend = channel.digest_at_time;\n        if (timeToSend \u0026\u0026 timeToSend.length === 5) {\n            // HH:MM\n            // The backend validation expects H:i, so this should be fine.\n            // The controller appends ':00' if needed for subscribe method.\n        }\n\n        form.post(route(\"notifications.subscriptions.store\"), {\n            // Assuming you have this route\n            preserveScroll: true,\n            onSuccess: () =\u003e {\n                // Maybe show a toast\n            },\n            onError: (errors) =\u003e {\n                console.error(\"Error updating subscription:\", errors);\n                // Revert optimistic updates if necessary or show error messages\n            },\n        });\n    }\n\n    function toggleSubscription(typeKey, channel) {\n        channel.subscribed = !channel.subscribed; // Optimistic update\n\n        if (!channel.subscribed) {\n            // If unsubscribing, also set digest to immediate as a default\n            channel.digest_interval = \"immediate\";\n        }\n        updateSubscription(typeKey, channel);\n    }\n\u003c/script\u003e\n```\n\nMake sure to define the route `notifications.subscriptions.store` in your `routes/web.php` (or `api.php`) pointing to your controller's `store` method.\n\n## Testing\n\n```bash\ncomposer test\n```\n\nor with coverage:\n\n```bash\nXDEBUG_MODE=coverage ./vendor/bin/pest --coverage\n```\n\n## Changelog\n\nPlease see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.\n\n## Contributing\n\nPlease see [CONTRIBUTING](.github/CONTRIBUTING.md) for details.\n\n## License\n\nThe MIT License (MIT). Please see [License File](LICENSE.md) for more information.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhumweb%2Fnotification-subscriptions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhumweb%2Fnotification-subscriptions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhumweb%2Fnotification-subscriptions/lists"}