Como Criar um QR Code para Roteamento de Aplicativo com Parâmetros Personalizados

opoinstall
2026-10-29
5 min read

Como vincular parâmetros personalizados ao criar QR codes? A vinculação de parâmetros personalizados para o roteamento de aplicativos requer a anexação de chaves de consulta de URL estruturadas a um link de landing page hospedado, a captura dessas chaves por meio de uma camada de integração web e o envio do payload validado para controladores de navegação nativos no momento da ativação direta do aplicativo ou no lançamento pós-instalação.

O roteamento in-app via QR codes é uma arquitetura de navegação cliente-servidor que incorpora parâmetros de destino estruturados em uma URL de código de barras bidimensional. Ao resolver esses parâmetros por meio de deep links diretos do sistema operacional quando o aplicativo já está instalado, ou por meio de recuperação adiada suportada pela plataforma quando o aplicativo é recém-instalado a partir de uma loja de aplicativos, as equipes de engenharia podem direcionar os usuários diretamente para telas específicas dentro do aplicativo, em vez da visualização inicial padrão.

Termo Definição Entidade Relacionada Papel na Intenção de Busca
Criar QR Code para App O fluxo de trabalho de engenharia para codificar URLs de roteamento acionáveis em códigos de barras 2D para navegação móvel. Restauração de Cena Informativo / Técnico
app qr code Um ativo de código de barras configurado para disparar a inicialização do aplicativo nativo ou a integração na loja. Universal Links / App Links Técnico / Comercial
dynamic qr code Um código de barras que aponta para um ponto de extremidade de roteamento intermediário cujo payload de destino pode ser atualizado no lado do servidor. Indireção de URI Comercial
qr code tracking A medição de solicitações de entrada de roteamento e ativações de telas subsequentes no aplicativo. Estatísticas de Canais e Rastreamento Comercial

Visão Geral da Arquitetura: Como Criar QR Code para Restauração de Cena no Aplicativo

O roteamento de QR parametrizado pode restaurar uma tela de detalhes de produto dentro de um aplicativo.

O Problema de Abandono na Tela Inicial: Por Que Redirecionamentos Genéricos de Lojas Interrompem Jornadas de Usuário de Alto Interesse

QR codes físicos são frequentemente implantados em ambientes contextuais de alto interesse: um comprador digitaliza uma etiqueta de prateleira para ver especificações de produtos, um cliente escaneia um display de mesa para pedir de uma categoria de menu específica, ou um jogador escaneia um código de barras de convite para entrar no lobby de um jogo multiplayer. Quando o código de barras codifica um link genérico de loja de aplicativos ou um domínio não parametrizado, a intenção contextual do usuário é perdida durante a transição do escaneamento físico para o tempo de execução do aplicativo nativo.

Ao iniciar o aplicativo, o usuário é direcionado para a tela inicial padrão de nível superior. Forçar o usuário a navegar manualmente por árvores de categorias complexas, reinserir consultas de pesquisa ou digitar identificadores de sala introduz um atrito cognitivo e mecânico substancial. A restauração de cenas in-app aborda esse abandono incorporando o estado de destino na URL do QR code e reconstruindo programaticamente essa visualização alvo dentro do cliente nativo.

Mecânicas de Roteamento de Estado Dual: Ativação Direta vs. Restauração de Cena Adiada

Aplicativos instalados usam links diretos, enquanto novas instalações precisam de recuperação de contexto suportada.

Quando um usuário escaneia um QR code de roteamento de aplicativo, o dispositivo móvel encontra-se em um dos dois estados de instalação. Uma arquitetura de roteamento completa lida com ambos os ciclos de vida por meio de caminhos técnicos distintos:

  • Roteamento por Ativação Direta (Aplicativo Já Instalado): Quando o aplicativo de destino já está presente no dispositivo, a leitura do QR code aciona um deep link do sistema operacional — especificamente Apple Universal Links no iOS ou Android App Links no Android. Ambos os mecanismos exigem a hospedagem de arquivos de associação de domínio verificados criptograficamente: o iOS requer um arquivo apple-app-site-association (AASA) emparelhado com uma permissão Associated Domains do Xcode, enquanto o Android requer um arquivo Digital Asset Links /.well-known/assetlinks.json correspondente à impressão digital SHA-256 do certificado de assinatura do aplicativo e a filtros de intenção de manifesto verificados. Quando verificados, o SO desperta o aplicativo e passa a URI diretamente para o delegado de ciclo de vida do cliente.
  • Restauração de Cena Adiada (Aplicativo Não Instalado): Quando o aplicativo está ausente, o usuário é direcionado por meio de uma página de destino web intermediária para a loja de aplicativos da plataforma. Como o binário nativo ainda não está instalado para receber uma intenção do sistema operacional, os parâmetros de roteamento devem ser armazenados antes do redirecionamento para a loja e recuperados durante o lançamento inicial do aplicativo (partida a frio no Dia 0) após a conclusão da instalação.

Limites de Recuperação Específicos de Plataforma: Google Play Install Referrer, iOS Universal Links e Adaptadores de SDK

Após um usuário instalar um aplicativo a partir de uma loja de aplicativos, a recuperação de parâmetros de referência e roteamento depende da plataforma e do mecanismo implementado pelo aplicativo ou SDK de atribuição. Passar um parâmetro route ou id por meio de uma URL web não garante, por si só, que o parâmetro sobreviverá a uma instalação pela loja de aplicativos:

  • Android e Google Play: A API Google Play Install Referrer pode fornecer metadados de referência suportados associados a uma instalação na Play Store. Esse mecanismo aplica-se especificamente a instalações concluídas através do caminho oficial da Google Play Store, e não a instalações paralelas de APKs (sideloads) de terceiros ou lojas alternativas. O aplicativo deve configurar e testar o payload de referência real e verificar se os parâmetros esperados sobrevivem ao fluxo de redirecionamento e instalação.
  • iOS e a App Store: A App Store da Apple não transfere automaticamente parâmetros arbitrários de consulta de landing pages web para um binário de aplicativo recém-instalado. Os Universal Links suportam o roteamento direto quando o aplicativo já está instalado e a configuração de domínio associada é válida; por si só, eles não garantem a recuperação adiada de parâmetros após uma nova instalação.
  • SDKs de Atribuição: Utilize apenas os mecanismos de recuperação documentados para a versão exata do SDK e a plataforma que está sendo implantada. Verifique a implementação, a coleta de dados e as implicações de privacidade, em vez de assumir que um SDK pode recuperar todos os parâmetros de roteamento em todos os navegadores e caminhos de instalação.

