pushManager.subscribe return null on android firefox (not working at all on chrome)

i’m trying to create a simple web push notif using service worker, it works well on browsers in desktop, but on android device not working!

on firefox android: pushManager.subscribe() return null
on chrome android, it doesnt even reach to subscribe stage

index.php file:

<?php
require __DIR__ . '/vendor/autoload.php';

use MinishlinkWebPushVAPID;

// Generate VAPID keys if they don't exist
$vapidFile = __DIR__ . '/vapid.json';
if (!file_exists($vapidFile)) {
    $keys = VAPID::createVapidKeys();
    file_put_contents($vapidFile, json_encode($keys));
} else {
    $keys = json_decode(file_get_contents($vapidFile), true);
}

// Save subscription if POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    var_dump(file_get_contents('php://input'));
    var_dump(__DIR__);
    file_put_contents(__DIR__ . '/subscription.json', file_get_contents('php://input'));
    echo 'Subscription saved';
    exit;
}
?>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Push Demo</title>
</head>
<body>
  <h2>Web Push Demo</h2>
  <button id="subscribeBtn">Subscribe for Push</button>

  <script>
    const publicKey = '<?= $keys['publicKey'] ?>';

    function urlBase64ToUint8Array(base64String) {
       // Ensure correct padding
      const padding = '='.repeat((4 - base64String.length % 4) % 4);
      const base64 = (base64String + padding)
        .replace(/-/g, '+')
        .replace(/_/g, '/');

      const rawData = atob(base64);
      const buffer = new Uint8Array(rawData.length);

      for (let i = 0; i < rawData.length; ++i) {
        buffer[i] = rawData.charCodeAt(i);
      }
      return buffer;
    }

    async function subscribe() {
      try{
        const permission = await Notification.requestPermission();
        if (permission !== 'granted') {
          alert('Please enable notifications!');
          return;
        }

        const reg = await navigator.serviceWorker.register('sw.js');

        // Wait for activation if needed
        if (!reg.active) {
          await new Promise(resolve => {
            if (reg.installing) {
              reg.installing.onstatechange = () => {
                if (reg.active) resolve();
              };
            } else if (reg.waiting) {
              resolve(); // Skip waiting for user
            }
          });
        }
        console.log("SW now active. PushManager:", reg.pushManager);
        console.log("reg object:", reg);
        console.log("reg.scope: " + reg.scope);

        const existingSub = await reg.pushManager.getSubscription();
        if (existingSub) {
          await existingSub.unsubscribe();
          console.log("Already subscribed:", existingSub);
          alert("Already subscribed");
        } else {
          console.log("Not subscribed yet.");
          alert("Not subscribed yet.");
        }
  console.log("Key length: " + urlBase64ToUint8Array(publicKey).length);

        const sub = await reg.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: urlBase64ToUint8Array(publicKey)
        });
        console.log(sub);
        alert(sub);

        await fetch('', {
          method: 'POST',
          body: JSON.stringify(sub),
          headers: { 'Content-Type': 'application/json' }
        });

       const checkSub = await reg.pushManager.getSubscription();
        if (existingSub) {
          console.log("subscribed");
          alert("subscribed");
        } else {
          console.log("Not subscribed yet.");
          alert("Not subscribed yet.");
        }

      }catch (err) {
        console.error("Subscription failed:");
        console.error("Error name:", err.name);
        console.error("Error message:", err.message);
        console.error("Error stack:", err.stack);
        alert(`Subscribe failed: ${err.name} - ${err.message}`);
        return null;
      }
    }

    document.getElementById('subscribeBtn').onclick = () => {
      if ('serviceWorker' in navigator && 'PushManager' in window) {
        subscribe().catch(err => console.log('Error: ' + err));
      } else {
        alert('Push not supported');
      }
    };
  </script>
</body>
</html>

and this is sw.js file in the same directory:

// sw.js
self.addEventListener('install', e => self.skipWaiting());
self.addEventListener('activate', e => console.log('SW activated'));
self.addEventListener('push', e => {
  const body = e.data?.text() || 'No payload';
  e.waitUntil(self.registration.showNotification('Push', { body }));
});

some considerations:

a. i ran this script on a valid https website

b. i used valid VAPID and it works on desktop browsers including firefox and chrome

c. i granted permissions on android device so permissions not the issue!

if you could help me on this, im gratefull…