Usar geolocalização

Se você quiser receber informações de geolocalização na sua extensão do Chrome, use a mesma API da plataforma da Web do navigator.geolocation que qualquer site usaria normalmente. Este artigo existe porque as extensões do Chrome tratam a permissão para acessar dados sensíveis de maneira diferente dos sites. A geolocalização é um dado muito confidencial, por isso os navegadores garantem que os usuários estejam totalmente cientes e sob controle de quando e onde sua localização exata é compartilhada.

Usar geolocalização nas extensões MV3

Na Web, os navegadores protegem os usuários dados de geolocalização, mostrando uma solicitação para conceder a essa origem acesso específico ao local. O mesmo modelo de permissão nem sempre é apropriado para extensões.

Uma captura de tela da solicitação de permissão que aparece quando um site solicita acesso à API Geolocation
O comando de permissão de geolocalização

As permissões não são a única diferença. Como mencionado acima, navigator.geolocation é uma API DOM, ou seja, algo que faz parte das APIs que compõem os sites. Consequentemente, ela não pode ser acessada dentro de contextos de worker, como o service worker de extensão, que é a estrutura principal das extensões do Manifest V3. No entanto, ainda é possível usar geolocation. Há apenas nuances com como e onde usá-lo.

Usar geolocalização em service workers

Não há objeto navigator dentro dos service workers. Ele só está disponível dentro de contextos que têm acesso ao objeto document de uma página. Para ter acesso a um service worker, use um Offscreen Document, que fornece acesso a um arquivo HTML que você pode agrupar com sua extensão.

Para começar, adicione "offscreen" à seção "permissions" do manifesto.

manifest.json:

{
  "name": "My extension",
    ...
  "permissions": [
    ...
   "offscreen"
  ],
  ...
}

Depois de adicionar a permissão "offscreen", adicione um arquivo HTML à extensão que inclua o documento fora da tela. Este caso não usa o conteúdo da página, então pode ser um arquivo quase em branco. Ele só precisa ser um pequeno arquivo HTML que carregue em seu script.

offscreen.html:

<!doctype html>
<title>offscreenDocument</title>
<script src="offscreen.js"></script>

Salve esse arquivo na raiz do seu projeto como offscreen.html.

Como mencionado, você precisa de um script com o nome offscreen.js. Você também precisará agrupá-la com sua extensão. Será a fonte das informações de geolocalização do service worker. É possível transmitir mensagens entre ela e o service worker.

offscreen.js:

chrome.runtime.onMessage.addListener(handleMessages);
function handleMessages(message, sender, sendResponse) {
  // Return early if this message isn't meant for the offscreen document.
  if (message.target !== 'offscreen') {
    return;
  }

  if (message.type !== 'get-geolocation') {
    console.warn(`Unexpected message type received: '${message.type}'.`);
    return;
  }

  // You can directly respond to the message from the service worker with the
  // provided `sendResponse()` callback. But in order to be able to send an async
  // response, you need to explicitly return `true` in the onMessage handler
  // As a result, you can't use async/await here. You'd implicitly return a Promise.
  getLocation().then((loc) => sendResponse(loc));

  return true;
}

// getCurrentPosition() returns a prototype-based object, so the properties
// end up being stripped off when sent to the service worker. To get
// around this, create a deep clone.
function clone(obj) {
  const copy = {};
  // Return the value of any non true object (typeof(null) is "object") directly.
  // null will throw an error if you try to for/in it. Just return
  // the value early.
  if (obj === null || !(obj instanceof Object)) {
    return obj;
  } else {
    for (const p in obj) {
      copy[p] = clone(obj[p]);
    }
  }
  return copy;
}

async function getLocation() {
  // Use a raw Promise here so you can pass `resolve` and `reject` into the
  // callbacks for getCurrentPosition().
  return new Promise((resolve, reject) => {
    navigator.geolocation.getCurrentPosition(
      (loc) => resolve(clone(loc)),
      // in case the user doesnt have/is blocking `geolocation`
      (err) => reject(err)
    );
  });
}

Com isso definido, você já pode acessar o documento fora da tela no service worker.

chrome.offscreen.createDocument({
  url: 'offscreen.html',
  reasons: [chrome.offscreen.Reason.GEOLOCATION || chrome.offscreen.Reason.DOM_SCRAPING],
  justification: 'geolocation access',
});

Quando você acessa um documento fora da tela, é necessário incluir um reason. O motivo do geolocation não estava disponível originalmente. Portanto, especifique um substituto de DOM_SCRAPING e explique na seção justification o que o código está realmente fazendo. Essas informações são usadas pelo processo de análise da Chrome Web Store para garantir que documentos fora da tela sejam usados para uma finalidade válida.

Assim que tiver uma referência ao documento fora da tela, você poderá enviar uma mensagem a ele solicitando que forneça informações de geolocalização atualizadas.

service_worker.js:

const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html';
let creating; // A global promise to avoid concurrency issues

chrome.runtime.onMessage.addListener(handleMessages);

async function getGeolocation() {
  await setupOffscreenDocument(OFFSCREEN_DOCUMENT_PATH);
  const geolocation = await chrome.runtime.sendMessage({
    type: 'get-geolocation',
    target: 'offscreen'
  });
  await closeOffscreenDocument();
  return geolocation;
}

async function hasDocument() {
  // Check all windows controlled by the service worker to see if one
  // of them is the offscreen document with the given path
  const offscreenUrl = chrome.runtime.getURL(OFFSCREEN_DOCUMENT_PATH);
  const matchedClients = await clients.matchAll();

  return matchedClients.some(c => c.url === offscreenUrl)
}

async function setupOffscreenDocument(path) {
  //if we do not have a document, we are already setup and can skip
  if (!(await hasDocument())) {
    // create offscreen document
    if (creating) {
      await creating;
    } else {
      creating = chrome.offscreen.createDocument({
        url: path,
        reasons: [chrome.offscreen.Reason.GEOLOCATION || chrome.offscreen.Reason.DOM_SCRAPING],
        justification: 'add justification for geolocation use here',
      });

      await creating;
      creating = null;
    }
  }
}

async function closeOffscreenDocument() {
  if (!(await hasDocument())) {
    return;
  }
  await chrome.offscreen.closeDocument();
}

Então agora, sempre que você quiser receber a geolocalização do seu service worker, basta chamar:

const location = await getGeolocation()

Usar geolocalização em um pop-up ou painel lateral

Usar a geolocalização em um popup ou painel lateral é muito simples. Pop-ups e painéis laterais são apenas documentos da Web e, por isso, têm acesso às APIs do DOM normais. Você pode acessar navigator.geolocation diretamente. A única diferença dos sites padrão é que você precisa usar o campo manifest.json "permission" para solicitar a permissão "geolocation". Se você não incluir a permissão, ainda terá acesso a navigator.geolocation. No entanto, qualquer tentativa de usá-lo vai causar um erro imediato, como se o usuário rejeitasse a solicitação. Saiba mais no exemplo de pop-up.

Usar geolocalização em um script de conteúdo

Assim como um pop-up, um script de conteúdo tem acesso total à API DOM. No entanto, os usuários passarão pelo fluxo normal de permissões. Isso significa que adicionar "geolocation" ao seu "permissions" não dará a você automaticamente acesso ao conjunto de dados de geolocalização. Saiba mais no exemplo de script de conteúdo.