Atribuição no iOS e Limite de Privacidade: Em Conformidade com as Regras Antidividas de Impressão Digital da Apple

As implementações de roteamento e restauração de cena adiada no iOS não devem depender de impressões digitais de dispositivos (device fingerprinting) proibidas para identificar um usuário ou dispositivo entre aplicativos ou sites. Sob a Orientação de Privacidade do Usuário e Uso de Dados da Apple e o Contrato de Licença do Programa de Desenvolvedores da Apple, os desenvolvedores são estritamente proibidos de derivar um identificador exclusivo de sinais de dispositivos, navegadores, configurações, localização ou rede para essa finalidade. A autorização do App Tracking Transparency (ATT) não torna o fingerprinting proibido permissível.

Use Universal Links para o roteamento direto de aplicativos verificado quando o aplicativo estiver instalado. Para a restauração de cena pós-instalação no iOS, use um mecanismo documentado cujo comportamento tenha sido revisado para a versão do SDK implantada e os requisitos aplicáveis da Apple. Revise as divulgações de privacidade do SDK e as declarações de API de motivo necessário quando aplicável, mas não trate a presença de um Manifesto de Privacidade (PrivacyInfo.xcprivacy) como prova de que uma implementação de atribuição está em conformidade.

Se o SDK selecionado depender de um método de correspondência que não possa ser verificado como permitido, não recomende esse método para implantação no iOS. Onde nenhum mecanismo permitido puder estabelecer o contexto de roteamento adiado de forma confiável, preserve o fluxo de onboarding normal, direcione o usuário para uma visualização inicial padrão segura e marque a instalação como não atribuída, em vez de fabricar uma correspondência não verificada.

Visualizando o Pipeline de Restauração de Cena com QR de Estado Dual

O diagrama de arquitetura abaixo ilustra os estágios a seguir à medida que um QR code parametrizado se ramifica em ativação direta do SO ou recuperação adiada na loja antes de convergir no validador de rotas no lado do cliente e no portão de autenticação:

[User Scans QR Code] ──> URL (?route=product_detail&id=sku_8842&channelCode=pos_01)
          │
     ┌────┴────────────────────────────────────────┐
     ▼ (App Already Installed)                     ▼ (App Not Installed)
[OS Universal Link / App Link]               [Intermediary H5 Landing Page]
     │                                             │
     ▼                                             ▼
[Hot/Cold Wakeup Callback]                   [Store Download & First Launch]
(onNewIntent / continueUserActivity)         (Platform-Supported Deferred Recovery)
     │                                             │
     └────────────────────┬────────────────────────┘
                          ▼
           [Client Route Validator & Auth Gate]
                          │
     ┌────────────────────┴────────────────────────┐
     ▼ (Allowlisted & Authenticated)               ▼ (Invalid Route / Unmatched)
[Navigate to Product Detail Screen]          [Fallback to Default Home Screen]

Etapa 1: Projetando o Esquema de Parâmetros de Roteamento Personalizados

Estruturando Payloads de Roteamento Acionáveis vs. Tags de Atribuição Passiva

Ao criar um QR code para roteamento de aplicativos, as equipes de engenharia devem estabelecer um contrato de dados limpo de duas camadas que separa as chaves de consulta de URL externas e compactas do payload JSON interno e normalizado:

Camada do Contrato Convenção de Nomenclatura de Campos Representação de Exemplo Propósito Arquitetural
Camada de Consulta de URL Externa route, id, channelCode ?route=product_detail&id=sku_8842&channelCode=pos_01 Minimiza o comprimento de caracteres da URI para manter a matriz do QR impressa compacta
Camada Normalizada Interna route_key, target_id, channel_code, fallback_route {"route_key": "product_detail", "target_id": "sku_8842", ...} Objeto canônico padronizado validado por roteadores H5 e de clientes nativos
Registro de Segurança do Cliente PROTECTED_ROUTES (Constante interna) setOf("room_lobby", "promo_campaign") Determina os requisitos de autenticação internamente; nunca é confiável a partir de URLs externas

Os parâmetros de URL do QR são normalizados em contextos separados de navegação e atribuição.

Neste modelo de duas camadas:

  • Tags de Atribuição Passiva (channelCode →\to channel_code) são consumidas por pipelines de análise para medir o volume de conversão de marketing sem alterar o estado da interface do usuário do cliente.
  • Parâmetros de Roteamento Acionáveis (route →\to route_key e id →\to target_id) instruem o controlador de navegação do cliente nativo sobre qual tela montar e qual entidade buscar.
  • Isolamento da Política de Autenticação: Se uma rota requer autenticação (requires_auth), isso deve ser derivado estritamente do registro de rotas interno e confiável do aplicativo cliente, em vez de ser aceito a partir de um parâmetro de consulta de QR externo.

Aplicando a Codificação Percentual do IETF RFC 3986 e Restrições de Tamanho de Payload

