Skip to content

Guia de Desenvolvimento Local & Setup (DX)

Guia de Desenvolvimento Local (Developer Experience)

Section titled “Guia de Desenvolvimento Local (Developer Experience)”

Este guia orienta engenheiros e agentes de IA sobre como configurar o ambiente de desenvolvimento local do Prime Crown, rodar a aplicação com emulação serverless de borda e executar os comandos de verificação de qualidade.

O Vitest preserva os excludes padrão e ignora **/.claude/worktrees/**. Ferramentas desktop podem manter checkouts completos nessa pasta; eles não pertencem ao checkout ativo e não devem duplicar a suíte nem disputar os workers do teste principal.

Execute npm run security:audit durante a preparação de releases e ao atualizar dependências. A baseline de agosto de 2026 usa Hono 4.12.33+, jsPDF 4.2.1+, Resend 6.18.1+, Vite 6.4.3+, Wrangler 4.118+, Sharp 0.35.3+ e resolve o Nanoid transitivo em 3.3.18+, sem advisories conhecidos pelo npm. O override de uuid existe apenas para corrigir a dependência transitiva antiga de xcode usada pelo Capacitor CLI; remova-o quando o pacote upstream passar a exigir uuid >=11.1.1. Não use npm audit fix --force, pois a sugestão atual rebaixa o Capacitor CLI e rompe o alinhamento 8.5.

Execute npm run pwa:doctor antes de publicar. O diagnóstico valida os dois manifestos instaláveis, ícones comuns e maskable, atalhos contra o ViewState real, registro/lifecycle do service worker, isolamento do boot offline, política de atualização e cache, rotas Cloudflare, App/Universal Links, viewport acessível, fonte local e configuração pública VAPID. A chave VAPID_PRIVATE_KEY é um secret externo e aparece separadamente; npm run pwa:doctor -- --strict deve ser usado no pipeline de release que possui esse secret.

As superfícies públicas de pagamento, extrato, proposta de agenda e avaliação devem renderizar exatamente uma região principal em todos os estados. Loading, erro/URL inválida e conteúdo usam <main> como raiz da viewport; status e alert pertencem ao texto anunciado, pois aplicá-los na raiz substituiria a semântica de landmark. O pwa:doctor protege esse contrato para VoiceOver, TalkBack e leitores de tela desktop.

Textos compactos de loading não usam animate-pulse: a redução de opacidade derruba o contraste mesmo quando a cor-base parece aceitável. Boot, atualização, loader compartilhado, histórico de notificações, metadados do dashboard, cabeçalhos, barra lateral e navegação inferior usam slate-600 estável sobre fundos claros; spinners, barras e ícones continuam responsáveis pela indicação visual de atividade. Não substitua o conteúdo visível de cartões ou itens expandidos por um aria-label resumido: deixe o próprio conteúdo formar o nome acessível e preserve os contadores visíveis nele. Badges LOCAL/DEV usam pares de cores AA para que auditorias locais não escondam defeitos reais em ruído do ambiente.

Formulários expandidos de autenticação e metadados dos portais públicos também seguem esse piso. Não aplique opacity-* ao contêiner de um estado histórico/desabilitado que possua texto; use superfície, borda e peso para comunicar a hierarquia sem reduzir o contraste de todos os descendentes.

public/service-worker.js deve conservar prime-crown-__BUILD_ID__; não incremente versões manualmente. O plugin de build substitui o token somente no dist/, usando o shell emitido e o template do worker como entrada. Após npm run build, dist/service-worker.js precisa conter um identificador hexadecimal de 12 caracteres e nenhum __BUILD_ID__.

Use npm run platforms:doctor para executar de uma vez os contratos PWA, Android e iOS. Os diagnósticos retornam sucesso quando o repositório está pronto e listam credenciais externas ausentes separadamente; os modos --strict de cada plataforma ficam reservados ao ambiente de release que possui essas credenciais. O reconhecimento do registro do service worker é tolerante à formatação e coberto pela execução real do doctor na suíte, evitando que um falso negativo impeça Android e iOS de serem avaliados. npm run check inclui a auditoria de dependências e falha a partir de severidade moderada, além de tipos, lint, código morto, testes e orçamento do bundle.

Depois de qualquer migração remota, execute também npm run db:audit:remote. O comando compara tabelas, colunas e índices do D1 com db/schema.sql; uma migração marcada como aplicada não prova ausência de drift quando uma reconstrução anterior de tabela removeu índices. A migração 0126 documenta esse caso e restaura os três índices por operações aditivas idempotentes.

Não adicione orientation aos manifestos PWA nem android:screenOrientation à activity principal. A aplicação compartilha layouts responsivos e deve acompanhar rotação, split-screen, tablets e dobráveis. O Info.plist deve continuar contendo retrato e paisagem; PWA, Android e iOS doctors verificam esse contrato.

Plus Jakarta Sans é carregada localmente por @fontsource-variable/plus-jakarta-sans/wght.css. Não reintroduza links para Google Fonts no index.html nem URLs de fonte em STATIC_ASSETS: uma requisição externa dentro de cache.addAll() torna a instalação offline inteira dependente desse host. O Vite deve emitir os .woff2 com hash junto do aplicativo.

O primeiro install chama precacheAppShell: guarda o HTML nas chaves / e /index.html, lê do próprio documento somente os assets iniciais e segue url() apenas no CSS para incluir fontes. Não substitua isso por uma lista manual de hashes nem por precache de todo dist/assets; hashes mudam a cada build e chunks de rotas lazy devem continuar sob demanda. tests/sw_app_shell.test.ts e o PWA doctor protegem o contrato estrutural.

O manifesto principal abre o sistema e oferece atalhos para LIVE_TRACKER e SCHEDULE; o manifesto de chat mantém escopo próprio em /chat-standalone. Alterar um destino exige usar um ViewState existente e preservar a validação RBAC na entrada — atalhos não concedem acesso a uma função protegida.

Mantenha os start_url canônicos (/ e /chat-standalone). A condição de standalone pertence a src/shared/platform/runtime.ts e deve usar apenas sinais reais do navegador/shell; query strings não podem simular uma instalação, pois o gate de campo depende dessa distinção. O prompt único do Chromium deve passar por src/shared/pwa/installPrompt.ts, inclusive quando capturado antes do mount React.

Ao criar modais ou folhas, reutilize AccessibleBackdrop em vez de adicionar onClick a uma div. O componente fornece semântica de botão, nome acessível e teclado nativo sem alterar o comportamento de toque no PWA, Android ou iOS. Operações que não podem ser interrompidas devem passar disabled ao backdrop.

Para um overlay modal completo, prefira AdaptiveBottomSheet: ele fornece diálogo nomeado, foco inicial/restaurado, contenção de Tab e integração com a pilha. Sempre forneça title, mesmo com hideHeader ou headerContent; no cabeçalho padrão, título e subtítulo visíveis são associados automaticamente por aria-labelledby e aria-describedby. Não implemente listeners de Escape por modal; somente a camada superior deve fechar. Durante salvamento ou envio indivisível, passe closeDisabled para desativar todos os caminhos de fechamento em conjunto.

Para menus ancorados, use usePopoverMenu em vez de tratá-los como diálogo. Mantenha uma única referência para o acionador, marque-o com aria-haspopup="menu", aria-expanded e aria-controls, e aplique role="menu"/role="menuitem" à superfície e às ações. O hook centraliza foco inicial, setas, Escape, pressão externa e retorno de foco; o estado da funcionalidade continua responsável por fechar o menu depois de executar uma ação.

Não use window.confirm, window.alert ou seus equivalentes globais em fluxos visíveis. Para confirmação, aguarde useConfirmation()({ title, message, confirmLabel, tone }); a promessa retorna false ao cancelar, fechar, pressionar Escape/Android Back ou substituir a solicitação. Para validação não destrutiva, use o sistema de toast existente.

Editores de configuração devem usar settingsDraftStore: rascunhos são memória de sessão, separados por usuário e sobrevivem à desmontagem da rota. Leia-os pelos seletores que recebem ownerId; não selecione state.values diretamente, pois a validação síncrona do proprietário evita um render com dados da conta anterior. syncCleanValues nunca deve substituir um grupo sujo; chame clearDirty somente depois que a persistência remota confirmar sucesso. O UnsavedSettingsGuard global registra beforeunload apenas enquanto existir um grupo sujo e cuida do encerramento de sessão, portanto não adicione listeners locais à página.

Não grave document.body.style.overflow diretamente. Overlays que não usam AdaptiveBottomSheet, como o lightbox, devem adquirir e liberar acquireBodyScrollLock(document); a contagem compartilhada impede que uma camada libere a rolagem pertencente a outra e preserva o estilo anterior.

Inicializações globais dependentes de hardware devem verificar capacidade antes de registrar listeners. Háptica delegada só é ligada para shells Capacitor ou runtimes com navigator.vibrate; Web Audio só aguarda o primeiro pointerdown/keydown quando existe AudioContext. Não restaure um listener paralelo de touchstart: Pointer Events já cobre toque no Android WebView, iOS 15+, PWA e navegadores suportados.

Uma superfície full-screen que não usa AdaptiveBottomSheet, mas deve ser fechada pelo Android Back, precisa registrar seu callback em ModalStackContext enquanto estiver visível. Use uma ref para manter o callback atual sem remover e reinserir a camada em cada render. A ordem nativa é camada superior → agendamento/painéis do shell → histórico de telas → saída.

Em Suspense que carrega um modal, use ModalLoader com um rótulo contextual. Não replique um overlay com spinner: o componente compartilhado já anuncia o estado ocupado sem expor o ícone decorativo ao leitor de tela.

Modais globais raramente abertos devem combinar React.lazy, renderização condicional pelo estado aberto e ModalLoader; apenas declarar lazy e montar o componente fechado ainda pode iniciar o download. Preserve eager os gates de segurança e bridges necessários no boot. A reserva global em App.tsx é a referência desse padrão.

No fluxo de instalação PWA, separe captura de apresentação: o listener de beforeinstallprompt e seu cache devem permanecer eager porque o evento não é repetido, enquanto PWAInstallPrompt pode ser carregado depois do boot em período ocioso. Não monte essa interface para funções atendidas pelo MobileRequirementGate; scheduleIdleWork fornece fallback e cancelamento nos navegadores sem requestIdleCallback.

Não replique regex de iPhone/iPad nos componentes. Use DeviceService.isIOS(): o Safari do iPadOS pode expor platform=MacIntel, e a combinação com maxTouchPoints é necessária para mostrar as instruções de Add to Home Screen sem classificar um Mac desktop como iPad.

Pesquisas com navegação por setas devem combinar combobox, aria-controls e aria-activedescendant com uma listbox cujas opções exponham aria-selected. Antes de aplicar módulo ou resto ao índice, trate a lista vazia explicitamente; % 0 produz NaN e quebra a próxima seleção por teclado.

Passe o texto do campo pela propriedade label de CFSelect; o componente gera e associa o ID automaticamente. Controles selecionáveis ou expansíveis devem ser elementos <button type="button"> com aria-pressed/aria-expanded, evitando funções ARIA sobre div. SVGs redundantes com texto ou legenda visível devem declarar aria-hidden="true"; visualizações sem equivalente textual precisam de nome acessível em vez de serem ocultadas.

CFInput também gerencia ID, rótulo e anúncio de erro; não duplique o rótulo fora do componente. Conjuntos de escolhas devem usar fieldset/legend ou um controle segmentado com aria-pressed. Em cartões que possuem uma ação principal e ações secundárias, mantenha botões irmãos em vez de aninhar botões ou depender de propagação de clique.

Não use <label> como título visual de seção. Todo label deve apontar para exatamente um controle por htmlFor/id; grupos usam fieldset/legend, e cabeçalhos comuns usam <p> ou heading apropriado. Entradas relacionadas, como data e hora de afastamento, precisam de nomes acessíveis próprios.

Componentes opcionalmente acionáveis devem alternar a semântica: renderize <button type="button"> somente quando houver callback e mantenha <div> quando o conteúdo for informativo. Em pickers estilizados, prefira o próprio input[type=date|time] cobrindo a área visual e forneça aria-label; isso preserva o seletor nativo nas três plataformas e teclado no desktop.

Listas de seleção arrastáveis precisam de alternativa de teclado: use listbox/option, aria-selected, foco e Enter/Espaço, mantendo os eventos de drag como conveniência adicional. Em cartões expansíveis com ações internas, o cabeçalho deve ser um botão irmão das ações secundárias; nunca dependa de stopPropagation sobre uma área estática.

Quando uma linha de tabela inteira abre detalhes e não pode ser substituída por um botão sem quebrar a estrutura HTML, torne-a focável, forneça aria-label e use activateOnEnterOrSpace de src/shared/keyboardActivation.ts. O helper preserva Enter/Espaço e ignora eventos de controles filhos; mantenha também um estado focus-visible explícito. Se a linha for condicionalmente acionável, foco e ativação devem ser condicionais pelo mesmo predicado.

Button e CFButton são não-submissores por padrão (type="button"). Todo botão que confirma um <form> precisa declarar type="submit" explicitamente; não dependa do default do HTML, pois uma futura recomposição pode transformar uma ação secundária em envio acidental.

O gate completo de acessibilidade pode ser reproduzido com npx biome lint --max-diagnostics=none --only=a11y src; a baseline atual é zero em todo o diretório, e as regras recomendadas também integram npm run lint. Mantenha ações de perfil, telefone, e-mail, chat, upload e preview como controles separados e nomeados. Supressões só são aceitas junto ao elemento, com justificativa concreta e uma alternativa acessível — atualmente isso cobre as regiões de drop nativo e a prévia local de voz ainda não enviada.

Movimento, toque e foco acessível são contratos globais em src/index.css. Não contorne prefers-reduced-motion com animação inline ou APIs JavaScript sem consultar a preferência do sistema. Controles usados em touch herdam touch-action: manipulation somente sob (hover: none) and (pointer: coarse), sem remover hover ou seleção no desktop. Um fallback :focus-visible prevalece sobre outline-none para navegação por teclado, e forced-colors usa a paleta de alto contraste do sistema. npm run pwa:doctor falha se qualquer uma dessas políticas desaparecer.

Não esconda uma ação somente com opacity-0 group-hover:opacity-100: touch não possui hover persistente. Renderize a ação visível por padrão e aplique [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover:opacity-100; reserve o padrão simples para decoração marcada com aria-hidden. Ações icon-only precisam de nome contextual e alvo de aproximadamente 44 px. O doctor inventaria também todos os usos de Button/CFButton e exige children visíveis ou aria-label/aria-labelledby.

Botões HTML com dimensões explícitas menores que 44 px ou padding compacto devem usar touch-target; a classe só aumenta o mínimo sob (hover: none) and (pointer: coarse), portanto não dilata o desktop. tests/touch_targets.test.ts percorre a AST de src/**/*.tsx e audita h-*, w-*, min-h-*, min-w-* e suas variantes importantes de 4–40 px, falhando para qualquer novo controle compacto descoberto; mantenha assertions explícitas para CTAs de banners, logout móvel e outros tamanhos que emergem da composição. Confirme a geometria renderizada em viewport móvel quando alterar shells globais. data-touch-target-exempt não é uma saída genérica: somente razões enumeradas no teste, acompanhadas de uma alternativa touch ou de uma superfície comprovadamente desktop, são aceitas.

