App Links e Universal Links: Deep Links em Produção (Parte 5)
Custom schemes funcionam bem para desenvolvimento, mas para produção você precisa de HTTPS. É aqui que entram App Links no Android e Universal Links no iOS — e onde a maioria dos problemas reais aparece. Este é o quinto conteúdo de uma série completa sobre Deep Links no Flutter. Se você ainda não viu os posts anteriores: Post 1 | Post 2 | Post 3 | Post 4. Até aqui usamos fitconnect:// para testes. Funciona, mas em produção esse scheme tem limitações sérias: fitconnect://fitconnect.app/signup?referralCode=TRAINER1234567890123 Qualquer app pode registrar o mesmo scheme — sem nenhuma verificação. Se o usuário não tem o app instalado, o sistema exibe um erro genérico. Não há como garantir que o link chegue ao app correto. https://deeplinkslab.dev/signup?referralCode=TRAINER1234567890123 ✅ Verificação server-side: o sistema operacional confirma com o servidor antes de abrir o app. ✅ Se o app não estiver instalado, o link abre no navegador como fallback natural. ✅ Profissional e seguro. A diferença não é só cosmética. É uma questão de confiança — tanto do sistema quanto do usuário. Até aqui, usamos fitconnect.app como domínio fictício para focar na implementação. Mas agora entramos na parte onde deep links deixam de ser apenas código — e passam a depender de infraestrutura real. App Links e Universal Links só funcionam com um domínio válido, acessível publicamente e com verificação bidirecional. Para isso, vamos usar um domínio real dedicado a testes: deeplinkslab.dev A partir deste ponto, tudo que implementarmos será o mesmo que você precisa em produção. Você não precisa usar este domínio — pode (e deveria) usar o seu próprio. Um domínio .dev custa em torno de R$ 60–80 por ano e já vem com HTTPS obrigatório, o que elimina uma configuração a menos. Qualquer registrador funciona: Registro.br, Namecheap, Cloudflare Registrar ou Squarespace Domains. Se quiser um guia específico — do registro do domínio até o deploy no Cloudflare Pages com os arquivos de verificação prontos — me avisa nos comentários. Se tiver demanda, faço um post dedicado só sobre isso. O assetlinks.json é o arquivo que o Android baixa do seu servidor para confirmar: "esse app tem permissão para tratar links desse domínio?". Sem ele, o android:autoVerify="true" que adicionamos no Post 2 não tem como concluir a verificação — e o link pode abrir um seletor em vez do app. O arquivo assetlinks.json precisa conter o SHA256 do certificado com que o app foi assinado. Para o keystore de debug: keytool -list -v \ -keystore ~/.android/debug.keystore \ -alias androiddebugkey \ -storepass android \ -keypass android | grep "SHA256:" Para o keystore de release, substitua o caminho e as credenciais pelo seu keystore de produção. Debug e release geram SHA256 diferentes — o arquivo de produção precisa conter o fingerprint do certificado de release. Durante o desenvolvimento, o fingerprint de debug é suficiente para testar localmente. Se você usa o Google Play App Signing, o SHA256 correto para produção está em: Play Console → Configuração → Integridade do app → Certificado de assinatura do app. [ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.fitconnect.app", "sha256_cert_fingerprints": [ "SEU_SHA256" ] } } ] A relação delegate_permission/common.handle_all_urls é o valor padrão e obrigatório — informa ao Android que o app tem permissão para tratar qualquer URL do domínio declarado. O arquivo deve estar acessível publicamente em: https://deeplinkslab.dev/.well-known/assetlinks.json Dois requisitos que costumam causar problemas em produção: O servidor precisa responder com Content-Type: application/json. O arquivo deve ser acessível via HTTPS sem redirecionamentos — o Android não segue redirects ao verificar o arquivo. Se qualquer um desses requisitos falhar, o Android simplesmente ignora o arquivo — e o link passa a abrir no navegador sem nenhum erro visível. No iOS, o equivalente é o apple-app-site-association (sem extensão). O iOS pode manter esse arquivo em cache. Se algo não funcionar durante o desenvolvimento, reinstalar o app geralmente força uma nova verificação. { "applinks": { "apps": [], "details": [ { "appID": "TEAM_ID.com.fitconnect.app", "paths": ["/signup", "/signup/*"] } ] } } O campo apps deve ser um array vazio — é uma exigência da Apple, mesmo que pareça redundante. O appID combina o Team ID com o bundle ID. O array paths lista os caminhos que o app está autorizado a tratar; /signup/* cobre rotas com subpaths ou query strings. Acesse developer.apple.com/account. Vá em Membership → o Team ID aparece na lista de informações da conta. https://deeplinkslab.dev/.well-known/apple-app-site-association O arquivo não deve ter extensão. Sirva-o com Content-Type: application/json e sem redirecionamentos — os mesmos requisitos do Android. O iOS 14+ usa uma CDN da Apple para cachear o arquivo, por isso mudanças podem demorar a ser refletidas; reinstalar o app força a re-verificação durante o desenvolvimento. A lógica é a mesma nas duas plataformas: o sistema operacional só abre o app se as duas pontas se reconhecerem. O app declara, no AndroidManifest.xml e no Runner.entitlements, quais domínios ele se propõe a tratar. O servidor, por sua vez, confirma no assetlinks.json e no apple-app-site-association quais aplicativos têm permissão para tratar URLs do domínio. Se qualquer lado estiver faltando ou inconsistente — fingerprint errado, arquivo inacessível, caminho não listado — o link abre no navegador como fallback, sem mensagem de erro para o usuário. Isso protege contra um vetor real: sem verificação bidirecional, um aplicativo malicioso poderia registrar o mesmo scheme ou domínio e interceptar links destinados ao seu app. A ferramenta oficial do Google valida o assetlinks.json sem precisar instalar o app: https://developers.google.com/digital-asset-links/tools/generator Informe o domínio e o package name — a ferramenta mostra se o arquivo está acessível, se o JSON é válido e se o fingerprint está correto. Também é possível verificar via adb em um device com o app instalado. No Android 12+, o fluxo recomendado é resetar o estado, forçar a reverificação e só então consultar o resultado: # Resetar o estado de verificação adb shell pm set-app-links --package com.fitconnect.app 0 all # Forçar reverificação adb shell pm verify-app-links --re-verify com.fitconnect.app # Consultar o resultado adb shell pm get-app-links com.fitconnect.app O status verified confirma que a verificação foi concluída com sucesso. Pular o reset pode fazer o comando retornar um estado antigo em cache — não necessariamente o resultado da configuração atual. curl https://deeplinkslab.dev/.well-known/apple-app-site-association Se o curl retornar o JSON correto, o arquivo está acessível. Para validar o comportamento completo, a Apple recomenda testar em um device físico — o simulador não reproduz a verificação de Associated Domains com fidelidade. Antes de fazer deploy, confirme cada item: Android: [ ] SHA256 obtido para o keystore de debug (testes) e release (produção) [ ] assetlinks.json com package name e fingerprint corretos [ ] Arquivo acessível em https://deeplinkslab.dev/.well-known/assetlinks.json [ ] Servidor respondendo com Content-Type: application/json [ ] Sem redirecionamentos no caminho /.well-known/assetlinks.json [ ] Validado com a ferramenta Google Digital Asset Links iOS: [ ] Team ID obtido no Apple Developer Portal [ ] apple-app-site-association com appID e paths corretos [ ] Arquivo acessível em https://deeplinkslab.dev/.well-known/apple-app-site-association [ ] Associated Domains configurado no Xcode (applinks:deeplinkslab.dev) [ ] Testado em device físico (não apenas simulador) Ao final desta etapa, você já tem: O assetlinks.json configurado com o SHA256 correto para Android. O apple-app-site-association com Team ID e paths autorizados para iOS. Clareza sobre como funciona a verificação bidirecional e por que ela existe. Ferramentas e comandos para validar os dois arquivos antes de ir a produção. Este é o quinto de 9 posts da série. Com Android, iOS e Flutter integrados e a verificação de domínio no lugar, o fluxo básico de deep links está completo — mas ainda só funciona quando o usuário já tem o app instalado. No próximo post, resolvemos o caso mais difícil: o que acontece quando o usuário clica no link e ainda não tem o app. No próximo post: Deferred Deep Links — como salvar o contexto do link e recuperar após a instalação. Código completo disponível no repositório: FitConnect no GitHub Este artigo é uma crosspost do Medium — leia lá também e deixa um clap se curtiu. Já subiu App Links em produção? Qual foi o erro mais difícil de debugar? Conta nos comentários.