Todos os parâmetros de roteamento anexados à URL de destino do QR devem aderir à sintaxe genérica de URI especificada no IETF RFC 3986:

  • Evite Delimitadores Aninhados: Não incorpore strings JSON brutas (?data={"route":"product","id":123}) diretamente em strings de consulta de URL. Chaves, aspas e dois-pontos não escapados frequentemente quebram os analisadores de URL de navegadores in-app e aumentam a densidade dos módulos do código de barras.
  • Achate Pares Chave-Valor: Represente o contexto de roteamento externo como parâmetros de consulta planos e com codificação percentual (https://promo.example.com/open?route=product_detail&id=sku_8842&channelCode=pos_01).
  • Aplique Limites Estritos de Caracteres: Restrinja os valores de route e id a caracteres alfanuméricos seguros para URLs, hífens e sublinhados (^[A-Za-z0-9_-]{1,64}$).

Definindo o Contrato de Lista de Permissões de Rotas do Lado do Cliente

Uma vez que os parâmetros de URL externos são ingeridos e normalizados na estrutura de payload interna, eles devem estar em conformidade com um contrato de esquema estrito. Se route_key for não reconhecido, ou se uma rota específica de entidade (product_detail, room_lobby, promo_campaign) estiver sem seu target_id obrigatório, o normalizador deve rebaixar o payload para home_default com target_id: null.

O JSON Schema abaixo define a estrutura canônica para um payload de restauração de cena normalizado:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "QrSceneRestorationPayload",
  "description": "Canonical, normalized routing payload. Authentication policy is derived separately from the application's trusted route registry.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema_version",
    "route_key",
    "target_id",
    "fallback_route"
  ],
  "properties": {
    "schema_version": {
      "const": "1.0.0"
    },
    "route_key": {
      "type": "string",
      "enum": [
        "product_detail",
        "room_lobby",
        "promo_campaign",
        "home_default"
      ]
    },
    "target_id": {
      "type": ["string", "null"],
      "pattern": "^[A-Za-z0-9_-]{1,64}$"
    },
    "channel_code": {
      "type": "string",
      "pattern": "^[A-Za-z0-9_.-]{1,64}$"
    },
    "fallback_route": {
      "const": "home_default"
    }
  },
  "allOf": [
    {
      "if": {
        "properties": {
          "route_key": {
            "enum": [
              "product_detail",
              "room_lobby",
              "promo_campaign"
            ]
          }
        },
        "required": ["route_key"]
      },
      "then": {
        "properties": {
          "target_id": {
            "type": "string"
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "route_key": {
            "const": "home_default"
          }
        },
        "required": ["route_key"]
      },
      "then": {
        "properties": {
          "target_id": {
            "type": "null"
          }
        }
      }
    }
  ],
  "examples": [
    {
      "schema_version": "1.0.0",
      "route_key": "product_detail",
      "target_id": "sku_8842",
      "channel_code": "pos_01",
      "fallback_route": "home_default"
    }
  ]
}

Etapa 2: Capturando e Encaminhando o Contexto de Roteamento na Landing Page

Analisando Chaves de Roteamento Permitidas na Landing Page H5

Quando um usuário sem o aplicativo instalado escaneia o QR code, o navegador móvel abre a landing page H5 intermediária hospedada. O script da landing page extrai as chaves de consulta externas (route, id, channelCode), rejeita parâmetros duplicados ambíguos e os normaliza para a estrutura canônica definida na Etapa 1.

O normalizador JavaScript abaixo impõe cardinalidade de chave única, limites de expressão regular e rebaixamento condicional para home_default, enquanto trata parâmetros reservados duplicados como inválidos:

const query = new URLSearchParams(window.location.search);

const routeValues = query.getAll("route");
const idValues = query.getAll("id");
const channelValues = query.getAll("channelCode");

const allowedRoutes = new Set([
  "product_detail",
  "room_lobby",
  "promo_campaign",
  "home_default"
]);

const idPattern = /^[A-Za-z0-9_-]{1,64}$/;
const channelPattern = /^[A-Za-z0-9_.-]{1,64}$/;

function normalizeQrRoutingPayload() {
  // Reject duplicate keys to defend against parameter pollution
  if (routeValues.length > 1 || idValues.length > 1 || channelValues.length > 1) {
    return {
      schema_version: "1.0.0",
      route_key: "home_default",
      target_id: null,
      fallback_route: "home_default"
    };
  }

  const rawRoute = routeValues.length === 1 ? routeValues[0] : null;
  const rawId = idValues.length === 1 ? idValues[0] : null;
  const rawChannel = channelValues.length === 1 ? channelValues[0] : null;

  let routeKey = allowedRoutes.has(rawRoute)
    ? rawRoute
    : "home_default";

  let targetId =
    rawId && idPattern.test(rawId)
      ? rawId
      : null;

  if (routeKey === "home_default" || targetId === null) {
    routeKey = "home_default";
    targetId = null;
  }

  const payload = {
    schema_version: "1.0.0",
    route_key: routeKey,
    target_id: targetId,
    fallback_route: "home_default"
  };

  if (rawChannel && channelPattern.test(rawChannel)) {
    payload.channel_code = rawChannel;
  }

  return payload;
}

const routingPayload = normalizeQrRoutingPayload();

Integrando o SDK Web Através de um Adaptador com Versão Verificada

A landing page coordena tanto as tentativas imediatas de ativação do aplicativo quanto o armazenamento temporário de parâmetros adiados. Antes de implantar um SDK Web de atribuição (como opoinstall.js), confirme a URL exata do pacote JavaScript, a assinatura de inicialização, o retorno de chamada de prontidão, o método de ativação e o esquema de parâmetros em relação à documentação oficial para a versão do SDK que está sendo implantada.

Mantenha o comportamento específico do SDK por trás de um pequeno adaptador de aplicativo que aceita o objeto routingPayload normalizado. Para especificações detalhadas sobre integração web e configuração de parâmetros de canal, consulte a documentação para desenvolvedores da OpoInstall.

Preservando Parâmetros de Roteamento em Links de Redirecionamento Direto para a Loja

Se o SDK Web JS estiver indisponível ou bloqueado por controles de segurança do navegador, a landing page recorre a um link de redirecionamento direto para a loja. Em implantações do Android que utilizam o Google Play, o link de fallback pode codificar as chaves de roteamento normalizadas no parâmetro referrer da Play Store, para que o cliente nativo possa recuperar o contexto de roteamento por meio da API Google Play Install Referrer quando instalado através do fluxo oficial da Play Store:

// Example Android Play Store fallback URL preserving normalized routing context
const referrerString = `route_key=${routingPayload.route_key}&target_id=${routingPayload.target_id || ""}&channel_code=${routingPayload.channel_code || ""}`;
const playStoreFallbackUrl = `https://play.google.com/store/apps/details?id=com.example.app&referrer=${encodeURIComponent(referrerString)}`;

