Cómo crear códigos QR para enrutamiento de aplicaciones con parámetros personalizados

opoinstall
2026-10-29
5 min read

¿Cómo vincular parámetros personalizados al crear códigos QR? Vincular parámetros personalizados para el enrutamiento de aplicaciones requiere adjuntar claves de consulta URL estructuradas a un enlace de página de destino alojado, capturar esas claves a través de una capa de integración web y enviar la carga útil validada a los controladores de navegación nativos tras la activación directa de la aplicación o el lanzamiento posterior a la instalación.

El enrutamiento dentro de la aplicación mediante códigos QR es una arquitectura de navegación cliente-servidor que incrusta parámetros de destino estructurados en la URL de un código de barras bidimensional. Al resolver estos parámetros a través de enlaces profundos directos del sistema operativo cuando la aplicación está instalada, o mediante la recuperación diferida compatible con la plataforma cuando la aplicación se instala recientemente desde una tienda de aplicaciones, los equipos de ingeniería pueden guiar a los usuarios directamente a pantallas específicas dentro de la aplicación en lugar de a la vista de inicio predeterminada.

Término Definición Entidad Relacionada Rol de Intención de Búsqueda
Crear código QR para aplicación El flujo de trabajo de ingeniería para codificar URLs de enrutamiento accionables en códigos de barras 2D para la navegación móvil. Restauración de Escenas Informativo / Técnico
código qr de aplicación Un recurso de código de barras configurado para activar el lanzamiento de la aplicación nativa o la incorporación en la tienda. Universal Links / App Links Técnico / Comercial
código qr dinámico Un código de barras que apunta a un punto final de enrutamiento intermediario cuya carga útil de destino se puede actualizar del lado del servidor. Indirección de URI Comercial
seguimiento de códigos qr La medición de las solicitudes de entrada de enrutamiento y las activaciones de pantallas posteriores dentro de la aplicación. Estadísticas de Canales y Seguimiento Comercial

Resumen de la arquitectura: Cómo crear un código QR para la restauración de escenas de la aplicación

El enrutamiento QR parametrizado puede restaurar una pantalla de detalle de producto dentro de una aplicación.

El problema del abandono en la pantalla de inicio: Por qué las redirecciones genéricas a la tienda interrumpen los recorridos de alta intención

Los códigos QR físicos se despliegan frecuentemente en entornos contextuales de alta intención: un comprador minorista escanea una etiqueta en el estante para ver las especificaciones del producto, un comensal escanea un cartel en la mesa para pedir desde una categoría de menú específica, o un jugador escanea un código de barras de invitación para unirse a una sala de juego multijugador. Cuando el código de barras codifica un enlace genérico a una tienda de aplicaciones o un dominio sin parámetros, la intención contextual del usuario se pierde durante la transición del escaneo físico al tiempo de ejecución de la aplicación nativa.

Al iniciar la aplicación, el usuario es dirigido a la pantalla de inicio predeterminada de nivel superior. Forzar al usuario a navegar manualmente por árboles de categorías complejos, volver a ingresar consultas de búsqueda o escribir identificadores de salas introduce una fricción cognitiva y mecánica sustancial. La restauración de escenas dentro de la aplicación aborda este abandono codificando el estado de destino en la URL del código QR y reconstruyendo programáticamente dicha vista objetivo dentro del cliente nativo.

Mecánica de enrutamiento de estado dual: Activación directa frente a restauración de escena diferida

Las aplicaciones instaladas usan enlaces directos mientras que las nuevas instalaciones necesitan recuperación de contexto compatible.