Quando um botão representa uma escolha persistente, exponha aria-pressed={selected}; não deixe o estado existir somente na classe condicional. Para abas, use um role="tablist", botões role="tab" e aria-selected; para conteúdo recolhível, use aria-expanded. tests/selection_state_semantics.test.ts mantém o inventário dos seletores e disclosures compartilhados entre web, PWA e shells nativos.

Controles segmentados compactos, como os filtros da Inbox e do Directory, combinam aria-pressed com touch-target: a primeira propriedade comunica a escolha e a segunda amplia somente a geometria touch. Preserve uma única fonte booleana para o estilo visual e o estado acessível.

Uma tablist também precisa de navegação, não apenas roles: mantenha roving tabIndex, aceite ArrowLeft/Right/Up/Down e Home/End, mova foco junto com a seleção e associe aba/painel por aria-controls, tabpanel e aria-labelledby. Prefira CFTabs; layouts específicos, como o dashboard do chat, devem preservar exatamente esse contrato.

Para combobox + listbox, mantenha o foco no input com aria-activedescendant e remova opções do fluxo de Tab; ao filtrar, normalize o índice e mantenha a opção ativa visível. Listboxes focáveis usam um único option com tabIndex=0, setas/Home/End para foco e Enter/Espaço para seleção. Não deixe dezenas de opções virarem dezenas de paradas sequenciais no teclado ou switch control.