Etapa 3: Implementando o Despacho de Rotas do Cliente Nativo e a Restauração de Cena

Lidando com Modos de Lançamento de Activity no Android: Por Que singleTask Requer Tanto onCreate Quanto onNewIntent

Em arquiteturas de deep link do Android, a forma como uma Activity recebe intents de roteamento depende de seu modo de lançamento configurado e do design do grafo de navegação. Os desenvolvedores podem usar modos de lançamento padrão com o Jetpack Navigation, singleTop ou singleTask, dependendo dos requisitos da pilha de tarefas.

Quando uma Activity é configurada com singleTop ou singleTask para evitar que instâncias duplicadas sejam empilhadas durante ativações por deep link, dois caminhos de ciclo de vida distintos devem ser tratados:

  • Partida a Frio (onCreate): Quando o processo do aplicativo não está em execução, a leitura do QR code inicia a Activity do zero. A Intent de entrada é entregue a onCreate(savedInstanceState: Bundle?).
  • Partida a Quente (onNewIntent): Quando o sistema operacional reutiliza uma instância de Activity existente sob o modo de lançamento configurado e as condições da pilha de tarefas, a Intent de deep link de entrada é entregue a onNewIntent(). Sob singleTop, a reutilização ocorre apenas quando a Activity de destino já está no topo da pilha de tarefas relevante; caso contrário, uma nova instância de Activity pode ser criada. Implemente o tratamento de onCreate() e onNewIntent() e verifique o comportamento real para a arquitetura de navegação do aplicativo.

Para lidar com ambos os estados de forma confiável sem bloquear permanentemente um usuário que legitimate escaneia o mesmo QR code uma segunda vez mais tarde em sua sessão, a Activity deve marcar cada instância de Intent processada com uma flag de consumo (EXTRA_ROUTE_CONSUMED) e ignorar intents reentregues exclusivamente por lançamentos históricos do alternador de aplicativos (Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY).

Lidando com Pontos de Entrada do Ciclo de Vida do iOS: Atividades de Continuação do AppDelegate vs. UISceneDelegate

No iOS, o roteamento direto por Universal Link e a recuperação de parâmetros adiados devem se alinhados com o ciclo de vida da interface do usuário configurado do aplicativo:

  • Ciclo de Vida Baseado em Cena (UISceneDelegate): Em aplicativos que adotam UIScene (padrão a partir do iOS 13), os Universal Links de inicialização a frio chegam em scene(_:willConnectTo:options:) por meio de connectionOptions.userActivities, enquanto os Universal Links de retomada a quente chegam em scene(_:continue:).
  • Ciclo de Vida do Delegate do Aplicativo (UIApplicationDelegate): Em aplicativos que não usam delegados de cena, os Universal Links são entregues a application(_:continue:restorationHandler:).
  • Recuperação Adiada no Primeiro Lançamento: Independentemente dos delegados de continuação de Universal Link, a consulta de parâmetros de instalação adiada é executada uma vez durante a inicialização inicial do aplicativo, após a hierarquia de visualização raiz estar pronta.

Os desenvolvedores podem baixar pacotes de SDK de plataforma e inspecionar modelos de integração de ciclo de vida por meio do centro de downloads do SDK da OpoInstall.

Unificando Parâmetros de Ativação Direta e Parâmetros Adiados de Primeiro Lançamento em um Único Roteador de Navegação

Para manter um comportamento consistente de segurança e navegação, as arquiteturas de clientes normalizam tanto as URIs de ativação direta quanto os payloads JSON de instalação adiada em um único modelo NavigationTarget antes de despachá-los para o roteador de UI:

  1. Verificação de Origem (Intents Diretas): Verifique se as URIs Intent.ACTION_VIEW de entrada usam o esquema https, correspondem ao host associado verificado do aplicativo (promo.example.com) e ao caminho (/open), e não contêm chaves de consulta duplicadas conflitantes.
  2. Normalização de Esquema e Verificação de Lista de Permissões: Mapeie chaves externas route / id ou campos internos route_key / target_id para o modelo canônico, rejeitando rotas não permitidas ou IDs malformados. Observe que a lógica de análise em Kotlin do cliente opera como um esqueleto de análise defensiva ilustrativa; em sistemas de produção, analise os payloads em relação a todas as regras do esquema canônico antes da distribuição.
  3. Proteção de Reentrância de Voo Único (Single-Flight): Use uma proteção atômica por entrega para que retornos de chamada concorrentes não possam disparar transações de fragmentos sobrepostas.

Gerenciando Portões de Autenticação: Enfileirando Rotas de Deep Link Pendentes Até que o Login do Usuário Seja Concluído

Rotas de QR protegidas podem ser retomadas após a autenticação, enquanto destinos inválidos retornam com segurança.

Quando um novo usuário instala um aplicativo a partir de uma loja de aplicativos por meio de um QR code e o inicia pela primeira vez, ele geralmente não está autenticado. Se o QR code tiver como alvo uma tela protegida (como room_lobby ou promo_campaign) e o roteador tentar navegar imediatamente, o portão de autenticação redirecionará o usuário para a tela de login e perderá o contexto do deep link.

Para preservar a restauração de cena através de limites de autenticação:

  • Consulta de Política de Rota Interna: Determine se a rota requer autenticação verificando o registro PROTECTED_ROUTES interno do cliente, em vez de confiar em sinalizadores de payload externos.
  • Armazenamento Temporário Atômico: Se o usuário não estiver autenticado, armazene o NavigationTarget validado em um PendingRouteStore atômico e com escopo de sessão (AtomicReference) e direcione o usuário para o fluxo de login. Observe que um PendingRouteStore em memória aplica-se ao processo ativo do aplicativo; se o fluxo de autenticação puder abranger a morte do processo, use um armazenamento local criptografado e com limite de tempo que limpe o estado na troca de contas.
  • Revalidação e Despacho Pós-Autenticação: Quando o login for concluído, consuma atomicamente o destino armazenado (getAndSet(null)), verifique se a sessão agora está autenticada, revalide os parâmetros da rota e execute a transição de tela.