Cuando un usuario escanea un código QR de enrutamiento de aplicación, el dispositivo móvil se encuentra en uno de los dos estados de instalación posibles. Una arquitectura de enrutamiento completa gestiona ambos ciclos de vida a través de rutas técnicas diferenciadas:

  • Enrutamiento de activación directa (Aplicación ya instalada): Cuando la aplicación de destino ya está presente en el dispositivo, el escaneo del código QR activa un enlace profundo del sistema operativo, específicamente Apple Universal Links en iOS o Android App Links en Android. Ambos mecanismos requieren alojar archivos de asociación de dominio verificados criptográficamente: iOS requiere un archivo apple-app-site-association (AASA) vinculado a un derecho de Associated Domains de Xcode, mientras que Android requiere un archivo Digital Asset Links /.well-known/assetlinks.json que coincida con la huella digital SHA-256 del certificado de firma de la aplicación y filtros de intents de manifiesto verificados. Una vez verificado, el SO activa la aplicación y pasa el URI directamente al delegado de ciclo de vida del cliente.
  • Restauración de escena diferida (Aplicación no instalada): Cuando la aplicación no está presente, el usuario es redirigido a través de una página de destino web intermediaria hacia la tienda de aplicaciones de la plataforma. Debido a que el binario nativo aún no está instalado para recibir un intent del sistema operativo, los parámetros de enrutamiento deben prepararse antes de la redirección a la tienda y recuperarse durante el lanzamiento inicial de la aplicación (arranque en frío del Día 0) una vez que se completa la instalación.

Límites de recuperación específicos de la plataforma: Google Play Install Referrer, iOS Universal Links y adaptadores de SDK

Después de que un usuario instala una aplicación desde una tienda de aplicaciones, la recuperación de los parámetros de referencia y enrutamiento depende de la plataforma y del mecanismo implementado por la aplicación o el SDK de atribución. Pasar un parámetro route o id a través de una URL web no garantiza por sí solo que el parámetro sobreviva a la instalación desde la tienda de aplicaciones:

  • Android y Google Play: La API de Google Play Install Referrer puede proporcionar metadatos de recomendación compatibles asociados con una instalación de Play Store. Este mecanismo se aplica específicamente a las instalaciones completadas a través de la ruta oficial de Google Play Store, y no a cargas laterales de APK de terceros o tiendas alternativas. La aplicación debe configurar y probar la carga útil real del referente y verificar que los parámetros esperados sobrevivan al flujo de redirección e instalación.
  • iOS y App Store: La App Store de Apple no transfiere automáticamente los parámetros de consulta arbitrarios de las páginas de destino web a un binario de aplicación recién instalado. Los Universal Links admiten el enrutamiento directo cuando la aplicación ya está instalada y la configuración del dominio asociado es válida; por sí solos, no garantizan la recuperación diferida de parámetros después de una nueva instalación.
  • SDKs de Atribución: Utilice únicamente los mecanismos de recuperación documentados para la versión exacta del SDK y la plataforma que se esté implementando. Verifique la implementación, la recolección de datos y las implicaciones de privacidad en lugar de asumir que un SDK puede recuperar cada parámetro de enrutamiento en cualquier navegador y ruta de instalación.

Atribución en iOS y límite de privacidad: Cumplimiento de las reglas anti-huella digital de Apple

Las implementaciones de enrutamiento y restauración de escenas diferidas en iOS no deben depender de la toma de huellas digitales del dispositivo prohibida para identificar a un usuario o dispositivo en aplicaciones o sitios web. Bajo las Directrices de Privacidad y Uso de Datos del Usuario de Apple y el Acuerdo de Licencia del Programa de Desarrolladores de Apple, los desarrolladores tienen estrictamente prohibido derivar un identificador único a partir de señales del dispositivo, navegador, configuración, ubicación o red para este propósito. La autorización de Transparencia de Seguimiento de Aplicaciones (ATT) no hace que la creación de huellas digitales prohibidas sea permisible.

Utilice Universal Links para el enrutamiento directo verificado de la aplicación cuando esta se encuentre instalada. Para la restauración de escenas posteriores a la instalación en iOS, utilice un mecanismo documentado cuyo comportamiento haya sido revisado para la versión del SDK implementada y los requisitos aplicables de Apple. Revise las divulgaciones de privacidad del SDK y las declaraciones de API de motivo requerido cuando corresponda, pero no trate la presencia de un Manifiesto de Privacidad (PrivacyInfo.xcprivacy) como prueba de que una implementación de atribución cumple con las normativas.

Si el SDK seleccionado se basa en un método de coincidencia que no se puede verificar como permitido, no recomiende dicho método para la implementación en iOS. Cuando ningún mecanismo permitido pueda establecer el contexto de enrutamiento diferido de manera confiable, preserve el flujo de incorporación normal, dirija al usuario a una vista de inicio predeterminada segura y marque la instalación como no atribuida en lugar de fabricar una coincidencia no verificada.

