# ExNudge
[](https://hex.pm/packages/ex_nudge)
[](https://hexdocs.pm/ex_nudge)
[](https://github.com/Joraeuw/ex_nudge/actions)
[](https://coveralls.io/github/Joraeuw/ex_nudge)
ExNudge is a pure elixir library that allows easy multi-platform integration with web push notifications and encryption of messages described under [RFC 8291](https://www.rfc-editor.org/rfc/rfc8291.html) and [RFC 8292](https://www.rfc-editor.org/rfc/rfc8292.html)
## Features
- **RFC 8291 Compliant** - Full Web Push Protocol implementation
- **VAPID Support/RFC 8292 Compliant** - Voluntary Application Server Identification
- **AES-GCM Encryption** - Secure payload encryption
- **Concurrent Sending** - Send to multiple subscriptions simultaneously
- **Telemetry integration** - Telemetry on sent notifications
## Installation
Add `ex_nudge` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:ex_nudge, "~> 1.0"}
]
end
```
## Quick Start
### 1. Generate VAPID Keys
```elixir
keys = ExNudge.generate_vapid_keys()
IO.puts("Public Key: #{keys.public_key}")
IO.puts("Private Key: #{keys.private_key}")
```
### 2. Configure Your Application
```elixir
# config/config.exs
config :ex_nudge,
vapid_subject: "mailto:your-email@example.com",
vapid_public_key: "your_public_key_here",
vapid_private_key: "your_private_key_here"
```
### 3. Send a Notification
```elixir
# Create a subscription and store it somewhere (typically in your database)
subscription = %ExNudge.Subscription{
endpoint: "https://fcm.googleapis.com/fcm/send/...",
keys: %{
p256dh: "client_public_key",
auth: "client_auth_secret"
},
metadata: "any metadata"
}
# Send the notification
case ExNudge.send_notification(subscription, "Hello, World!") do
{:ok, response} ->
IO.puts("Notification sent successfully!")
{:error, :subscription_expired} ->
IO.puts("Subscription has expired, remove from database")
{:error, reason} ->
IO.puts("Failed to send: #{inspect(reason)}")
end
```
### Batch Sending
```elixir
subscriptions = [subscription1, subscription2, subscription3]
results = ExNudge.send_notifications(subscriptions, "Multicast message")
# Process results
results
|> Enum.each(fn
{:ok, subscription, _response} ->
IO.puts("Successfully sent notification for #{subscription.metadata}")
{:error, subscription, :subscription_expired} ->
# Remove expired subscription from database
MyApp.remove_subscription(subscription)
IO.puts("Removed expired subscription: #{subscription.metadata}")
{:error, subscription, %HTTPoison.Response{status_code: status_code}} ->
IO.puts("HTTP error #{status_code} for #{subscription.metadata}")
{:error, subscription, reason} ->
IO.puts("Failed to send to #{subscription.metadata}: #{inspect(reason)}")
end)
# You can also configure concurrency which defaults to System.schedulers_online() * 2
results = ExNudge.send_notifications(
subscriptions,
"Multicast message",
concurrency: 10
)
```
### Custom Options
```elixir
# Send with custom options
ExNudge.send_notification(subscription, message, [
ttl: 3600, # Time to live (seconds)
urgency: :high, # :very_low, :low, :normal, :high
topic: "breaking_news" # Replace previous messages with same topic
])
```
### Telemetry
```elixir
# Attach telemetry handler
:telemetry.attach("my-handler", [:ex_nudge, :send_notification], fn name, measurements, metadata, config ->
case metadata.status do
:success ->
Logger.info("Notification sent successfully",
duration: measurements.duration,
endpoint: metadata.endpoint)
:error ->
Logger.error("Notification failed",
error: metadata.error_reason,
http_status: metadata.http_status_code)
end
end, nil)
```
## Browser Integration
### JavaScript Client Code
```javascript
navigator.serviceWorker.register('/sw.js');
const permission = await Notification.requestPermission();
if (permission === 'granted') {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: 'your_vapid_public_key'
});
// Send subscription to your server
await fetch('/api/subscriptions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
}
```
### Service Worker (sw.js)
Keep in mind that sw.js, icon-192x192.png and badge-72x72.png usually are statically served files. <br>
Take a look into the [ServiceWorkerRegistration documentation](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification#options) for more details on options.
```javascript
self.addEventListener('push', event => {
const data = event.data ? event.data.text() : 'Default message';
const options = {
body: data,
icon: '/icon-192x192.png',
badge: '/badge-72x72.png',
vibrate: [100, 50, 100],
data: { url: "/" }
};
event.waitUntil(
self.registration.showNotification('App Name', options)
);
});
self.addEventListener('notificationclick', event => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data.url));
});
```
## Contributing
1. Fork the repository
2. Create a feature branch
3. Write tests for your changes
4. Ensure all tests pass: `mix test`
5. Submit a pull request