Skip to content

Send and keep track of Expo push notifications with Django and Celery

License

Notifications You must be signed in to change notification settings

DoctorJohn/django-expo-notifications

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

80 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Django Expo Notifications

PyPI PyPI - Python Version PyPI - Django Version Codecov License

A Django app that allows you to keep track of devices, send Expo push notifications and check their tickets for receipts. This project uses Celery and the Expo Server SDK for Python to send push messages and check push receipts in the background.

Admin action for sending messages (light mode) Admin action for sending messages (dark mode)

Features

  • Keep track of user device tokens and preferred languages
  • Send push notifications efficiently in bulk
  • Automatically retry sending messages in case of a failure
  • Automatically keep track of message tickets
  • Automatically check push receipts
  • Automatically retry checking receipts in case of a failure
  • Automatically flag inactive devices
  • Django admin actions for sending messages and checking receipts

Installation

pip install django-expo-notifications

After installing the Django app, add it to your project's INSTALLED_APPS setting:

INSTALLED_APPS = [
    "expo_notifications",  # <-- Add this line
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]

The expo_notifications app comes with a set of models. To make them available in your database, run Django's migrate management command:

python manage.py migrate

Finally, make sure your Django project is configured to use Celery. This can be done by following Celery's First steps with Django guide.

Settings

You may optionally set the following settings in your Django project's settings module. The code snippet below shows the default values for each setting.

from datetime import timedelta


EXPO_NOTIFICATIONS_TOKEN = None

EXPO_NOTIFICATIONS_RECEIPT_CHECK_DELAY = timedelta(minutes=30)

EXPO_NOTIFICATIONS_SENDING_TASK_MAX_RETRIES = 5

EXPO_NOTIFICATIONS_SENDING_TASK_RETRY_DELAY = timedelta(seconds=30)

EXPO_NOTIFICATIONS_CHECKING_TASK_MAX_RETRIES = 3

EXPO_NOTIFICATIONS_CHECKING_TASK_RETRY_DELAY = timedelta(minutes=1)

Enhanced Security for Push Notifications

At some point, you most likely want to enable Enhanced Security for Push Notifications in your Expo account. This will require you to provide an Expo access token in your Django project's settings module like this:

EXPO_NOTIFICATIONS_TOKEN = "your-expo-viewer-access-token-here"

Check out the Expo documentation for more information.

Receipt Check Delay

Expo recommends checking the receipts of push notifications after some delay. This gives Expo time to process the push notifications and generate receipts. The default delay is based on the value used in the official Expo Server SDK for Node. Feel free to adjust this setting to your needs:

EXPO_NOTIFICATIONS_RECEIPT_CHECK_DELAY = timedelta(hours=1)

However, note that Expo only keeps ticket receipts for around a day and Celery generally prefers if tasks are not scheduled too far in the future.

Usage

The most basic usage of this app involves managing user devices and sending messages to them. Everything else (i.e. background task scheduling, retries, checking ticket receipts, deactivating devices, etc.) is handled automatically.

Quickstart

from django.contrib.auth import get_user_model
from expo_notifications.models import Device, Message


some_user = get_user_model().objects.first()

device = Device.objects.create(
    user=some_user,
    push_token="ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
)

device.messages.send(
    title="Hello, World!",
    body="This is a test message.",
)

Keeping track of user devices

The Device model represents a user device that can receive push notifications as long as it's is_active flag is True. A user may have multiple devices, however, each device must have an Expo token that is unique to that device and user.

from django.contrib.auth import get_user_model
from expo_notifications.models import Device


some_user = get_user_model().objects.first()

Device.objects.update_or_create(
    user=some_user,
    push_token="ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
    defaults={"is_active": True},
)

We recommend using update_or_create to avoid duplicate devices and to automatically reactivate existing devices if necessary.

Keeping track of device language

The Device model has an optional lang field that can be used to store the language/region code of the device.g. as per ISO 639-1). This can be useful if you want to send localized push notifications to your users.

from django.contrib.auth import get_user_model
from expo_notifications.models import Device


some_user = get_user_model().objects.first()

Device.objects.update_or_create(
    user=some_user,
    push_token="ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
    defaults={"is_active": True, "lang": "en"},
)

We recommend using ISO 639-1 and if needed, ISO 3166-1 alpha-2 codes.

Sending push notifications

The recommended way to send messages is to create and send them in bulk like this:

from expo_notifications.models import Device, Message


messages = [
    Message(device=device, title="Hello, World!")
    for device in Device.objects.active
]

Message.objects.bulk_send(messages)

Similar to bulk_send, we provide a send method that can be used to send a single message:

from expo_notifications.models import Device, Message


device = Device.objects.active.first()

Message.objects.send(device=device, title="Hello, World!")

Both bulk_send and send methods use Django's bulk_create and create under the hood and are also available on related managers:

from expo_notifications.models import Device


device = Device.objects.active.first()
device.messages.send(title="Hello, World!")

Separately create and send messages

While creating and sending messages in one go is the recommended way, you can also create messages first and send them later:

from expo_notifications.models import Message


# Sending a single message (i.e. a model instance)
single_message = Message.objects.first()
single_message.send()

# Sending multiple messages (i.e. a queryset)
multiple_messages = Message.objects.all()
multiple_messages.send()

Django Admin Actions

The expo_notifications app comes with Django admin actions that can be used to send messages and check their receipts. Note that these actions are mainly meant for troubleshooting and manual testing.

Send Selected Messages

The Send selected messages action can be used to troubleshoot your setup and test sending push notifications. The action will send the selected messages in bulk via a Celery task and schedule a task to check their receipts.

To see whether the messages were sent successfully, you can check their tickets in the Django admin interface. Messages which were rejected by Expo will have their is_success flag set to False and their error_message field may contain a rejection reason.

Check Selected Tickets

The Check selected tickets action can be used to manually check the receipts of selected messages without a delay. This action is solely meant for troubleshooting, since the app automatically schedules tasks to check the receipts of all sent messages.

To see whether a message was successfully sent to a device, you can check its ticket receipts in the Django admin interface. Messages which could not be sent to a device will have their is_success flag set to False and their error_message field may contain a reason.

Example Project

Take a look at our Django example project under tests/project. It can be run by executing these commands:

  1. uv sync
  2. uv run tests/project/manage.py migrate
  3. uv run tests/project/manage.py createsuperuser
  4. uv run tests/project/manage.py runserver

Live testing

Sending push notifications and checking their receipts can be done by using the "Send selected messages" action in the Django admin interface. In order for this to work, you need to run a Celery worker and make sure the Celery worker is authorized to communicate with Expo.

Unless the default Celery broker setup works for you (i.e. RabbitMQ on localhost), you need to provide a Celery broker url:

export CELERY_BROKER_URL="redis://localhost:6379/0"

In case you enabled Enhanced Security for Push Notifications in your Expo account, you also need to provide an Expo access token.

export EXPO_NOTIFICATIONS_TOKEN="your-expo-viewer-access-token-here"

Now you can run a Celery worker and the Django dev server in parallel to test sending push notifications and checking their receipts:

  • uv run celery --workdir tests/project --app project worker -l INFO
  • uv run tests/project/manage.py runserver