Menus ancorados devem usar usePopoverMenu e fornecer onOpen, onClose, ref do gatilho e ref do menu. O hook centraliza clique externo, Escape com retorno de foco, Tab sem captura, abertura por Arrow Down/Up, foco inicial da opção marcada, navegação circular/Home/End e busca incremental pelo texto visível. O typeahead normaliza caixa/acentos, acumula caracteres por 700 ms e interpreta repetição da mesma inicial como ciclo; não capture Espaço nem combinações com modificadores. Não crie listeners documentais paralelos em cada componente; bottom sheets mobile continuam usando o lifecycle modal próprio.

A rota permanente /slides usa capturas reais do aplicativo armazenadas em public/slides/img. Para atualizá-las, inicie o frontend em http://localhost:3000 e execute o utilitário pertinente em scripts/capture-slide-*.mjs ou scripts/capture-wizard-shots.mjs com Node. Eles abrem o Google Chrome do macOS em modo headless, com viewport móvel e perfil temporário próprio em /tmp, sem reutilizar o perfil pessoal do navegador. Alguns fluxos dependem dos dados demonstrativos locais e da API local disponível.

Não trate title como nome acessível: ele é apenas tooltip para mouse. Um botão/anchor só de ícone deve manter title opcionalmente e adicionar aria-label ou aria-labelledby; o ícone recebe aria-hidden. O doctor analisa conteúdo textual e falha quando um dos controles com tooltip não possui texto real nem nome ARIA. Padrões nomeados de hover como group-hover/msg também são auditados. Uma exceção precisa oferecer alternativa touch equivalente — como o scroll horizontal nativo que substitui as setas desktop do seletor de datas.