Prontidão do Host de Navegação e Restauração de Estado

O retorno de chamada onNavigationGraphReady() deve ser conectado à inicialização real do host de navegação do aplicativo. Quando o grafo de navegação é definido em um NavHostFragment hospedado em XML, a activity pode estabelecer essa conexão após o host e seu destino estarem prontos. Se o grafo for instalado de forma assíncrona, invoque o retorno de chamada apenas após a conclusão da inicialização do grafo.

Uma solicitação de navegação pendente armazenada exclusivamente em um campo da Activity pode ser perdida durante alterações de configuração. Para implementações de produção, retenha o estado de roteamento não consumido em um ViewModel consciente do ciclo de vida e use SavedStateHandle quando a restauração em caso de recriação da Activity orientada pelo sistema ou morte do processo for necessária. O código mostrado aqui é um exemplo arquitetônico e requer integração de navegação e sessão específica do aplicativo.

O código Kotlin abaixo fornece um esqueleto de roteamento ilustrativo para Android com adaptadores de SDK e navegação pertencentes ao aplicativo que demonstram esses controles:

<!-- Android Manifest Configuration for App Link Verification -->
<!-- File: app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.app">

    <application
        android:allowBackup="true"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat.Light.NoActionBar">

        <activity
            android:name=".SceneRestorationActivity"
            android:exported="true"
            android:launchMode="singleTask">

            <!-- Verified Android App Link Intent Filter -->
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <!-- Origin must match /.well-known/assetlinks.json SHA-256 fingerprint -->
                <data
                    android:scheme="https"
                    android:host="promo.example.com"
                    android:pathPrefix="/open" />
            </intent-filter>
        </activity>
    </application>
</manifest>
// Android Routing Activity Implementation
// File: app/src/main/java/com/example/app/SceneRestorationActivity.kt
//
// INTEGRATION NOTE:
// This example is an architectural routing skeleton, not a drop-in production implementation.
// Bind AttributionRoutingAdapter and NavController initialization to your verified SDK release,
// session manager, and NavHostFragment before deployment.

package com.example.app

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.fragment.NavHostFragment
import org.json.JSONObject
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference

/**
 * Validated navigation target model representing an allowlisted in-app destination.
 * Authentication requirement is derived strictly from the internal route registry.
 */
data class NavigationTarget(
    val eventKey: String,
    val routeKey: String,
    val targetId: String?,
    val requiresAuth: Boolean
)

/**
 * Process-scoped atomic store holding a pending route when an unauthenticated user
 * triggers a protected deep link during cold start or first-launch onboarding.
 */
object PendingRouteStore {
    private val pending = AtomicReference<NavigationTarget?>(null)

    fun stage(target: NavigationTarget) {
        pending.set(target)
    }

    fun consume(): NavigationTarget? {
        return pending.getAndSet(null)
    }
}

class SceneRestorationActivity : AppCompatActivity() {

    companion object {
        private const val TAG = "SceneRestorationRouter"
        private const val TRUSTED_HOST = "promo.example.com"
        private const val TRUSTED_PATH = "/open"
        private const val EXTRA_ROUTE_CONSUMED = "com.example.app.EXTRA_ROUTE_CONSUMED"

        private const val ROUTE_HOME = "home_default"
        private const val ROUTE_PRODUCT = "product_detail"
        private const val ROUTE_LOBBY = "room_lobby"
        private const val ROUTE_PROMO = "promo_campaign"

        private val ALLOWED_ROUTES = setOf(ROUTE_HOME, ROUTE_PRODUCT, ROUTE_LOBBY, ROUTE_PROMO)
        private val PROTECTED_ROUTES = setOf(ROUTE_LOBBY, ROUTE_PROMO)
        private val SAFE_ID_REGEX = Regex("^[A-Za-z0-9_-]{1,64}$")

        // Process-level guard ensuring deferred install recovery is queried only once per cold start
        private val hasQueriedDeferredInstall = AtomicBoolean(false)
    }

    // Per-delivery re-entrancy guard; allows legitimate subsequent scans of the same QR code
    private val processingIntent = AtomicBoolean(false)

    // Staging container for routes that arrive before the UI/NavController is mounted
    private val pendingUiTarget = AtomicReference<NavigationTarget?>(null)
    private var navigationReady = false

