Usar geolocalização

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

Usar geolocalização em extensões MV3

Na Web, os navegadores protegem os dados de geolocalização dos usuários exibindo um aviso solicitando que eles concedam acesso à origem específica à localização deles. O mesmo modelo de permissão nem sempre é apropriado para extensões.

Uma captura de tela da solicitação de permissão exibida quando um site solicita acesso à API de geolocalização
O prompt 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. Por isso, ele não pode ser acessado dentro de contextos de worker, como o service worker de extensão, que é a espinha dorsal das extensões do manifesto v3. No entanto, você ainda pode usar geolocation. Há algumas nuances de como e onde você o usa.

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 dentro de 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 seu documento fora da tela. Este caso não está usando o conteúdo da página, então pode ser um arquivo quase em branco. Basta ser um pequeno arquivo HTML que carrega em seu script.

offscreen.html:

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

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

Como mencionado, você precisa de um script chamado offscreen.js. Você também precisará agrupá-lo com sua extensão. Ele será a fonte das informações de geolocalização do service worker. É possível transmitir mensagens entre ele 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, agora está tudo pronto para 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',
});

Ao acessar um documento fora da tela, você precisa incluir um reason. O motivo 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á fazendo. Essas informações são usadas pelo processo de análise da Chrome Web Store para garantir que os documentos fora da tela sejam usados para uma finalidade válida.

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

.

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();
}

Agora, sempre que você quiser acessar a geolocalização do seu service worker, basta chamar:

const location = await getGeolocation()

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

Usar a geolocalização em um pop-up 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 DOM normais. Você pode acessar navigator.geolocation diretamente. A única diferença em relação aos 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 causará um erro imediato, como se o usuário tivesse rejeitado a solicitação. Confira isso no exemplo em 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 seguirão o fluxo de permissão normal. Isso significa que adicionar "geolocation" à sua "permissions" não dará a você automaticamente acesso às informações de geolocalização dos usuários. Confira isso no exemplo de script de conteúdo.