Campos de formulário recebem font-size: 16px somente abaixo de 768 px para evitar o zoom automático do Safari/WKWebView. Não mova essa regra para o desktop nem remova os atributos name/autocomplete dos fluxos de autenticação: eles são o contrato com preenchimento automático, Keychain e gerenciadores de senha nativos. O PWA doctor também protege a regra móvel.

Audite useButtonType por contexto, nunca com autofix global. Confirme primeiro se o arquivo contém <form>: ações comuns usam type="button", o botão que confirma o formulário usa type="submit", e resets intencionais usam type="reset". A auditoria está limpa e deve continuar sendo usada como verificação focada de regressão.

O aplicativo Android usa Capacitor 8, JDK 21 e Android SDK 36. O PWA continua sendo o build principal; a sincronização Android copia o dist/ para o projeto nativo e atualiza plugins/configuração. android:doctor deriva dos metadados instalados todo plugin com suporte Android e exige correspondência exata tanto no Settings Gradle quanto nas dependências do app, além de conferir a configuração copiada. Esquecer npm run android:sync, manter módulo removido ou perder um vínculo agora bloqueia o gate. Em execução, o Android abre https://app.primecrowncleaning.co.uk: essa origem é necessária para as APIs relativas, cookies HttpOnly, OAuth e passkeys continuarem iguais aos do PWA.

