1
votes

Update from a place of mild understanding:

I have read much about Service Workers and advanced the project I'm working on far past where it was when I wrote this.

The main issue I was exploring here (getting firebase-messaging-sw.js served from the root of the domain) was solved by the ServiceWorkerView below.

Fun fact: turns out that the reason to have firebase-messaging-sw.js served from the root of the domain is that the scope of a Service Worker's control is defined by the URL. So, while:

http://whatever.com/firebase-messaging-sw.js has control over everything,

http://whatever.com/static/firebase-messaging-sw.js only has control over everything under /static/ (in this case, only the static resources served by Django). Even though the JS file is static, it needs to control pages outside that path.

A good starting point for learning about Service Workers

Original Question:

How do I get the FCM service worker (firebase-messaging-sw.js) to work in Django?

I'm using django with fcm-django.

How do I serve firebase-messaging-sw.js at the root?

Django serves static assets by default under the path “/static/“. Changing the STATIC_URL to “/“ makes firebase-messaging-sw.js available but nothing else is..

I tried a suggestion to put the following in my urls.py:

url(r'^(?!/static/.*)(?P<path>.*\..*)$', RedirectView.as_view(url='/static/%(path)s'))

While clearly a glorious use of regex magic, it turns out that the service worker can't be behind a redirect. (I guess this also has to do with the fun fact)

I wrote a view pointed to by an explicit URL in the urlconf. that works.

class ServiceWorkerView(View):
    def get(self, request, *args, **kwargs):
        return render(request, 'fcmtest/firebase-messaging-sw.js')

urls.py:

path('firebase-messaging-sw.js', views.ServiceWorkerView.as_view(), name='service_worker')

Now the url http://localhost:8000/firebase-messaging-sw.js serves up the javascript; however, Firebase complains that the content-type is text/plain

So I changed the view to:

class ServiceWorkerView(View):
    def get(self, request, *args, **kwargs):
        return render(request, 'fcmtest/firebase-messaging-sw.js', content_type="application/x-javascript")

And the error I get is:

Uncaught (in promise) TypeError: Failed to register a ServiceWorker: A bad HTTP response code (404) was received when fetching the script.

My firebase-messaging-sw.js (note none of the console.log statements print):

importScripts("https://www.gstatic.com/firebasejs/5.2.0/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/5.2.0/firebase-messaging.js");
importScripts("https://www.gstatic.com/firebasejs/5.2.0/init.js");

// Initialize Firebase
var config = {
    apiKey: "AIzaSyBOb5gh1Lry84116M1uvAS_xnKtcWUoNlA",
    authDomain: "pyfcm-5f794.firebaseapp.com",
    databaseURL: "https://pyfcm-5f794.firebaseio.com",
    projectId: "pyfcm-5f794",
    storageBucket: "pyfcm-5f794.appspot.com",
    messagingSenderId: "813943398064"
};
console.log('in service worker - before initializeApp')
firebase.initializeApp(config);
console.log('in service worker - after initializeApp')

const messaging = firebase.messaging();
console.log('in service worker - after firebase.messaging')

// if the app is in the background (user not on page)
messaging.setBackgroundMessageHandler(function(payload) {
    console.log('in service worker - in setBackgroundMessageHandler')
    const title = "Hello World";
    const options = {
        body: payload.data.status
    }
    return self.registration.showNotification(title, options);
});