Visualización de la canalización de restauración de escena QR de estado dual

El diagrama de arquitectura a continuación ilustra las siguientes etapas a medida que un código QR parametrizado se ramifica hacia la activación directa del SO o la recuperación diferida en la tienda antes de converger en el validador de rutas del lado del cliente y la puerta de autenticación:

[El usuario escanea el código QR] ──> URL (?route=product_detail&id=sku_8842&channelCode=pos_01)
          │
     ┌────┴────────────────────────────────────────┐
     ▼ (Aplicación ya instalada)                   ▼ (Aplicación no instalada)
[OS Universal Link / App Link]               [Página de destino H5 intermediaria]
     │                                             │
     ▼                                             ▼
[Devolución de llamada de activación caliente/fría]  [Descarga en tienda y primer lanzamiento]
(onNewIntent / continueUserActivity)         (Recuperación diferida compatible con la plataforma)
     │                                             │
     └────────────────────┬────────────────────────┘
                          ▼
           [Validador de ruta del cliente y puerta de autenticación]
                          │
     ┌────────────────────┴────────────────────────┐
     ▼ (Incluido en lista blanca y autenticado)    ▼ (Ruta inválida / No coincidente)
[Navegar a la pantalla de detalle del producto]    [Volver a la pantalla de inicio predeterminada]

Paso 1: Diseño del esquema de parámetros de enrutamiento personalizado

Estructuración de cargas útiles de enrutamiento accionables frente a etiquetas de atribución pasivas

Al crear un código QR para el enrutamiento de aplicaciones, los equipos de ingeniería deben establecer un contrato de datos limpio de dos niveles que separe las claves de consulta URL externas y compactas de la carga útil JSON interna y normalizada:

Nivel del Contrato Convención de Nomenclatura de Campos Representación de Ejemplo Propósito Arquitectónico
Nivel de consulta URL externa route, id, channelCode ?route=product_detail&id=sku_8842&channelCode=pos_01 Minimiza la longitud de los caracteres del URI para mantener la matriz del código QR impreso compacta
Nivel interno normalizado route_key, target_id, channel_code, fallback_route {"route_key": "product_detail", "target_id": "sku_8842", ...} Objeto canónico estandarizado validado por los enrutadores H5 y de clientes nativos
Registro de seguridad del cliente PROTECTED_ROUTES (Constante interna) setOf("room_lobby", "promo_campaign") Determina los requisitos de autenticación internamente; nunca se confía en URLs externas

Los parámetros de la URL QR se normalizan en contextos separados de navegación y atribución.

En este modelo de dos niveles:

  • Etiquetas de atribución pasivas (channelCode →\to channel_code) son consumidas por las canalizaciones analíticas para medir el volumen de conversión de marketing sin alterar el estado de la interfaz de usuario del cliente.
  • Parámetros de enrutamiento accionables (route →\to route_key e id →\to target_id) indican al controlador de navegación del cliente nativo qué pantalla montar y qué entidad recuperar.
  • Aislamiento de políticas de autenticación: Si una ruta requiere autenticación (requires_auth), debe derivarse estrictamente del registro de rutas confiables internas de la aplicación cliente en lugar de aceptarse desde un parámetro de consulta QR externo.

Cumplimiento de la codificación por porcentajes IETF RFC 3986 y las restricciones de tamaño de la carga útil

