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.