and finally, my device registration page (I'm not sure how to run the javascript and then submit. form.submit() didn't work but that might be because of other things not working..? (fcm/fcmtest/templates/fcmtest/device_registration.html):

<head>
    {% load static %}
    <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>FCM Device Registration</title>

    <!-- update the version number as needed -->
    <script src="https://www.gstatic.com/firebasejs/5.2.0/firebase.js"></script>
    <script>
    // Initialize Firebase
    var config = {
    apiKey: "AIzaSyBOb5gh1Lry84116M1uvAS_xnKtcWUoNlA",
    authDomain: "pyfcm-5f794.firebaseapp.com",
    databaseURL: "https://pyfcm-5f794.firebaseio.com",
    projectId: "pyfcm-5f794",
    storageBucket: "pyfcm-5f794.appspot.com",
    messagingSenderId: "813943398064"
    };

    firebase.initializeApp(config);
    </script>

    <script defer src="https://www.gstatic.com/firebasejs/5.2.0/firebase-app.js"></script>
    <!-- include only the Firebase features as you need -->
    <script defer src="https://www.gstatic.com/firebasejs/5.2.0/firebase-auth.js"></script>
    <!--<script defer src="https://www.gstatic.com/firebasejs/5.2.0/firebase-database.js"></script>-->
    <script defer src="https://www.gstatic.com/firebasejs/5.2.0/firebase-messaging.js"></script>
    <!--<script defer src="https://www.gstatic.com/firebasejs/5.2.0/firebase-storage.js"></script>-->
    <!-- initialize the SDK after all desired features are loaded -->
    <!--<script defer src="https://www.gstatic.com/firebasejs/5.2.0/init.js"></script>-->

</head>

<body>

<script>

function deviceRegistration() {
    console.log('in deviceRegistration()')
    firebase.initializeApp(config);
    const messaging = firebase.messaging();

    // public key generated in firebase console
    messaging.usePublicVapidKey("BMEoHrnzLq5WNeyahbSxJNTyS-44bXug2wetxAWVMLMSUIQE0dexhP4pJhcSA-ZlQlneHURmYQcnq9ofym_sStY");
    // console.log('{% static "firebase-messaging-sw.js" %}')
    console.log("/firebase-messaging-sw.js")
    // changed location passed to navigator.serviceWorker.register since I am no longer serving firebase-messaging.js from localhost/static/ (see that url hack)
    // navigator.serviceWorker.register('{% static "firebase-messaging-sw.js" %}')
    navigator.serviceWorker.register("/firebase-messaging-sw.js")
    .then((registration) => {
        console.log('before messaging.useServiceWorker')
        messaging.useServiceWorker(registration);
        console.log('after messaging.useServiceWorker')

        // Request permission and get token.....
        messaging.requestPermission()
        .then(function(){
            console.log("Have Permission");
        })
        .then(function(token) {
            form = document.getElementById("registration_form");

            registration_token = messaging.getToken();
            form.registration_id = registration_token;

            var isAndroid = navigator.userAgent.toLowerCase().indexOf("android") >= -1;
            if (isAndroid) {
                form.type = "android";
            } else {
                var isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
                if (isiOS) {
                    form.type = "ios";
                } else {
                    form.type = "web";
                }
            }

            //form.submit();
        })
        .catch(function(err){
            console.log("Permission denied");
        })
    });

    // request permission to send notifications
    messaging.requestPermission()
        .then(function(){
            console.log("Have Permission");
        })
        .then(function(token) {
            form = document.getElementById("registration_form");

            registration_token = messaging.getToken();
            form.registration_id = registration_token;

            var isAndroid = navigator.userAgent.toLowerCase().indexOf("android") >= -1;
            if (isAndroid) {
                form.type = "android";
            } else {
                var isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
                if (isiOS) {
                    form.type = "ios";
                } else {
                    form.type = "web";
                }
            }

            //form.submit();
        })
        .catch(function(err){
            console.log("Permission denied");
        })

    // if user is on the page then show message directly instead of a notification
    messaging.onMessage(function(payload) {
        console.log('payload: ', payload);
    });

}

</script>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}


<form id="registration_form" method="post">{% csrf_token %}
    <input id="name" type="text" />
    <input id="type" type="hidden" />
    <input id="user" type="hidden" value="{{ user.id }}" />
    <input id="registration_id" type="hidden" />
    <input id="btn_register" type="button" value="Register" onclick="deviceRegistration();" />
</form>

</body>

I tried adding a manifest.json as per instructions on https://firebase.google.com/docs/cloud-messaging/js/client

It is currently served from my static files and is linked in the registration and send pages

The errors in my console:

Uncaught (in promise) TypeError: Failed to register a ServiceWorker: A bad HTTP response code (404) was received when fetching the script.

A bad HTTP response code (404) was received when fetching the script.

Failed to load resource: net::ERR_INVALID_RESPONSE

firebase-messaging-sw.js:3 Uncaught

(anonymous) @ firebase-messaging-sw.js:3

index.esm.js:1945 Uncaught (in promise)

Object

browserErrorMessage:

"Failed to register a ServiceWorker: ServiceWorker script evaluation failed"

code:

"messaging/failed-serviceworker-registration"

message:

"Messaging: We are unable to register the default service worker. Failed to register a ServiceWorker: ServiceWorker script evaluation failed (messaging/failed-serviceworker-registration)."

stack:

"FirebaseError: Messaging: We are unable to register the default service worker. Failed to register a ServiceWorker: ServiceWorker script evaluation failed (messaging/failed-serviceworker-registration).↵ at https://www.gstatic.com/firebasejs/5.2.0/firebase-messaging.js:1:34104"

2

2 Answers

3
votes

The issue of not being able to serve firebase-messaging-sw.js can be solved by creating a ServiceWorkerView to GET the file directly and hardcoding the URLconf.

fcm/fcmtest/views.py:

class ServiceWorkerView(View):
    def get(self, request, *args, **kwargs):
        return render(request, 'fcmtest/firebase-messaging-sw.js', content_type="application/x-javascript")

fcm/fcmtest/urls.py

app_name = 'fcmtest'
urlpatterns = [
    ...
    path('firebase-messaging-sw.js', views.ServiceWorkerView.as_view(), name='service_worker')
]

The reason to serve firebase-messaging-sw.js from the root of the domain is that the scope of a Service Worker's control is defined by the URL path. Everything under the path the Service Worker is available at is under its control.


The 404 might be due to a javascript error in device_registration.html. config is referenced but has not been assigned to.

function deviceRegistration() {
    firebase.initializeApp(config);

Also, it is not necessary (maybe even problematic) to do this with Firebase Cloud Messaging:

navigator.serviceWorker.register("/firebase-messaging-sw.js")
.then((registration) => {
    messaging.useServiceWorker(registration);
})

Instead, use firebase.initializeApp(config);


It might be a good idea to serve manifest.json at the root.


The issue with form.submit() is that the code to alter the elements in the form from the promise is incomplete. After debugging that it will work great.

0
votes

I like what Inversus has done because it will work in dev/test/prod. That being said, in production I have elected to let nginx serve the firebase-messaging-sw.js file . . .

    # Firebase Cloud Messaging (FCM) requires this file to be served from
    # your root directory
    location =/firebase-messaging-sw.js {
        root /home/ubuntu/yourpath/yourproject/FCM/;
    }