Integrando Assinaturas e Licenças Permanentes da Microsoft Store em Electron

Desafios na Integração de Comercialização via Microsoft Store

Implementar modelos de negócios baseados em assinatura ou licença perpétua em aplicações Electron distribuídas pela Microsoft Store exige uma arquitetura específica. O núcleo do problema reside na incompatibilidade entre o ambiente de execução do Electron (Node.js) e as APIs nativas de comercialização do Windows (WinRT).

O namespace Windows.Services.Store é essencial para gerenciar compras e verificar licenças, porém, ele só está acessível via código nativo. O processo principal do Electron, rodando sobre Node.js, não pode importar tipos WinRT diretamente. Além disso, o estado da licença é dinâmico: usuários podem cancelar, renovar ou alternar dispositivos, exigindo que a aplicação sincronize esses cambios sem depender exclusivamente de ação manual.

Outra complexidade envolve a coexistência de diferentes canais de distribuição. Versões portáteis ou de desenvolvimento não possuem o runtime da Store, e tentativas de chamar StoreContext nesses ambientes devem falhar graciosamente, sem travar a aplicação.

Arquitetura em Camadas

Para isolara complexidade do WinRT da lógica de negócios, adota-se uma estrutura de quatro camadas:


1. Renderizador (Frontend)
   ↕ Ponte IPC
2. Processo Principal (Electron/TypeScript)
   ↕ Interface Broker
3. Addon Nativo (C++/Node-API)
   ↕ API WinRT
4. Windows.Services.Store

A camada inferior consiste em um addon nativo compilado (.node). Ele expõe métodos assíncronos que encapsulam as chamadas RequestPurchaseAsync e GetUserCollectionAsync. O objetivo deste addon é apenas traduzir objetos COM e estruturas WinRT para JSON, enviando os resultados de volta ao thread JavaScript através de Napi::ThreadSafeFunction.

Acima disso, um serviço em TypeScript gerencia a lógica de negócios: cache, retry, derivação de权益 (entitlements) e broadcast de estado. Este serviço comunica-se com a camada nativa através de uma interface abstrata, permitindo substituir a implementação futura (ex: macOS StoreKit) sem alterar o núcleo da aplicação.

Normalização de Dados

Os dados retornados pelo WinRT são complexos e específicos da plataforma. Datas vêm como ticks do Windows (iniciando em 1601), e coleções são representadas por interfaces COM. Para evitar vazar essa complexidade para o frontend, cria-se um modelo de dados intermediário:

interface DadosBrutosLicenca {
  timestampObtencao: string;
  disponibilidade: 'suportado' | 'indisponivel' | 'erro';
  licencaAppAtiva: boolean;
  produto: DadosProduto | null;
  sku: DadosSku | null;
  licenca: DadosLicenca | null;
  elegibilidadeCompra: 'licenciavel' | 'nao_licenciavel' | 'erro_rede' | 'desconhecido';
  codigoErro: string | null;
  mensagemErro: string | null;
}

A conversão de tempo requer atenção especial. Os ticks do Windows devem ser convertidos para milissegundos Unix:

const OFFSET_EPOCH_WINDOWS_MS = 11644473600000n;
const NANOSEGUNDOS_POR_MS = 10000n;

const unixMillis =
  (ticksWindows / NANOSEGUNDOS_POR_MS) - OFFSET_EPOCH_WINDOWS_MS;

É crucial que essa conversão seja consistente entre o addon C++ e o processo principal para evitar discrepâncias de estado.

Máquina de Estado de Negócio

O frontend não deve consumir os dados brutos. Uma função de normalização traduz DadosBrutosLicenca em estados de negócio claros:

function calcularStatusNegocio(
  dados: DadosBrutosLicenca,
  config: ConfiguracaoProduto
): StatusLicenca {
  if (dados.disponibilidade !== 'suportado') {
    return 'desconhecido';
  }

  const dataExpiracao =
    dados.licenca?.dataExpiracao ?? dados.sku?.dataFimColecao ?? null;
  const tempoExpiracao = dataExpiracao ? Date.parse(dataExpiracao) : Number.NaN;
  const expirado = Number.isFinite(tempoExpiracao) && tempoExpiracao < Date.now();
  
  const possuiPropriedade = Boolean(
    dados.licenca?.ativa ||
    dados.sku?.naColecaoUsuario ||
    dados.produto?.naColecaoUsuario
  );

  if (possuiPropriedade && !expirado) {
    return 'ativo';
  }
  if (expirado) {
    return 'expirado';
  }
  // Outros estados: inativo, cancelado, periodo_carencia, pendente
  return 'inativo';
}

Para licenças perpétuas, o campo dataExpiracao pode ser nulo. A lógica deve considerar licenca.ativa como suficiente para validar o estado "ativo", garantindo que compras vitalícias não sejam invalidadas erroneamente.