O alvo iOS vive em ios/, requer macOS com Xcode e usa o mesmo bundle ID uk.co.primecrowncleaning.app, origem HTTPS, SPA e providers nativos do Android. A versão mínima inicial é iOS 15. Sincronize sempre depois do build web:

Terminal window
npm run ios:sync # build web + cap sync ios
npm run ios:open # abre ios/App/App.xcodeproj no Xcode
npm run ios:run # sincroniza e executa em simulador/device
npm run ios:doctor # audita projeto, Xcode e configuração externa

ios:doctor retorna sucesso quando a configuração versionada do repositório está íntegra e lista separadamente o que falta no ambiente. Em CI de release use npm run ios:doctor -- --strict para exigir também Xcode completo e as credenciais Apple disponíveis no processo.

O target informa que usa apenas criptografia isenta (HTTPS) e não carrega o requisito legado armv7; ios:doctor também valida bundle ID, versão/build inicial, deployment target, APNs de produção, zoom acessível, proteção do app switcher e todas as chaves de privacidade exigidas pelos plugins instalados. O doctor deriva o grafo de plugins iOS dos metadados das dependências instaladas, compara todos os caminhos com CapApp-SPM/Package.swift e confere a configuração copiada e a quantidade de classes nativas; assim, esquecer npm run ios:sync passa a bloquear o gate. O plugin de geolocalização exige NSLocationAlwaysAndWhenInUseUsageDescription por sua implementação interna, mas o aplicativo solicita somente localização durante o uso e não declara modo de localização em background.

A WebView iOS ativa limitsNavigationsToAppBoundDomains e o Info.plist limita o bridge Capacitor a app.primecrowncleaning.co.uk. Conteúdo externo deve passar por externalBrowserProvider, que abre o navegador do sistema no nativo. Privacidade, Termos, WhatsApp, Google Maps e Google Calendar usam esse caminho. Anexos autenticados passam por fileDeliveryProvider, que preserva o link normal no PWA e usa cache temporário + share sheet no Android/iOS. Para arquivos gerados, consuma o retorno shared | downloaded | cancelled; PDF pode retornar também failed. Cancelamento da folha não é exceção nem sucesso e não deve acionar fallback, háptico ou toast positivo. Enquanto gerar, desabilite a ação e publique loading/aria-busy; controles revelados por hover precisam permanecer visíveis e ter ao menos 44 px quando (hover: none).

Exports de listas devem receber a coleção já filtrada que o usuário vê, não reler a store inteira. Ações icon-only precisam de aria-label; Button/CFButton reutilizam esse rótulo na mensagem “in progress” quando não há children. Em Team & Clients, mantenha import/export restritos a ADMIN e preserve o loading do export até download/share/cancelamento terminar.

Decisões de plataforma no shell devem consultar getRuntimePlatform()/DeviceService antes do user-agent. O user-agent continua permitido apenas como fallback para navegador/PWA. Isso é especialmente importante no iPad, que pode se apresentar como macOS, e nas instruções de recuperação: o iOS nativo aponta para Settings → Prime Crown, enquanto Safari/PWA aponta para as permissões do navegador.

Ao alterar statusBarProvider, derive a cor dos glifos da luminância de primaryColor quando ela existir. themeMode descreve o conteúdo e só pode ser fallback sem cor explícita; não o use para sobrescrever o contraste do chrome que o usuário realmente vê.

Todo provider que faz import() + addListener() assíncrono precisa cobrir três caminhos: desmontagem antes do import, desmontagem enquanto addListener resolve e rejeição em qualquer etapa. Use flag active, remova imediatamente handles tardios e finalize cadeias fire-and-forget com catch; o NativeBridge, teclado e rede são as referências desse contrato.

Providers que registram vários listeners sequenciais também precisam de rollback transacional. Siga initializeNativePush: acumule handles somente após cada await, envolva o restante em try/catch, aguarde a remoção dos anteriores antes de relançar e use Promise.allSettled no disposer para que uma falha não interrompa os demais cleanups.

No fluxo flexível do Google Play, o listener existe depois de addListener e antes de startFlexibleUpdate; portanto o catch da inicialização deve removê-lo. Estados DOWNLOADED, CANCELED e FAILED são terminais e também precisam de cleanup. Preserve o fallback App Store do iOS separado dos status exclusivos do Play.

PrivacyInfo.xcprivacy faz parte dos Resources do target, declara ausência de tracking, os usos de UserDefaults (CA92.1) e metadados de arquivos no contêiner (C617.1), além das categorias coletadas já descritas na política pública. Todas estão vinculadas à conta, não são usadas para tracking e têm finalidade de funcionamento do aplicativo. O questionário App Privacy no App Store Connect deve reproduzir esse manifesto; não marque categorias como publicidade ou analytics sem uma mudança correspondente no produto, na política e neste arquivo.

