DEV Community

Cristian Dornelles
Cristian Dornelles

Posted on

Web Redirect: A Ponte Entre Navegador e App Store (Parte 7)

Header

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:

  1. Maria compartilha: https://deeplinkslab.dev/signup?referralCode=MARIA1234567890123
  2. João clica, mas não tem o app instalado.
  3. 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:

Arquitetura da solução: fluxo da página de redirect

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>
Enter fullscreen mode Exit fullscreen mode

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 (visibilitychangehidden) 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");
});
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

Fluxo completo integrado: do clique à primeira abertura com código


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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Copiar código para clipboard:

function copyCode() {
  navigator.clipboard.writeText(affiliateCode);
  alert("Código copiado!");
}
Enter fullscreen mode Exit fullscreen mode

Analytics de conversão:

gtag("event", "referral_page_opened", {
  referral_code: affiliateCode,
  platform: isAndroid ? "android" : isIOS ? "ios" : "desktop",
});
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

O que construímos até aqui

Ao final desta etapa, você já tem:

  • A página signup.html que 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 localStorage como ponte para o DeepLinkService no cenário sem app instalado.
  • As três opções de deploy e critérios para escolher a mais adequada ao projeto.
  • Os comandos adb e open para 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)