Tolerância a Falhas e Retry

Instabilidades de rede não devem revogar acesso imediatamente. A estratégia recomendada é manter o último estado conhecido como "obsoleto" (stale) em caso de falha na consulta.

O serviço de licenças implementa um loop de retry com backoff. Se o estado anterior era "ativo" e a nova consulta indica "inativo", o sistema trata isso como uma regressão potencial e tenta novamente antes de aceitar a mudança.

private identificarMotivoRetry(
  atual: Snapshot,
  anterior: Snapshot | null
): 'store_indisponivel' | 'regressao_status' | null {
  if (atual.disponibilidade !== 'suportado') {
    return 'store_indisponivel';
  }
  if (anterior?.status === 'ativo' && atual.status !== 'ativo') {
    return 'regressao_status';
  }
  return null;
}

Somente após esgotar as tentativas, o snapshot é marcado como stale. O frontend pode optar por manter funcionalidades habilitadas nesse estado, priorizando a experiência do usuário pagante.

Desacoplamento de权益 (Entitlements)

O status da assinatura não deve ser verificado diretamente pelo código que habilita funcionalidades. Cria-se uma camada de avaliador de权益:

avaliar(snapshot: Snapshot): string[] {
  if (snapshot.disponibilidade !== 'suportado' || snapshot.status !== 'ativo') {
    return [];
  }
  return [...this.listaEntitlementsAtivos];
}

A configuração do produto define quais权益 são concedidos (ex: badge_patrocinador, recurso_premium). O código da aplicação verifica apenas a presença desses strings na lista, permitindo alterar modelos de assinatura sem refatorar o consumo.

Gerenciamento de Ambientes Sem Store

Para versões que não rodam dentro do ecossistema Microsoft Store, o broker deve ser substituído por uma implementação nula.

private async inicializarBroker(): Promise<brokerplataforma> {
  try {
    return this.definirBroker(
      await this.fabricaAdapter(this.handleJanela, this.configProduto)
    );
  } catch (erro) {
    // Fallback para ambiente sem runtime da Store
    return this.definirBroker(new BrokerIndisponivel(erro));
  }
}
</brokerplataforma>

O BrokerIndisponivel implementa a mesma interface, retornando sempre "não suportado". Isso previne crashes e permite exibir mensagens informativas ao usuário.

Comunicação IPC e Inicialização

A inicialização do serviço no processo principle deve ser condicional, ativada apenas via argumentos de linha de comando específicos para builds da Store.

function iniciarServicoAssinatura(): void {
  if (!recursoAssinaturaAtivo || servicoAssinatura) {
    return;
  }

  servicoAssinatura = new GestorAssinaturas({
    broker: new BrokerMicrosoftStore({
      handleJanela: janelaPrincipal?.getNativeWindowHandle() ?? null,
    }),
    avaliadorEntitlements: new AvaliadorEntitlements(),
  });

  registrarHandlersIPC({
    servicoAssinatura,
    obterJanelas: () => ElectronBrowserWindow.getAllWindows(),
  });
}

O handleJanela é crítico para que as janelas de compra da Store tenham um proprietário válido. O buffer retornado por getNativeWindowHandle() deve ser convertido corretamente para HWND no lado nativo, respeitando a precisão de 64 bits.

O renderizador acessa essas funções via preload, ouvindo eventos de mudança de estado para atualizar a UI reativamente. Um intervalo periódico no backend garante a sincronização com alterações feitas externamente no cliente da Store.

Considerações Críticas de Implementação

Segurança de Threads no Addon: Callbacks do WinRT não ocorrem no thread JS. Use Napi::ThreadSafeFunction::BlockingCall para marshalling seguro. Evite deadlocks garantindo que o thread de callback do WinRT não seja o mesmo que processa o JS.

Inicialização COM: O Electron pode já ter inicializado COM no thread principal. Envolva winrt::init_apartment em um bloco try-catch no addon para ignorar erros de inicialização duplicada.

Precisão do Handle da Janela: Ponteiros de janela em sistemas 64-bit excedem a precisão segura de number no JavaScript. Transmita o handle como Buffer ou string hexadecimal para evitar truncamento antes de converter para HWND no C++.

Isolamento de Estado: Licenças de assinatura e produtos perpétuos devem ser persistidos separadamente. Use chaves de produto distintas no armazenamento local para evitar que a atualização de um sobrescreva o cache do outro.

Refresh Pós-Compra: O resultado da compra indica apenas o sucesso da transação, não o estado atual da licença. Sempre force uma consulta de atualização (refresh) imediatamente após uma compra bem-sucedida para garantir que os权益 sejam liberados corretamente.

Tags: Electron microsoft-store winrt node-addon licenciamento

Publicado em 10-25 06:49