    // Application-owned adapter isolating vendor SDK and session calls
    private val attributionAdapter = AttributionRoutingAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_scene_restoration)

        // Connect NavHost initialization when defined in layout
        connectNavigationHost()

        // Activity recreation (e.g., screen rotation) is not a new install or new QR scan
        if (savedInstanceState != null) {
            return
        }

        val launchIntent = intent
        val isDirectLinkLaunch =
            launchIntent?.action == Intent.ACTION_VIEW &&
            launchIntent.data != null

        if (isDirectLinkLaunch) {
            processIncomingIntentOnce(launchIntent) { candidate ->
                handleIncomingWakeupIntent(candidate)
            }
            // Do not start deferred recovery after a direct-link launch,
            // even if the direct route was rejected.
            return
        }

        // Only initial normal application launches may request deferred recovery
        if (hasQueriedDeferredInstall.compareAndSet(false, true)) {
            queryDeferredSceneParameters()
        }
    }

    /**
     * Connect this method to the actual NavHost initialization.
     * Uses currentDestination check and destination listener to safely verify graph readiness
     * without throwing IllegalStateException on unset controller.graph.
     */
    private fun connectNavigationHost() {
        val navHost = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as? NavHostFragment

        if (navHost == null) {
            Log.w(TAG, "NavHost not attached; route remains queued.")
            // For an asynchronously attached NavHost, call this
            // method again after the host has been attached.
            return
        }

        val controller = navHost.navController

        // currentDestination is nullable before a destination has been established.
        // Do not access controller.graph merely to check whether setGraph() has been called.
        if (controller.currentDestination != null) {
            onNavigationGraphReady()
            return
        }

        // If the graph/destination becomes available later,
        // mark navigation ready when a destination is established.
        val listener = object : androidx.navigation.NavController.OnDestinationChangedListener {
            override fun onDestinationChanged(
                navController: androidx.navigation.NavController,
                destination: androidx.navigation.NavDestination,
                arguments: Bundle?
            ) {
                navController.removeOnDestinationChangedListener(this)
                onNavigationGraphReady()
            }
        }

        controller.addOnDestinationChangedListener(listener)
    }

    /**
     * Captures warm-start QR scans when the Activity is reused under singleTop or singleTask.
     */
    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        processIncomingIntentOnce(intent) { candidate ->
            handleIncomingWakeupIntent(candidate)
        }
    }

    private fun processIncomingIntentOnce(incomingIntent: Intent?, process: (Intent) -> Unit) {
        val candidate = incomingIntent ?: return
        // Ignore historical intents re-delivered from recent-apps switcher or configuration changes
        if ((candidate.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) return
        if (candidate.getBooleanExtra(EXTRA_ROUTE_CONSUMED, false)) return

        if (!processingIntent.compareAndSet(false, true)) return
        try {
            process(candidate)
            candidate.putExtra(EXTRA_ROUTE_CONSUMED, true)
        } finally {
            processingIntent.set(false)
        }
    }

    private fun handleIncomingWakeupIntent(incomingIntent: Intent?): Boolean {
        if (incomingIntent?.action != Intent.ACTION_VIEW) return false
        val uri: Uri = incomingIntent.data ?: return false

        // 1. Verify trusted HTTPS origin and path
        val isTrusted =
            uri.scheme.equals("https", ignoreCase = true) &&
            uri.host.equals(TRUSTED_HOST, ignoreCase = true) &&
            uri.path == TRUSTED_PATH

        if (!isTrusted) {
            Log.w(TAG, "Rejected untrusted deep-link origin: $uri")
            return false
        }

        // 2. Reject missing or conflicting duplicate query parameters
        if (uri.getQueryParameters("route").size != 1 ||
            uri.getQueryParameters("id").size != 1) {
            Log.w(TAG, "Rejected missing or duplicate routing keys in URI")
            return false
        }

        val rawRoute = uri.getQueryParameter("route")
        val rawId = uri.getQueryParameter("id")

        val target = validateAndBuildTarget("wakeup", rawRoute, rawId)
        if (target.routeKey != ROUTE_HOME) {
            dispatchNavigationTarget(target)
            return true
        }
        return false
    }

    private fun queryDeferredSceneParameters() {
        // Application-defined timeout configuration; tune for deployed service behavior
        attributionAdapter.fetchDeferredInstallParams(timeoutMs = 5000L) { jsonPayload ->
            if (jsonPayload == null) {
                Log.d(TAG, "No deferred routing parameters recovered; remaining on default view.")
                return@fetchDeferredInstallParams
            }
            val target = parseJsonRoutingPayload("deferred_install", jsonPayload)
            runOnUiThread {
                dispatchNavigationTarget(target)
            }
        }
    }

    private fun parseJsonRoutingPayload(sourcePrefix: String, rawJson: String): NavigationTarget {
        return try {
            val obj = JSONObject(rawJson)
            // Defensive normalization: map canonical 'route_key'/'target_id' with external 'route'/'id' fallback
            val rawRoute = obj.optString("route_key", "").ifEmpty { obj.optString("route", "") }
            val rawId = if (obj.has("target_id") && !obj.isNull("target_id")) {
                obj.optString("target_id", "")
            } else {
                obj.optString("id", "")
            }
            validateAndBuildTarget(sourcePrefix, rawRoute, rawId)
        } catch (e: Exception) {
            Log.w(TAG, "Malformed routing JSON payload: ${e.message}")
            NavigationTarget(sourcePrefix, ROUTE_HOME, null, false)
        }
    }

    /**
     * Enforces strict allowlist and regex bounds on untrusted routing parameters.
     */
    private fun validateAndBuildTarget(
        sourcePrefix: String,
        rawRoute: String?,
        rawId: String?
    ): NavigationTarget {
        val cleanRoute = if (rawRoute != null && ALLOWED_ROUTES.contains(rawRoute)) {
            rawRoute
        } else {
            ROUTE_HOME
        }

        if (cleanRoute == ROUTE_HOME) {
            return NavigationTarget(sourcePrefix, ROUTE_HOME, null, false)
        }

        // Entity-specific routes strictly require a valid, sanitized targetId
        val cleanId = if (rawId != null && SAFE_ID_REGEX.matches(rawId)) {
            rawId
        } else {
            null
        }

        if (cleanId == null) {
            Log.w(TAG, "Route $cleanRoute rejected due to missing or invalid target_id.")
            return NavigationTarget(sourcePrefix, ROUTE_HOME, null, false)
        }

        val needsAuth = PROTECTED_ROUTES.contains(cleanRoute)
        return NavigationTarget("${sourcePrefix}_${cleanRoute}_${cleanId}", cleanRoute, cleanId, needsAuth)
    }

    /**
     * Invoked when the application's NavHost / NavController graph is attached and ready.
     */
    fun onNavigationGraphReady() {
        navigationReady = true
        flushPendingNavigation()
    }

    private fun flushPendingNavigation() {
        if (!navigationReady || isFinishing || isDestroyed) return
        val target = pendingUiTarget.getAndSet(null) ?: return
        dispatchReadyNavigationTarget(target)
    }

    private fun dispatchNavigationTarget(target: NavigationTarget) {
        if (isFinishing || isDestroyed) return

        // Stage route if the UI navigation graph has not finished inflating
        if (!navigationReady) {
            pendingUiTarget.set(target)
            return
        }

        dispatchReadyNavigationTarget(target)
    }

    private fun dispatchReadyNavigationTarget(target: NavigationTarget) {
        if (target.routeKey == ROUTE_HOME) {
            navigateToHomeScreen()
            return
        }

        // Check authentication gate for protected destinations
        if (target.requiresAuth && !attributionAdapter.isUserAuthenticated()) {
            Log.d(TAG, "Protected route ${target.routeKey} staged pending user authentication.")
            PendingRouteStore.stage(target)
            navigateToLoginScreen()
            return
        }

        executeScreenTransition(target.routeKey, target.targetId)
    }

    /**
     * Invoked by the authentication controller after login or registration completes.
     */
    fun onUserAuthenticationCompleted() {
        val pending = PendingRouteStore.consume()
        if (pending == null) {
            navigateToHomeScreen()
            return
        }

        if (isFinishing || isDestroyed) return

        if (!attributionAdapter.isUserAuthenticated()) {
            PendingRouteStore.stage(pending)
            navigateToLoginScreen()
            return
        }

        val validated = validateAndBuildTarget(
            pending.eventKey,
            pending.routeKey,
            pending.targetId
        )

        if (validated.routeKey == ROUTE_HOME) {
            navigateToHomeScreen()
            return
        }

        executeScreenTransition(validated.routeKey, validated.targetId)
    }

    private fun executeScreenTransition(routeKey: String, targetId: String?) {
        // Implementation: perform backend-authorized resource fetch and invoke NavController.navigate()
        Log.d(TAG, "Dispatching verified screen transition: route=$routeKey, id=$targetId")
    }

    private fun navigateToLoginScreen() {
        // Implementation: transition to LoginFragment / LoginActivity
    }

    private fun navigateToHomeScreen() {
        // Implementation: display default home screen
    }
}