No Xcode, selecione o Apple Developer Team em Signing & Capabilities. Antes de instalar em aparelho ou arquivar para TestFlight, habilite Push Notifications e mantenha Associated Domains com applinks:app.primecrowncleaning.co.uk e webcredentials:app.primecrowncleaning.co.uk. O domínio publica /.well-known/apple-app-site-association; configure APPLE_TEAM_ID no Cloudflare com o Team ID de 10 caracteres. O arquivo fica indisponível com status 503 enquanto essa variável não existir, evitando publicar uma associação inválida.

Valide deep links com o app instalado e encerrado, em background e aberto. Os formatos suportados incluem https://app.primecrowncleaning.co.uk/chat?channelId=..., primecrown://chat?channelId=... e primecrown://open/?view=CHAT&channelId=.... Links HTTPS de domínio diferente e schemes desconhecidos devem ser rejeitados. O badge de não lidas usa o plugin nativo tanto no ícone iOS quanto no launcher Android, com fallback para a App Badge API do PWA.

Não chame o plugin de badge diretamente. updateAppBadge serializa as escritas e useNativeAppBadge limpa no teardown da sessão; chamadas paralelas fora desse contrato podem fazer uma contagem antiga vencer a mais recente. Mantenha a normalização equivalente em public/service-worker.js, pois ele atualiza o ícone quando a aplicação está fechada.

Antes de enviar para App Review, crie uma conta de demonstração ativa com acesso representativo e informe usuário, senha e instruções nas Review Notes do App Store Connect; não versione essas credenciais. O backend e os recursos revisados precisam permanecer disponíveis durante toda a análise. As notas devem indicar onde testar login Apple/Google, exclusão de conta em Profile, notificações, câmera, voz e localização contextual, deixando explícito que não há localização em background. Valide o archive via TestFlight em iPhone físico e confira que os links /privacy, /terms e /account-deletion estão publicados.

Depois que o registro do aplicativo receber seu Apple ID numérico, valide também Profile/Dashboard → New version available → Open App Store num build anterior distribuído pelo TestFlight. A consulta de versão usa o bundle ID; sinais simultâneos de mount/resume são serializados e colapsados em uma única repetição final. O Android mantém atualização imediata/flexível pelo Google Play e o PWA usa o service worker.

Info.plist já declara os textos de privacidade para câmera, localização em uso e microfone, além do scheme primecrown://. O fluxo atual não declara localização em background. AppDelegate.swift encaminha sucesso/falha do registro APNs ao plugin Capacitor e o target declara Push Notifications; o entitlement escolhe development no Debug e production no Release. O backend envia esse token diretamente ao APNs com APPLE_TEAM_ID, APNS_KEY_ID e APNS_PRIVATE_KEY; APNS_USE_SANDBOX=true deve existir apenas no ambiente usado por builds locais do Xcode, pois TestFlight/App Store usam produção. O ícone e a launch screen derivam da mesma coroa azul pelo script node scripts/generate-brand-icons.mjs. Para distribuição ainda serão necessários o App Store Connect record, certificados/provisioning e cliente OAuth iOS. Esses artefatos ligados à conta Apple não devem ser inventados nem versionados com segredo.

O teste de push no Profile envia imediatamente no aplicativo nativo; não aguarda o usuário colocar o app em background, pois Android/iOS podem suspender o timer da WebView e impedir que o POST aconteça. Em foreground a entrega pode chegar ao listener sem banner do sistema, mas o resultado do transporte é mostrado no app. O POST termina em até 15 segundos e os saltos FCM/APNs/Web Push possuem timeout no servidor. Mensagens de erro permanecem neutras ao transporte: Android usa FCM, iOS usa APNs e PWA usa Web Push.

Terminal window
npm run android:sync # build web + cap sync android
npm run android:open # abre o projeto no Android Studio
npm run android:run # sincroniza e executa em device/emulador

Use sempre npm run android:sync como última sincronização antes do Gradle. O build da documentação também grava em dist/; portanto, executar npm run docs:build seguido diretamente de npx cap sync android copiaria o site de documentação para dentro do aplicativo.

No Android Studio, configure o Gradle JDK como JDK 21. Em linha de comando, quando o Java global não for compatível, passe o JDK sem alterar a configuração do sistema:

Terminal window
cd android
./gradlew -Dorg.gradle.java.home=/caminho/para/jdk-21 bundleDebug

O package ID inicial é uk.co.primecrowncleaning.app, o target é API 36 e o bundle de debug é gerado em android/app/build/outputs/bundle/debug/. A build publicada registra o mesmo service worker também no runtime Capacitor porque o shell abre a origem remota canônica. Ele oferece continuidade de leitura com o último boot durante perdas breves de sinal, mas mutações continuam dependendo das filas explicitamente implementadas (por exemplo, chat) e não devem ser prometidas como suporte offline irrestrito. Ao alterar o cache de /api/boot, preserve a chave canônica e a limpeza em 401/403, login, logout e impersonação para não compartilhar dados entre contas.