Todos los parámetros de enrutamiento añadidos a la URL de destino del QR deben cumplir con la sintaxis genérica de URI especificada en IETF RFC 3986:

  • Evitar delimitadores anidados: No incruste cadenas JSON sin procesar (?data={"route":"product","id":123}) directamente en las cadenas de consulta de la URL. Las llaves, comillas y dos puntos no escapados rompen con frecuencia los analizadores de URL de los navegadores integrados en las aplicaciones y aumentan la densidad de los módulos del código de barras.
  • Aplanar pares clave-valor: Represente el contexto de enrutamiento externo como parámetros de consulta planos codificados por porcentaje (https://promo.example.com/open?route=product_detail&id=sku_8842&channelCode=pos_01).
  • Aplicar límites de caracteres estrictos: Restrinja los valores de route e id a caracteres alfanuméricos seguros para URLs, guiones y guiones bajos (^[A-Za-z0-9_-]{1,64}$).

Definición del contrato de lista blanca de rutas del lado del cliente

Una vez que los parámetros de la URL externa se ingieren y normalizan en la estructura de carga útil interna, deben cumplir con un contrato de esquema estricto. Si route_key no se reconoce, o si a una ruta específica de entidad (product_detail, room_lobby, promo_campaign) le falta su target_id requerido, el normalizador debe degradar la carga útil a home_default con target_id: null.

El esquema JSON a continuación define la estructura canónica para una carga útil de restauración de escena normalizada:

{
  "$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"
    }
  ]
}

Paso 2: Captura y reenvío del contexto de enrutamiento en la página de destino

Análisis de claves de enrutamiento permitidas en la página de destino H5

Cuando un usuario sin la aplicación instalada escanea el código QR, el navegador móvil abre la página de destino H5 intermediaria alojada. El script de la página de destino extrae las claves de consulta externas (route, id, channelCode), rechaza los parámetros duplicados ambiguos y los normaliza en la estructura canónica definida en el Paso 1.

El normalizador de JavaScript a continuación aplica la cardinalidad de clave única, los límites de expresiones regulares y la degradación condicional a home_default, al tiempo que trata los 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();

Integración del SDK web mediante un adaptador verificado por versión

La página de destino coordina tanto los intentos de activación inmediata de la aplicación como la preparación de parámetros diferidos. Antes de implementar un SDK web de atribución (como opoinstall.js), confirme la URL exacta del paquete de JavaScript, la firma de inicialización, la devolución de llamada de disponibilidad, el método de activación y el esquema de parámetros con la documentación oficial de la versión del SDK que se está implementando.

Mantenga el comportamiento específico del SDK detrás de un pequeño adaptador de aplicación que acepte el objeto routingPayload normalizado. Para obtener especificaciones detalladas sobre la integración web y la configuración de parámetros de canal, consulte la documentación para desarrolladores de OpoInstall.

Preservación de los parámetros de enrutamiento a través de enlaces de reserva directa a la tienda