/**
 * Application-owned adapter skeleton. Replace stubbed methods with verified OpoInstall SDK
 * callbacks, Google Play Install Referrer queries, and your production session manager.
 */
class AttributionRoutingAdapter {
    fun fetchDeferredInstallParams(timeoutMs: Long, callback: (String?) -> Unit) {
        // Bind to verified SDK install parameter API or Play Install Referrer in production
        callback(null)
    }

    fun isUserAuthenticated(): Boolean {
        // Bind to production authentication session state
        return false
    }
}

Segurança e Diagnóstico: Prevenindo Injeção de Rotas e Quedas de Navegação

Defendendo-se contra Injeção de Rotas por Deep Link e Exploits de Redirecionamento Aberto conforme as Diretrizes da OWASP

Parâmetros extraídos de QR codes ou retornos de chamada de atribuição adiada representam entradas externas não confiáveis. Em alinhamento com o Padrão de Verificação de Segurança de Aplicativos Móveis da OWASP e as Diretrizes e Riscos de Segurança do Top 10 Móvel da OWASP, os roteadores de clientes devem impor três limites defensivos:

  • Proibir o Carregamento Arbitrário de URLs em WebViews: Nunca permita que um parâmetro de roteamento (?route=webview&url=https://...) carregue uma URL externa não validada dentro de um WebView de aplicativo interno. Os invasores podem codificar URLs de phishing em um QR code, explorando o frame de UI nativo do aplicativo para coletar credenciais.
  • Rejeitar Poluição de Parâmetros Duplicados: Se uma URL manipulada contiver várias chaves conflitantes (?route=home_default&route=room_lobby), rejeite a URI em vez de permitir que o analisador escolha um primeiro ou último elemento arbitrário.
  • Aplicar Autorização de Recursos no Backend: Passar target_id=sku_8842 apenas instrui o cliente sobre qual visualização solicitar. Quando a tela consulta a API do backend, o servidor deve verificar independentemente se a sessão do usuário atual está autorizada a acessar essa entidade, prevenindo vulnerabilidades de Referência Direta Insegunda a Objetos (IDOR).

Diagnosticando Condições de Corrida de Navegação entre Partida a Frio e Partida a Quente

Durante uma partida a frio, intents de deep link ou retornos de chamada de SDK em cache podem ser resolvidos antes que o NavHostFragment principal ou o grafo de navegação raiz termine de inflar. Tentar chamar navController.navigate() antes que a hierarquia de visualização esteja pronta causa exceções em tempo de execução ou transições descartadas. Sempre adie a execução da navegação até que a hierarquia de visualização do host esteja anexada e ativa (!isFinishing && !isDestroyed).

Lidando com Promoções Expiradas, SKUs Excluídos e Parâmetros Ausentes de Forma Elegante

QR codes físicos frequentemente permanecem implantados em lojas de varejo ou mídia impressa após o término de uma promoção digital ou a descontinuação de um SKU de produto. Quando a tela de destino busca dados para target_id e recebe uma resposta HTTP 404 Not Found ou 410 Gone do backend, o cliente deve capturar o erro, exibir um aviso informativo claro para o usuário e navegar limpidamente para home_default.

Lista de Verificação de Verificação Pré-Implantação para Roteamento de QR com Estado Dual

Antes de liberar material físico de QR, as equipes de engenharia devem executar o seguinte protocolo de verificação de sete pontos em dispositivos físicos:

  • Etapa 1: Ativação em Partida a Frio com App Instalado: Force o fechamento do aplicativo instalado e escaneie o QR code. Confirme se o SO abre o aplicativo diretamente na tela de destino (product_detail).
  • Etapa 2: Ativação em Partida a Quente com App Instalado e Escaneamento Repetido: Com o aplicativo aberto em uma aba secundária, coloque o aplicativo em segundo plano e escaneie o QR code. Confirme se onNewIntent direciona para a tela de destino. Navegue para longe, escaneie exatamente o mesmo QR code uma segunda vez e confirme se ele navega novamente em vez de ser bloqueado por uma desduplicação excessivamente ampla.
  • Etapa 3: Recuperação Adiada Desinstalada (Rota Pública): Desinstale o aplicativo, escaneie o QR code, conclua a instalação pela loja e abra o aplicativo. Confirme se o retorno de chamada de parâmetro adiado normaliza o payload e direciona para product_detail.
  • Etapa 4: Recuperação Adiada Desinstalada com Portão de Autenticação (Rota Protegida): Desinstale o aplicativo, escaneie um QR code direcionado a room_lobby, instale e inicie com a sessão desconectada. Confirme se o aplicativo direciona para o Login primeiro e, ao concluir o login, consome o PendingRouteStore e navega para room_lobby.
  • Etapa 5: Rejeição de Origem Não Confiável e Chave Duplicada: Acione uma intent ACTION_VIEW a partir de um host não confiável ou com parâmetros de consulta duplicados (?route=product_detail&route=room_lobby). Confirme se handleIncomingWakeupIntent rejeita a intent e registra um aviso.
  • Etapa 6: Idempotência de Rotação de Tela: Após chegar a uma tela acessada por deep link, roticione o dispositivo para acionar a recriação da Activity. Confirme se a flag EXTRA_ROUTE_CONSUMED impede que a intent original seja reexecutada.
  • Etapa 7: Fallback Gracioso Offline / Sem Correspondência: Inicie uma instalação limpa no modo avião. Confirme se o tempo limite de recuperação é resolvido sem bloquear a thread de UI e direciona o usuário para home_default.

Análise Comparativa de Mecanismos de Roteamento In-App

Avaliando Esquemas de URL, Universal Links e App Links, e Restauração de Cena Adiada

As plataformas móveis fornecem mecanismos distintos para direcionar os usuários para visualizações de aplicativos em diferentes estados de instalação:

Mecanismo de Roteamento Estado do App de Destino Requisito de Verificação do SO Sobrevivência de Parâmetros Através da Instalação pela Loja Principal Restrição de Engenharia
Custom URL Scheme Apenas instalado Nenhum (Apenas declaração de manifesto) Não (Falha se o app não estiver instalado) Aciona pop-ups de erro do navegador se ausente; vulnerável a sequestro de esquema
Universal Links / App Links Primário instalado iOS: Arquivo AASA + permissão Associated Domains; Android: assetlinks.json + certificado de assinatura de app correspondente e configuração de filtro de intenção verificada Não (Requer camada adiada para novas instalações) Pode ser suprimido por determinados contêineres de webview in-app restritos
Google Play Install Referrer Novas instalações Android Caminho de distribuição oficial da Google Play Store Sim (String de referência suportada) Específico da Google Play Store para Android; não cobre iOS ou lojas que não sejam da Play
Restauração de Cena Assistida por SDK Instalado e Novas Instalações Associação de domínio + adaptador de SDK verificado Condicional ao mecanismo de plataforma suportado Requer validação de lista de permissões de rotas e revisão de privacidade da plataforma

Compensações Arquiteturais entre Estados de Instalação e Sistemas Operacionais

Esquemas de URL personalizados (myapp://product/123) carecem de verificação criptográfica de propriedade de domínio, o que significa que qualquer aplicativo pode registrar o mesmo esquema e interceptar links. Além disso, escanear um QR code de esquema personalizado bruto quando o aplicativo não está instalado resulta em um erro de navegador irrecuperável.

Universal Links e Android App Links verificam a propriedade de domínio via HTTPS e recorrem a uma landing page web quando o aplicativo está ausente, tornando-se a fundação necessária para o roteamento de QR. Combinar Universal Links e App Links para ativação quando instalado com recuperação adiada suportada pela plataforma (como o Google Play Install Referrer no Android e adaptadores de SDK revisados quanto à privacidade) permite a restauração de cena de estado dual a partir de um único ativo de QR.

Perguntas Frequentes (FAQ)

Como os parâmetros de QR personalizados abrem uma página específica no aplicativo se ele ainda não estiver instalado?
Quando o aplicativo não está instalado, o QR code abre uma página de destino web que registra os parâmetros de roteamento antes de direcionar o usuário para a loja de aplicativos. No primeiro lançamento após a instalação, o aplicativo tenta recuperar os parâmetros por meio de um mecanismo de recuperação adiada suportado pela plataforma, valida a rota de destino e navega para a tela designada.
Por que a leitura do meu QR code abre o aplicativo na tela inicial em vez da página de destino?
O direcionamento para a tela inicial normalmente ocorre por três motivos: a Activity do Android está configurada como `singleTop` ou `singleTask` sem sobrescrever `onNewIntent` para capturar intents de partida a quente, o roteador de navegação foi executado antes que o grafo de UI principal terminasse de ser montado, ou a tela de destino exigia autenticação do usuário e descartou a rota pendente durante o fluxo de login.
Como evito que QR codes maliciosos direcionem os usuários para telas internas não autorizadas?
Nunca passe caminhos de URL brutos diretamente para o seu controlador de navegação. Verifique o esquema, o host e o caminho da URI de entrada, imponha uma lista de permissões rígida no lado do cliente de chaves de rota permitidas, valide formatos de ID de entidade usando expressões regulares e verifique o estado de autenticação do cliente e as permissões de recursos no lado do servidor antes de renderizar telas protegidas.

Resumo e Estrutura de Decisão

Para criar QR codes para roteamento de aplicativos com parâmetros personalizados, as equipes de engenharia devem unir strings de consulta de URL externas e compactas a controladores de navegação nativos conscientes do estado. Uma implementação completa lida tanto com a ativação direta do sistema operacional para usuários existentes quanto com a recuperação condicional de parâmetros adiados para novas instalações na loja de aplicativos.

Como a continuidade dos parâmetros através das lojas de aplicativos depende de mecanismos específicos da plataforma — como o Google Play Install Referrer no Android e fluxos de trabalho verificados e em conformidade com a privacidade no iOS — os aplicativos devem tratar os payloads de roteamento recuperados como candidatos não confiáveis. Ao combinar despachantes de rotas permitidas, enfileiramento de estado de autenticação e fallbacks graciosos para a tela inicial, as equipes podem entregar uma restauração de cena confiável sem comprometer a estabilidade do aplicativo ou as regras de privacidade da plataforma.

Os SDKs e a documentação para desenvolvedores da OpoInstall fornecem componentes para implementar o roteamento de web para app e a passagem de parâmetros. Confirme as assinaturas de API específicas, os esquemas de retorno de chamada e os comportamentos de plataforma suportados pela versão do SDK implantada antes do lançamento em produção.

Para detalhes de integração, consulte a documentação para desenvolvedores da OpoInstall, acesse o centro de downloads do SDK da OpoInstall ou configure seus domínios de roteamento de aplicativos no console de desenvolvedor da OpoInstall.

Materiais Relacionados

Share this article