O login Google nativo exige que o app Android desse package ID esteja registrado no Firebase/Google Cloud com o SHA-1 de cada certificado usado para instalar o aplicativo. Cadastre ao menos a chave de desenvolvimento/upload para testes locais e a chave de Google Play App Signing para instalações vindas da loja; depois baixe novamente google-services.json e substitua android/app/google-services.json. O OAuth Web Client ID permanece configurado no backend e é usado como webClientId/audience do ID token. Não adicione options.scopes ao login básico: o plugin já solicita openid, e-mail e perfil e trata scopes explícitos como fluxo avançado que exige uma MainActivity modificada. O PWA continua usando Google Identity Services e não depende do plugin nativo.

Os recursos Android em android/app/src/main/res/ e os catálogos iOS em ios/App/App/Assets.xcassets/ derivam da marca em public/brand/. Edite public/brand/icon-extracted.svg e rode node scripts/generate-brand-icons.mjs — regenera os PNGs do PWA, os mipmaps Android, o AppIcon e a splash iOS a partir do mesmo SVG. A coroa deve ser o path do Lucide Crown (o mesmo do Login / Sidebar), ~50% do plate, com stroke-linejoin="round". A splash e o adaptive icon vector (drawable-v24/ic_launcher_foreground.xml) devem acompanhar a mesma geometria. A chave de upload da Play Store não pertence ao repositório e ainda precisa ser criada/configurada antes do primeiro bundle de produção assinado.

Geolocalização nativa usa @capacitor/geolocation; npm run android:sync adiciona ao manifesto as permissões ACCESS_COARSE_LOCATION e ACCESS_FINE_LOCATION. Teste em aparelho real tanto a escolha Precise quanto a recusa: o gate de requisitos deve distinguir permissão negada de sinal fraco, e o PWA deve continuar usando a permissão de localização do navegador.

Não consulte câmera diretamente em hooks. cameraProvider normaliza check/request para Capacitor e web; no navegador a solicitação abre getUserMedia dentro do gesto e encerra todas as tracks imediatamente. Safari/Firefox podem não oferecer permissions.query({ name: 'camera' }), portanto unknown é um estado esperado até o usuário acionar a câmera.

Não instancie AudioContext em componentes ou hooks. Use playWithAudio ou playAudioConfirmationTone em src/shared/services/audioContext.ts: o contexto único é desbloqueado no primeiro gesto, resume() é aguardado e o retorno booleano distingue reprodução agendada de áudio indisponível. Fechar esse contexto interrompe os alertas seguintes, especialmente no Safari/iOS.

O Gradle recusa qualquer tarefa release sem as quatro credenciais completas. Guarde o arquivo e as senhas fora do repositório e forneça-os pelo ambiente ou pelos secrets do CI:

Terminal window
export PRIME_CROWN_UPLOAD_STORE_FILE="/caminho/seguro/prime-crown-upload.jks"
export PRIME_CROWN_UPLOAD_STORE_PASSWORD="..."
export PRIME_CROWN_UPLOAD_KEY_ALIAS="prime-crown-upload"
export PRIME_CROWN_UPLOAD_KEY_PASSWORD="..."
export PRIME_CROWN_VERSION_CODE="1"
export PRIME_CROWN_VERSION_NAME="1.0.0"
npm run android:bundle:release

O resultado é android/app/build/outputs/bundle/release/app-release.aab. O versionCode deve aumentar em todo envio à Play Store; versionName é a versão visível. Arquivos .jks, .keystore, .p12 e .aab são ignorados pelo Git. Mantenha backup criptografado da upload key: perdê-la impede assinar atualizações até concluir o processo de redefinição da chave na Play Console.

Execute npm run android:doctor antes de abrir o Android Studio. O diagnóstico valida package/SDK, cliente Firebase, App Links, push e ações, permissões, bloqueio de backup, privacy screen, HTTPS, ícones e gate de assinatura; JDK 21, FIREBASE_SERVICE_ACCOUNT_JSON do mesmo projeto e as quatro variáveis PRIME_CROWN_UPLOAD_* aparecem separadamente como itens externos. Use -- --strict somente no pipeline de release assinado com as credenciais carregadas.

No fluxo Bitwarden, o projeto usa quatro itens: prime-crown:ANDROID_UPLOAD_KEYSTORE_BASE64, prime-crown:ANDROID_UPLOAD_STORE_PASSWORD, prime-crown:ANDROID_UPLOAD_KEY_ALIAS e prime-crown:ANDROID_UPLOAD_KEY_PASSWORD. Para recuperar a chave somente durante o build:

Terminal window
bw unlock --raw > /tmp/prime-crown-bw-session
chmod 600 /tmp/prime-crown-bw-session
PRIME_CROWN_VERSION_CODE=1 PRIME_CROWN_VERSION_NAME=1.0.0 npm run android:bundle:release:bw

