Receiving duplicate FCM notifications on Android phone, works normally on desktop

I am making a Flask web app that uses the Google Sheets API to scan a school bus position spreadsheet and determine which section a bus is in. Then, it sends a notification with the user’s bus number, quadrant/section, and the buses it’s in between. The app works fine on desktop devices, but on Android, it sends duplicate notifications. One contains the site favicon, while the other doesn’t.

I thought this was a problem with ngrok, the tunneling service I was using to connect my phone to my laptop which is hosting the app over HTTPS, but as it turns out, connecting from a desktop device still doesn’t send duplicate notifications and works as expected, so I don’t think this is a problem with ngrok.Here is an extremely simplified version of my code, with all the irrelevant parts removed. It has the same issue as the extensive code.

Flask app:

from flask import Flask, request, jsonify, render_template, send_from_directory
import firebase_admin
from firebase_admin import credentials, messaging
from flask_cors import CORS
import os
from dotenv import load_dotenv

# Load environment variables
load_dotenv()

app = Flask(__name__,
    template_folder='templates',
    static_folder='static',
    static_url_path=''
)
CORS(app)

# Initialize Firebase Admin SDK
cred = credentials.Certificate('Core/firetoken.json')  # Your Firebase credentials file
firebase_admin.initialize_app(cred)

@app.route('/firebase-messaging-sw.js')
def sw():
    response = send_from_directory(app.static_folder, 'firebase-messaging-sw.js')
    response.headers['Content-Type'] = 'application/javascript'
    response.headers['Service-Worker-Allowed'] = '/'
    return response

@app.route('/')
def home():
    return render_template('index.html',
        firebase_config=dict(
            api_key=os.getenv('FIREBASE_API_KEY'),
            auth_domain=os.getenv('FIREBASE_AUTH_DOMAIN'),
            project_id=os.getenv('FIREBASE_PROJECT_ID'),
            storage_bucket=os.getenv('FIREBASE_STORAGE_BUCKET'),
            messaging_sender_id=os.getenv('FIREBASE_MESSAGING_SENDER_ID'),
            app_id=os.getenv('FIREBASE_APP_ID'),
            measurement_id=os.getenv('FIREBASE_MEASUREMENT_ID')
        ),
        vapid_key=os.getenv('VAPID_KEY')
    )

@app.route('/store_token', methods=['POST'])
def store_token():
    data = request.json
    token = data.get('token')
    
    if not token:
        return jsonify({'error': 'Token is required'}), 400

    try:
        # Send a test notification
        message = messaging.Message(
            notification=messaging.Notification(
                title="Test Notification",
                body="This is a test notification!"
            ),
            token=token
        )
        messaging.send(message)
        return jsonify({'status': 'Notification sent successfully'})
    except Exception as e:
        return jsonify({'error': str(e)}), 500

if __name__ == '__main__':
    app.run(debug=True)

HTML Template:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Simple Notification Test</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        .container {
            background-color: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        button {
            background-color: #4CAF50;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin: 10px 0;
        }
        #status {
            margin: 20px 0;
            padding: 10px;
            border-radius: 4px;
        }
        .success { background-color: #dff0d8; color: #3c763d; }
        .error { background-color: #f2dede; color: #a94442; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Notification Test</h1>
        <button id="send-notification">Send Test Notification</button>
        <p id="status"></p>
    </div>

    <script type="module">
        import { initializeApp } from "https://www.gstatic.com/firebasejs/11.0.1/firebase-app.js";
        import { getMessaging, getToken, onMessage } from "https://www.gstatic.com/firebasejs/11.0.1/firebase-messaging.js";

        const firebaseConfig = {
            apiKey: "{{ firebase_config.api_key }}",
            authDomain: "{{ firebase_config.auth_domain }}",
            projectId: "{{ firebase_config.project_id }}",
            storageBucket: "{{ firebase_config.storage_bucket }}",
            messagingSenderId: "{{ firebase_config.messaging_sender_id }}",
            appId: "{{ firebase_config.app_id }}",
            measurementId: "{{ firebase_config.measurement_id }}"
        };

        const vapidKey = "{{ vapid_key }}";

        try {
            const app = initializeApp(firebaseConfig);
            const messaging = getMessaging(app);

            // Register service worker
            if ('serviceWorker' in navigator) {
                navigator.serviceWorker.register('/firebase-messaging-sw.js')
                    .then(registration => console.log('Service Worker registered'))
                    .catch(err => console.error('Service Worker registration failed:', err));
            }

            document.getElementById('send-notification').addEventListener('click', async () => {
                try {
                    const permission = await Notification.requestPermission();
                    if (permission === 'granted') {
                        const currentRegistration = await navigator.serviceWorker.getRegistration();
                        const token = await getToken(messaging, { 
                            vapidKey: vapidKey,
                            serviceWorkerRegistration: currentRegistration
                        });

                        const response = await fetch('/store_token', {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify({ token: token })
                        });

                        const result = await response.json();
                        if (!response.ok) throw new Error(result.error);

                        document.getElementById('status').innerText = 'Notification sent successfully!';
                        document.getElementById('status').className = 'success';
                    } else {
                        throw new Error('Notification permission denied');
                    }
                } catch (error) {
                    document.getElementById('status').innerText = `Error: ${error.message}`;
                    document.getElementById('status').className = 'error';
                }
            });

            // Listen for messages
            onMessage(messaging, (payload) => {
                document.getElementById('status').innerText = `Received: ${payload.notification.title} - ${payload.notification.body}`;
                document.getElementById('status').className = 'success';
            });

        } catch (error) {
            console.error('Initialization error:', error);
            document.getElementById('status').innerText = `Error: ${error.message}`;
            document.getElementById('status').className = 'error';
        }
    </script>
</body>
</html>

Here is a screenshot of the problem:
Screenshot of duplicate Android notifications