Si el SDK web de JS no está disponible o está bloqueado por los controles de seguridad del navegador, la página de destino recurre a un enlace de redirección directa a la tienda. En implementaciones de Android que utilizan Google Play, el enlace de reserva puede codificar las claves de enrutamiento normalizadas en el parámetro referrer de Play Store para que el cliente nativo pueda recuperar el contexto de enrutamiento a través de la API de Google Play Install Referrer cuando se instala a través del flujo oficial de 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)}`;

Paso 3: Implementación del envío de rutas en el cliente nativo y restauración de escenas

Manejo de modos de lanzamiento de actividades en Android: Por qué singleTask requiere tanto onCreate como onNewIntent

En las arquitecturas de enlaces profundos de Android, la forma en que una actividad recibe intents de enrutamiento depende de su modo de lanzamiento configurado y del diseño del gráfico de navegación. Los desarrolladores pueden usar modos de lanzamiento estándar con Jetpack Navigation, singleTop o singleTask según los requisitos de su pila de tareas.

Cuando una Actividad se configura con singleTop o singleTask para evitar que se apilen instancias duplicadas durante las activaciones por enlaces profundos, se deben gestionar dos rutas de ciclo de vida distintas:

  • Arrancón en frío (onCreate): Cuando el proceso de la aplicación no se está ejecutando, el escaneo del código QR inicia la Actividad desde cero. El Intent entrante se entrega a onCreate(savedInstanceState: Bundle?).
  • Arrancón en caliente (onNewIntent): Cuando el sistema operativo reutiliza una instancia de Actividad existente bajo el modo de lanzamiento configurado y las condiciones de la pila de tareas, el Intent de enlace profundo entrante se entrega a onNewIntent(). Bajo singleTop, la reutilización ocurre solo cuando la Actividad de destino ya está en la parte superior de la pila de tareas relevante; de lo contrario, se puede crear una nueva instancia de Actividad. Implemente el manejo tanto de onCreate() como de onNewIntent(), y verifique el comportamiento real para la arquitectura de navegación de la aplicación.

Para manejar ambos estados de manera confiable sin bloquear permanentemente a un usuario que legítimamente escanea el mismo código QR por segunda vez más adelante en su sesión, la Actividad debe marcar cada instancia de Intent procesada con una bandera de consumo (EXTRA_ROUTE_CONSUMED) e ignorar los intents reentregados únicamente por lanzamientos históricos del selector de aplicaciones (Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY).

Gestión de puntos de entrada del ciclo de vida en iOS: Actividades de continuación AppDelegate frente a UISceneDelegate

En iOS, el enrutamiento directo de Universal Links y la recuperación de parámetros diferidos deben alinearse con el ciclo de vida de la interfaz de usuario configurado de la aplicación:

  • Ciclo de vida basado en escenas (UISceneDelegate): En las aplicaciones que adoptan UIScene (estándar desde iOS 13 en adelante), los Universal Links de inicio en frío llegan a scene(_:willConnectTo:options:) a través de connectionOptions.userActivities, mientras que los Universal Links de reanudación en caliente llegan a scene(_:continue:).
  • Ciclo de vida del delegado de la aplicación (UIApplicationDelegate): En las aplicaciones que no utilizan delegados de escena, los Universal Links se entregan a application(_:continue:restorationHandler:).
  • Recuperación diferida del primer lanzamiento: Independientemente de los delegados de continuación de Universal Links, la consulta de parámetros de instalación diferida se ejecuta una vez durante el inicio inicial de la aplicación después de que la jerarquía de vistas raíz esté lista.

Los desarrolladores pueden descargar paquetes de SDK de plataforma e inspeccionar plantillas de integración del ciclo de vida a través del centro de descargas del SDK de OpoInstall.

Unificación de los parámetros de activación directa y los parámetros diferidos del primer lanzamiento en un único enrutador de navegación

Para mantener un comportamiento coherente de seguridad y navegación, las arquitecturas de clientes normalizan tanto los URI de activación directa como las cargas útiles JSON de instalación diferida en un único modelo NavigationTarget antes de enviarlas al enrutador de interfaz de usuario:

  1. Verificación de origen (Intents directos): Verifique que los URI de Intent.ACTION_VIEW entrantes utilicen el esquema https, coincidan con el host asociado verificado de la aplicación (promo.example.com) y la ruta (/open), y no contengan claves de consulta duplicadas y conflictivas.
  2. Normalización de esquemas y verificación de lista blanca: Mapee las claves externas route / id o los campos internos route_key / target_id en el modelo canónico, rechazando rutas no permitidas o IDs malformados. Tenga en cuenta que la lógica de análisis de Kotlin del cliente funciona como un esqueleto de análisis ilustrativo y defensivo; en los sistemas de producción, analice las cargas útiles con respecto a las reglas completas del esquema canónico antes del envío.
  3. Protección de reentrada de vuelo único: Utilice un protector atómico por entrega para que las devoluciones de llamada concurrentes no puedan desencadenar transacciones de fragmentos superpuestas.

Gestión de puertas de autenticación: Puesta en cola de rutas de enlaces profundos pendientes hasta que se complete el inicio de sesión del usuario

Las rutas QR protegidas se pueden reanudar después de la autenticación, mientras que los destinos no válidos regresan de forma segura.

Cuando un nuevo usuario instala una aplicación desde una tienda de aplicaciones a través de un código QR y la inicia por primera vez, por lo general no está autenticado. Si el código QR apunta a una pantalla protegida (como room_lobby o promo_campaign) y el enrutador intenta navegar inmediatamente, la puerta de autenticación redirigirá al usuario a la pantalla de inicio de sesión y perderá el contexto del enlace profundo.

Para preservar la restauración de la escena a través de los límites de autenticación:

  • Búsqueda de políticas de rutas internas: Determine si la ruta requiere autenticación verificando el registro de PROTECTED_ROUTES interno del cliente en lugar de confiar en los indicadores de carga útil externa.
  • Puesta en escena atómica: Si el usuario no está autenticado, almacene el NavigationTarget validado en un PendingRouteStore (AtomicReference) atómico con ámbito de sesión y dirija al usuario al flujo de inicio de sesión. Tenga en cuenta que un PendingRouteStore en memoria se aplica al proceso de aplicación activo; si el flujo de autenticación puede abarcar la muerte del proceso, utilice un almacén local encriptado y con límite de tiempo que borre el estado al cambiar de cuenta.
  • Revalidación y envío posteriores a la autenticación: Cuando se completa el inicio de sesión, consuma atómicamente el objetivo preparado (getAndSet(null)), verifique que la sesión esté ahora autenticada, revalide los parámetros de la ruta y ejecute la transición de pantalla.

Disponibilidad del host de navegación y restauración de estados

La devolución de llamada onNavigationGraphReady() debe conectarse a la inicialización real del host de navegación de la aplicación. Cuando el gráfico de navegación se define en un NavHostFragment alojado en XML, la actividad puede establecer esta conexión después de que el host y su destino estén listos. Si el gráfico se instala de forma asíncrona, invoque la devolución de llamada solo después de que secomplete la inicialización del gráfico.

Una solicitud de navegación pendiente almacenada únicamente en un campo de Actividad puede perderse durante los cambios de configuración. Para implementaciones de producción, conserve el estado de enrutamiento no consumido en un ViewModel compatible con el ciclo de vida y utilice SavedStateHandle cuando sea necesaria la restauración ante una recreación de Actividad impulsada por el sistema o la muerte del proceso. El código que se muestra aquí es un ejemplo arquitectónico y requiere una integración de navegación y sesión específica de la aplicación.

El código de Kotlin a continuación proporciona un esqueleto de enrutamiento de Android ilustrativo con adaptadores de SDK y navegación propios de la aplicación que demuestran estos 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
    }
}

Seguridad y diagnóstico: Prevención de inyección de rutas y caídas de navegación

Defensa contra la inyección de rutas de enlaces profundos y exploits de redirección abierta según las pautas de OWASP

Los parámetros extraídos de códigos QR o devoluciones de llamada de atribución diferida representan entradas externas no confiables. En consonancia con el Estándar de Verificación de Seguridad de Aplicaciones Móviles de OWASP y los riesgos y directrices de seguridad móviles del Top 10 de OWASP, los enrutadores de clientes deben aplicar tres límites defensivos:

  • Prohibir la carga arbitraria de URLs en WebView: Nunca permita que un parámetro de enrutamiento (?route=webview&url=https://...) cargue una URL externa no validada dentro de un WebView de una aplicación interna. Los atacantes pueden codificar URLs de phishing en un código QR, aprovechando el marco de la interfaz de usuario nativa de la aplicación para recopilar credenciales.
  • Rechazar la contaminación de parámetros duplicados: Si una URL manipulada contiene múltiples claves conflictivas (?route=home_default&route=room_lobby), rechace el URI en lugar de permitir que el analizador elija un elemento arbitrario inicial o final.
  • Aplicar la autorización de recursos del backend: Pasar target_id=sku_8842 solo indica al cliente qué vista debe solicitar. Cuando la pantalla consulta la API del backend, el servidor debe verificar de forma independiente que la sesión de usuario actual esté autorizada para acceder a dicha entidad, evitando vulnerabilidades de Referencia Directa a Objetos Inseguros (IDOR).

Diagnóstico de condiciones de carrera en la navegación durante el arranque en frío frente al arranque en caliente

Durante un arranque en frío, los intents de enlaces profundos o las devoluciones de llamada de SDK almacenadas en caché pueden resolverse antes de que el NavHostFragment principal o el gráfico de navegación raíz hayan terminado de inflarse. Intentar llamar a navController.navigate() antes de que la jerarquía de vistas esté lista provoca excepciones en tiempo de ejecución o transiciones interrumpidas. Siempre difiera la ejecución de la navegación hasta que la jerarquía de vistas del host esté conectada y activa (!isFinishing && !isDestroyed).

Los códigos QR físicos a menudo permanecen desplegados en tiendas minoristas o medios impresos después de que una promoción digital ha finalizado o la referencia de un producto (SKU) ha sido retirada. Cuando la pantalla de destino recupera datos para target_id y recibe una respuesta HTTP 404 Not Found o 410 Gone del backend, el cliente debe capturar el error, mostrar un aviso informativo claro al usuario y navegar limpiamente hacia home_default.

Lista de verificación de verificación previa al mantenimiento para enrutamiento QR de estado dual

Antes de lanzar material QR físico, los equipos de ingeniería deben ejecutar el siguiente protocolo de verificación de siete puntos en dispositivos físicos:

  • Paso 1: Activación en arranque en frío instalado: Cierre a la fuerza la aplicación instalada y escanee el código QR. Confirme que el SO abre la aplicación directamente en la pantalla de destino (product_detail).
  • Paso 2: Activación en arranque en caliente instalado y escaneo repetido: Con la aplicación abierta en una pestaña secundaria, envíe la aplicación al segundo plano y escanee el código QR. Confirme que onNewIntent enrute a la pantalla de destino. Vaya a otro lugar, escanee exactamente el mismo código QR por segunda vez y confirme que vuelve a navegar en lugar de ser bloqueado por una deduplicación excesivamente amplia.
  • Paso 3: Recuperación diferida desinstalada (Ruta pública): Desinstale la aplicación, escanee el código QR, complete la instalación desde la tienda e inicie. Confirme que la devolución de llamada del parámetro diferido normaliza la carga útil y enruta a product_detail.
  • Paso 4: Recuperación diferida desinstalada con puerta de autenticación (Ruta protegida): Desinstale la aplicación, escanee un código QR dirigido a room_lobby, instale e inicie la sesión mientras está desconectado. Confirme que la aplicación se dirija primero a Iniciar sesión y, al completar el inicio de sesión, consuma PendingRouteStore y navegue a room_lobby.
  • Paso 5: Origen no confiable y rechazo de clave duplicada: Active un intent ACTION_VIEW desde un host no confiable o con parámetros de consulta duplicados (?route=product_detail&route=room_lobby). Confirme que handleIncomingWakeupIntent rechace el intent y registre una advertencia.
  • Paso 6: Idempotencia de la rotación de pantalla: Después de llegar a una pantalla vinculada profundamente, rote el dispositivo para activar la recreación de la actividad. Confirme que la marca EXTRA_ROUTE_CONSUMED evite que el intent original se vuelva a ejecutar.
  • Paso 7: Respaldo elegante sin conexión / sin coincidencias: Inicie una instalación limpia en modo avión. Confirme que el tiempo de espera de recuperación se resuelva sin bloquear el subproceso de la interfaz de usuario y dirija al usuario a home_default.

Análisis comparativo de los mecanismos de enrutamiento dentro de la aplicación

Evaluación de esquemas de URL, enlaces universales y de aplicaciones, y restauración de escenas diferidas

Las plataformas móviles proporcionan mecanismos distintos para enrutar a los usuarios a las vistas de las aplicaciones a través de diferentes estados de instalación:

Mecanismo de Enrutamiento Estado de la App de Destino Requisito de Verificación del SO Supervivencia de Parámetros en la Instalación desde la Tienda Restricción Principal de Ingeniería
Esquema de URL Personalizado Solo instalada Ninguno (Solo declaración en el manifiesto) No (Falla si la app no está instalada) Genera ventanas emergentes de error del navegador si está ausente; vulnerable al secuestro de esquemas
Universal Links / App Links Instalada de forma principal iOS: Archivo AASA + derecho Associated Domains; Android: assetlinks.json + certificado de firma de app coincidente y configuración de filtro de intent verificada No (Requiere una capa diferida para nuevas instalaciones) Puede ser suprimido por ciertos contenedores webview restringidos dentro de aplicaciones
Google Play Install Referrer Nuevas instalaciones en Android Ruta de distribución oficial de Google Play Store Sí (Cadena de referente compatible) Específico de Google Play Store para Android; no cubre iOS ni tiendas ajenas a Play
Restauración de Escenas Asistida por SDK Instalada y Nuevas Instalaciones Asociación de dominios + adaptador de SDK verificado Condicional al mecanismo de plataforma compatible Requiere validación de la lista blanca de rutas y revisión de la privacidad de la plataforma

Compromisos arquitectónicos entre estados de instalación y sistemas operativos

Los esquemas de URL personalizados (myapp://product/123) carecen de verificación de propiedad de dominios mediante criptografía, lo que significa que cualquier aplicación puede registrar el mismo esquema e interceptar los enlaces. Además, escanear un código QR con un esquema personalizado sin procesar cuando la aplicación no está instalada da como resultado un error de navegador irrecuperable.

Los Universal Links y los Android App Links verifican la propiedad del dominio a través de HTTPS y redirigen a una página de destino web cuando la aplicación está ausente, lo que los convierte en la base necesaria para el enrutamiento mediante códigos QR. La combinación de Universal Links y App Links para activaciones cuando la aplicación está instalada con una recuperación diferida compatible con la plataforma (como Google Play Install Referrer en Android y adaptadores de SDK con revisión de privacidad) permite la restauración de escenas de estado dual a partir de un único recurso de código QR.

Preguntas frecuentes (FAQ)

¿Cómo abren los parámetros QR personalizados una página específica dentro de la aplicación si aún no está instalada?
Cuando la aplicación no está instalada, el código QR abre una página de destino web que registra los parámetros de enrutamiento antes de dirigir al usuario a la tienda de aplicaciones. Tras el lanzamiento inicial posterior a la instalación, la aplicación intenta recuperar los parámetros a través de un mecanismo de recuperación diferida compatible con la plataforma, valida la ruta de destino y navega a la pantalla designada.
¿Por qué al escanear mi código QR se abre la aplicación en la pantalla de inicio en lugar de en la página de destino?
Aterrizar en la pantalla de inicio suele ocurrir por tres razones: la Actividad de Android está configurada como `singleTop` o `singleTask` sin anular `onNewIntent` para capturar los intents de arranque en caliente, el enrutador de navegación se ejecutó antes de que el gráfico principal de la interfaz de usuario terminara de montarse, o la pantalla de destino requería la autenticación del usuario y descartó la ruta pendiente durante el flujo de inicio de sesión.
¿Cómo evito que los códigos QR maliciosos dirijan a los usuarios a pantallas internas no autorizadas?
Nunca pase rutas de URL sin procesar directamente a su controlador de navegación. Verifique el esquema, el host y la ruta del URI entrante, aplique una lista blanca estricta en el lado del cliente de las claves de ruta permitidas, valide los formatos de identificación de entidad mediante expresiones regulares y compruebe tanto el estado de autenticación del cliente como los permisos de recursos del lado del servidor antes de renderizar las pantallas protegidas.

Resumen y marco de decisiones

Para crear códigos QR para el enrutamiento de aplicaciones con parámetros personalizados, los equipos de ingeniería deben conectar las cadenas de consulta de URL externas y compactas con controladores de navegación nativos conscientes del estado. Una implementación completa gestiona tanto la activación directa del sistema operativo para los usuarios existentes como la recuperación condicional de parámetros diferidos para las nuevas instalaciones desde la tienda de aplicaciones.

Debido a que la continuidad de los parámetros a través de las tiendas de aplicaciones depende de mecanismos específicos de la plataforma —como Google Play Install Referrer en Android y flujos de trabajo verificados y conformes con la privacidad en iOS—, las aplicaciones deben tratar las cargas útiles de enrutamiento recuperadas como candidatos no confiables. Mediante la combinación de despachadores de rutas en listas blancas, puesta en cola de estados de autenticación y alternativas elegantes a la pantalla de inicio, los equipos pueden ofrecer una restauración de escenas confiable sin comprometer la estabilidad de la aplicación o las reglas de privacidad de la plataforma.

Los SDKs y la documentación para desarrolladores de OpoInstall proporcionan componentes para implementar el enrutamiento web a aplicación y el paso de parámetros. Confirme las firmas de API específicas, los esquemas de devolución de llamada y los comportamientos de plataforma admitidos por la versión de su SDK desplegada antes del lanzamiento en producción.

Para obtener detalles de integración, consulte la documentación para desarrolladores de OpoInstall, acceda al centro de descargas del SDK de OpoInstall o configure sus dominios de enrutamiento de aplicaciones en la consola de desarrolladores de OpoInstall.

Materiales relacionados

Share this article