O wrapper apaga o keystore reconstruído e o arquivo de sessão ao terminar. Em macOS com Homebrew ele seleciona automaticamente o JDK 21; em outro ambiente, defina PRIME_CROWN_JAVA_HOME.

Os comandos android:release:auto e ios:release:auto, assim como os wrappers diretos de cada loja, exigem que toda alteração rastreada esteja em um commit antes de abrir o Bitwarden. Arquivos locais não rastreados não participam do bundle e não bloqueiam o fluxo. O mesmo commit é capturado no início e conferido novamente depois de cap sync/build, antes do upload; se HEAD ou qualquer arquivo rastreado mudar nesse intervalo, a publicação é interrompida. Assim o AAB/IPA aceito pela loja e a tag android-v<N>/ios-v<N> sempre representam o mesmo snapshot reproduzível.

O manifesto associa https://app.primecrowncleaning.co.uk/* ao aplicativo. src/shared/platform/androidAppLinks.ts contém os fingerprints da upload key e da app signing key; mantenha o fallback public/.well-known/assetlinks.json idêntico. O domínio entrega o contrato sem redirecionamento pela rota functions/.well-known/assetlinks.json.ts, incluída explicitamente em public/_routes.json, porque deploys do Pages podem ignorar diretórios estáticos ocultos.

O projeto Firebase deve possuir um app Android com package ID uk.co.primecrowncleaning.app. android/app/google-services.json configura esse cliente e npm run android:sync atualiza o plugin. @capacitor/push-notifications registra o token FCM Android ou o device token APNs iOS na API autenticada sem alterar o Web Push do PWA. No Android, o boot cria os canais prime_crown_messages (chat) e prime_crown_notifications (resto); pushes de chat data-only são desenhados por PrimeCrownMessagingService no estilo WhatsApp. No iOS, functions/lib/apns.ts envia alerta, som, badge e thread-id diretamente ao APNs. Para testar Android localmente, defina FIREBASE_SERVICE_ACCOUNT_JSON; para iOS configure as três credenciais APNs e use sandbox somente com um binário development. Em produção, todas pertencem ao Bitwarden/Cloudflare e nunca ao repositório.


  • Node.js: Versão 20 LTS ou superior.
  • npm: Versão 10+.
  • Wrangler CLI: Instalado via dependências (npx wrangler).
  • Sistema Operacional: macOS, Linux ou Windows (WSL2 recomendado).

Passo 1: Clonar o Repositório e Instalar Dependências

Section titled “Passo 1: Clonar o Repositório e Instalar Dependências”
Terminal window
git clone [email protected]:avizrafael/prime-crown.git
cd prime-crown
npm install

Passo 2: Inicializar o Banco de Dados Local (Cloudflare D1 Emulation)

Section titled “Passo 2: Inicializar o Banco de Dados Local (Cloudflare D1 Emulation)”

O comando db:init executa o arquivo db/schema.sql contra a instância local do D1 SQLite mantida pelo Wrangler:

Terminal window
npm run db:init

Passo 3: Configurar Variáveis de Ambiente

Section titled “Passo 3: Configurar Variáveis de Ambiente”

Crie um arquivo .dev.vars na raiz do projeto (baseado em .dev.vars.example):

JWT_SECRET="seu-jwt-secret-de-desenvolvimento-local"
GEMINI_API_KEY="sua-chave-opcional-do-gemini"

⚡ 3. Executando a Aplicação (Dois Terminais)

Section titled “⚡ 3. Executando a Aplicação (Dois Terminais)”

A arquitetura do Prime Crown requer a execução simultânea do backend serverless (Pages Functions) e do frontend (Vite Dev Server):

graph LR
    Sub1["Terminal 1: npm run pages:dev"] -->|Emula Backend Workers na Porta 8788| Backend["Pages Functions / D1 SQLite"]
    Sub2["Terminal 2: npm run dev"] -->|Vite Dev Server na Porta 3000| Frontend["React 19 SPA"]
    Frontend <-->|Proxy HTTP /api/*| Backend
  • Terminal 1 (Backend & Worker Emulation):

    Terminal window
    npm run pages:dev

    (Inicia a emulação do Cloudflare Pages Functions na porta 8788)

  • Terminal 2 (Frontend SPA):

    Terminal window
    npm run dev

    (Inicia o Vite na porta 3000, redirecionando todas as chamadas /api/* via proxy para a porta 8788)

Acesse a aplicação no navegador em: http://localhost:3000


🧪 4. Comandos de Verificação e Qualidade

Section titled “🧪 4. Comandos de Verificação e Qualidade”

Antes de efetuar commits ou abrir Pull Requests, execute a suíte de validação:

Terminal window
# Executa a verificação completa (Typecheck + Lint + Tests)
npm run check
# Ou individualmente:
npm run typecheck # Validação rigorosa do TypeScript em src/ e functions/
npm run lint # Linter de código ultra-rápido via Biome
npm test # Execução de 297 testes automatizados com Vitest