O que construímos até aqui
Nos primeiros posts da série definimos a estrutura base em Dart (Post 1), implementamos a captura nativa no Android (Post 2) e no iOS (Post 3), e conectamos tudo ao Flutter com DeepLinkService, MethodChannel e EventChannel (Post 4). No Post 5, publicamos os arquivos de verificação em deeplinkslab.dev — App Links e Universal Links funcionando em produção. No Post 6, resolvemos o caso do usuário que ainda não tem o app: como salvar o referralCode antes da instalação e recuperá-lo no primeiro launch.
Falta uma peça: quem salva esse código antes de redirecionar para a loja?
Você implementou deep links. Android captura, iOS captura, Flutter processa. Tudo funciona — quando o app está instalado.
Mas o que acontece quando o usuário clica no link e o app não está lá?
Sem uma página de redirect, o navegador abre um erro 404. O código de indicação (referralCode) se perde. A experiência termina antes de começar.
A solução é uma página HTML inteligente que serve de ponte: detecta a plataforma, tenta abrir o app, preserva o código e redireciona para a loja certa se necessário.
Este é o sétimo conteúdo de uma série completa sobre Deep Links no Flutter. Se você ainda não viu os posts anteriores: Post 1 — Guia para Iniciantes | Post 2 — Android com Kotlin | Post 3 — iOS com Swift | Post 4 — Integração Flutter | Post 5 — Produção | Post 6 — Deferred Deep Links.
Neste artigo você vai aprender:
- Como criar uma página HTML que detecta a plataforma e tenta abrir o app.
- Como preservar o código de indicação (referralCode) mesmo antes da instalação.
- As opções de deploy: backend, Nginx e Flutter Web.
O problema
O cenário concreto:
- Maria compartilha:
https://deeplinkslab.dev/signup?referralCode=MARIA1234567890123 - João clica, mas não tem o app instalado.
- O navegador abre — e o que João vê? Um erro 404. Ou uma página em branco. Ou nada.
A experiência para João é péssima. O referralCode de Maria se perde. A conversão não acontece.
A página de redirect resolve os três problemas ao mesmo tempo.
Arquitetura da solução
O fluxo da página é direto:
O localStorage entra como backup: antes de tentar abrir o app, a página salva o código no navegador. Se o app estiver instalado e abrir direto, o código chega via deep link. Se não, o código fica salvo para ser recuperado na primeira abertura — integrando com o DeepLinkService que vimos no Post 6.
signup.html
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FitConnect - Cadastro com Código de Indicação</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
border-radius: 20px;
padding: 40px;
max-width: 500px;
text-align: center;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.logo {
font-size: 64px;
margin-bottom: 20px;
}
.affiliate-code {
background: #f0f4ff;
border: 2px dashed #667eea;
border-radius: 10px;
padding: 20px;
margin: 20px 0;
font-family: "Courier New", monospace;
font-size: 24px;
font-weight: bold;
color: #667eea;
letter-spacing: 2px;
}
.btn {
display: inline-block;
padding: 15px 40px;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
text-decoration: none;
margin: 10px;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<div class="logo">🏋️</div>
<h1>FitConnect</h1>
<div id="content">
<div class="spinner"></div>
<p>Preparando seu cadastro...</p>
</div>
</div>
<script>
const config = {
customScheme: "fitconnect://",
playStoreUrl: "https://play.google.com/store/apps/details?id=com.fitconnect.app",
appStoreUrl: "https://apps.apple.com/app/id123456789",
fallbackUrl: "https://deeplinkslab.dev",
};
const urlParams = new URLSearchParams(window.location.search);
const affiliateCode = urlParams.get("referralCode");
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
const isAndroid = /android/i.test(userAgent);
const isIOS = /iPad|iPhone|iPod/.test(userAgent);
const isMobile = isAndroid || isIOS;
function handleRedirect() {
if (!isMobile) {
showDesktopInstructions();
return;
}
if (affiliateCode && affiliateCode.length === 20) {
showAffiliateCode(affiliateCode);
attemptToOpenApp();
} else {
redirectToStore();
}
}
function showAffiliateCode(code) {
document.getElementById("content").innerHTML = `
<p>Seu código de indicação:</p>
<div class="affiliate-code">${code}</div>
<p style="font-size: 14px; color: #666;">
Este código será aplicado automaticamente!
</p>
<div class="spinner"></div>
<p>Abrindo o app...</p>
`;
}
function attemptToOpenApp() {
const deepLink = affiliateCode
? `${config.customScheme}deeplinkslab.dev/signup?referralCode=${affiliateCode}`
: `${config.customScheme}deeplinkslab.dev/signup`;
window.location = deepLink;
setTimeout(() => {
redirectToStore();
}, 2000);
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
clearTimeout();
}
});
}
function redirectToStore() {
document.getElementById("content").innerHTML = `
<p>Baixe o FitConnect para usar!</p>
${affiliateCode ? `
<div class="affiliate-code">${affiliateCode}</div>
<p style="font-size: 14px;">Guarde este código!</p>
` : ""}
<a href="${isAndroid ? config.playStoreUrl : config.appStoreUrl}"
class="btn btn-primary">
Baixar App
</a>
`;
setTimeout(() => {
window.location = isAndroid ? config.playStoreUrl : config.appStoreUrl;
}, 3000);
}
// Salvar código em localStorage (backup para Deferred Deep Link)
if (affiliateCode && isMobile) {
localStorage.setItem("pending_referral", affiliateCode);
}
handleRedirect();
</script>
</body>
</html>
As decisões por trás do código
Por que setTimeout(2000)?
Não há evento nativo no browser para detectar se um custom scheme abriu o app com sucesso. O setTimeout de 2 segundos é a heurística padrão: se o app abrir, o sistema move a página para segundo plano (visibilitychange → hidden) e o redirect para a loja nunca dispara. Se o app não estiver instalado, o browser ignora o fitconnect:// silenciosamente e, 2 segundos depois, o redirect acontece.
Por que salvar no localStorage antes de tudo?
O salvamento acontece antes de attemptToOpenApp(). Se o app abrir via custom scheme, o código chega pelo deep link — o localStorage fica como fallback. Se o app não estiver instalado, o código fica salvo para ser recuperado na primeira abertura, integrando com o getPendingReferralCode() do DeepLinkService (Post 6).
Por que mostrar o código visualmente antes de redirecionar?
Se o redirect automático falhar — ou se o usuário quiser anotar o código antes de instalar — ele está visível na tela. Isso é importante especialmente no caso desktop, onde não há tentativa de abrir o app.
Deploy
Opção 1: Backend Node.js/Express
const express = require("express");
const path = require("path");
const app = express();
app.get("/signup", (req, res) => {
const userAgent = req.headers["user-agent"];
const isAndroid = /android/i.test(userAgent);
const isIOS = /iPhone|iPad|iPod/i.test(userAgent);
// Desktop — redireciona para o site principal
if (!isAndroid && !isIOS) {
return res.redirect("https://deeplinkslab.dev");
}
// Mobile — serve a página de redirect
res.sendFile(path.join(__dirname, "web_redirect", "signup.html"));
});
app.listen(443, () => {
console.log("Server running on https://deeplinkslab.dev");
});
Opção 2: Nginx
server {
listen 443 ssl;
server_name deeplinkslab.dev;
location = /signup {
try_files /signup.html =404;
}
location /.well-known/ {
alias /var/www/deeplinkslab.dev/.well-known/;
default_type application/json;
}
}
Essa configuração também cobre o caminho /.well-known/ para os arquivos assetlinks.json e apple-app-site-association do Post 5.
Opção 3: Flutter Web
# Gerar build web do Flutter
flutter build web --release
# Deploy (Firebase Hosting, Vercel, Netlify)
firebase deploy
Qual opção escolher? Para começar, o Nginx é o mais simples se você já tem um servidor. Se o projeto não tem backend ainda, Firebase Hosting ou Netlify entregam o signup.html como arquivo estático com configuração mínima — e ambos suportam headers customizados para o Content-Type: application/json dos arquivos de verificação.
Fluxo completo integrado
Com todos os componentes no lugar, o fluxo ponta a ponta fica:
Testando o redirect
Cenário 1: App instalado
# Instalar o app
flutter run
# Abrir link no navegador do emulador
adb shell am start -a android.intent.action.VIEW \
-d "https://deeplinkslab.dev/signup?referralCode=TEST12345678901234"
# Resultado esperado: app abre direto na SignupPage com código preenchido
Cenário 2: App não instalado
# Desinstalar o app
adb uninstall com.fitconnect.app
# Abrir link no navegador
adb shell am start -a android.intent.action.VIEW \
-d "https://deeplinkslab.dev/signup?referralCode=TEST12345678901234"
# Resultado esperado:
# - Navegador abre signup.html
# - Código "TEST12345678901234" aparece na tela
# - Após 2s, redireciona para Play Store
Cenário 3: Desktop
# Abrir no navegador
open "https://deeplinkslab.dev/signup?referralCode=TEST12345678901234"
# Resultado esperado:
# - Código visível na tela
# - Mensagem orientando a abrir no celular
Melhorias avançadas
Quatro adições que elevam a experiência sem mudar a estrutura central:
QR Code para desktop — gera o QR do link atual para o usuário escanear com o celular:
// <script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
function showDesktopInstructions() {
const qrContainer = document.createElement("div");
qrContainer.id = "qrcode";
new QRCode(qrContainer, { text: window.location.href, width: 200, height: 200 });
document.getElementById("content").appendChild(qrContainer);
}
Copiar código para clipboard:
function copyCode() {
navigator.clipboard.writeText(affiliateCode);
alert("Código copiado!");
}
Analytics de conversão:
gtag("event", "referral_page_opened", {
referral_code: affiliateCode,
platform: isAndroid ? "android" : isIOS ? "ios" : "desktop",
});
Loading states explícitos — em vez de um spinner genérico, estados com feedback claro:
showStatus("Abrindo o FitConnect...", "loading");
showStatus("Redirecionando para loja...", "redirect");
showStatus("App aberto com sucesso!", "success");
O que construímos até aqui
Ao final desta etapa, você já tem:
- A página
signup.htmlque detecta plataforma, exibe o código e tenta abrir o app. - A lógica de
setTimeout(2000)como heurística para detectar se o app abriu. - O
localStoragecomo ponte para oDeepLinkServiceno cenário sem app instalado. - As três opções de deploy e critérios para escolher a mais adequada ao projeto.
- Os comandos
adbeopenpara testar os três cenários.
Este é o sétimo de 9 posts da série. Com a página de redirect no lugar, o fluxo de deep links está completo para todos os cenários: app instalado, app não instalado, desktop. No próximo post — o último da série principal — vamos testar tudo sistematicamente e construir um guia de troubleshooting para os problemas mais comuns. Se você já criou uma página de redirect para o seu app, conta nos comentários como foi — vou usar isso no Post 8.
No próximo post: testes completos, checklist de deploy e troubleshooting dos problemas mais comuns.
Código completo disponível no repositório: FitConnect no GitHub



Top comments (0)