Saltar al contenido
Chatbots de WhatsApp desde cero
Temario

Ejercicio: Formularios dentro de WhatsApp con Flows

  • ⏱ 45 minutos
  • 📶 Nivel: intermedio

Objetivo

Trabajar con un Flow de «datos de delivery» sin necesitar todavía una cuenta de Meta: validar su JSON, construir el mensaje que lo abre, leer la respuesta del cliente y validar los datos como lo haría el endpoint.

Cómo se trabaja

La parte A es práctica con la documentación y el Flow Builder (necesitas la cuenta de la lección 12, pero puedes leer el JSON sin ella). La parte B son cinco funciones de JavaScript puro que se comprueban en esta página. Un Flow es, al final, un objeto JSON: puedes revisarlo con código.

El Flow del minimarket tiene dos pantallas: DATOS (zona, dirección y referencia) y RESUMEN (confirmación, pantalla final con terminal: true).

💡 Reto extra

Añade a validarFlow la comprobación de que todo name de los campos de entrada (TextInput, Dropdown, ...) sea único dentro de su pantalla. Dos campos con el mismo name se pisan al enviar el formulario.

▶ Prueba tu código aquí (opcional)
Consola

Pasos

Marca cada paso a medida que lo completes. Tu avance se guarda en este navegador.

💡 Pista 1: Estructura del Flow

Cada pantalla es { id, terminal?, layout: { type: "SingleColumnLayout", children: [...] } }. El Footer es un hijo con on-click-action; el navigate lleva next: { type: "screen", name: "OTRA" } y el complete termina el Flow.

💡 Pista 2: response_json es texto

mensaje.interactive.nfm_reply.response_json es un string: hay que pasarlo por JSON.parse, dentro de un try/catch, porque un cliente (o un atacante) puede mandar texto roto.

💡 Pista 3: Recorrer los hijos

flow.screens.flatMap(p => p.layout.children) junta todos los componentes. Para buscar el Footer de una pantalla usa children.find(c => c.type === "Footer").

🔎 Comprueba tu trabajo

Pega tu código y pulsa Comprobar. La revisión ocurre en tu navegador; nada se envía a ningún sitio. Cada punto en rojo incluye una pista. Pega solo el JavaScript, sin las etiquetas <script>.

    ✅ Ver una solución posible

    Intenta resolverlo antes de mirar. Tu versión puede ser distinta y también correcta.

    flows.js
    function validarFlow(flow) {
      const errores = [];
      if (!flow || !flow.version) errores.push("Falta version");
      const pantallas = Array.isArray(flow && flow.screens) ? flow.screens : [];
      if (pantallas.length === 0) errores.push("No hay pantallas");
      const ids = pantallas.map((p) => p.id);
      if (new Set(ids).size !== ids.length) errores.push("Hay ids de pantalla repetidos");
      for (const p of pantallas) {
        const hijos = (p.layout && p.layout.children) || [];
        const footer = hijos.find((c) => c.type === "Footer");
        if (p.terminal && !footer) errores.push("La pantalla terminal " + p.id + " no tiene Footer");
        for (const c of hijos) {
          const accion = c["on-click-action"];
          if (accion && accion.name === "navigate") {
            const destino = accion.next && accion.next.name;
            if (!ids.includes(destino)) errores.push("navigate hacia una pantalla inexistente: " + destino);
          }
        }
      }
      return errores;
    }
    
    function armarMensajeFlow(to, flowId, flowToken, pantalla) {
      return {
        messaging_product: "whatsapp",
        recipient_type: "individual",
        to,
        type: "interactive",
        interactive: {
          type: "flow",
          body: { text: "Completa tus datos de entrega." },
          action: {
            name: "flow",
            parameters: {
              flow_message_version: "3",
              flow_token: flowToken,
              flow_id: flowId,
              flow_cta: "Pedir delivery",
              flow_action: "navigate",
              flow_action_payload: { screen: pantalla },
            },
          },
        },
      };
    }
    
    function leerRespuestaFlow(mensaje) {
      try {
        if (!mensaje || mensaje.type !== "interactive") return null;
        const i = mensaje.interactive;
        if (!i || i.type !== "nfm_reply") return null;
        const datos = JSON.parse(i.nfm_reply.response_json);
        return datos && typeof datos === "object" ? datos : null;
      } catch (e) {
        return null;
      }
    }
    
    function validarDatosDelivery(datos, zonas) {
      if (!zonas.includes(datos.zona)) return { ok: false, error: "Esa zona no tiene reparto." };
      const direccion = String(datos.direccion || "").trim();
      if (direccion.length < 8) return { ok: false, error: "La direccion es muy corta." };
      return { ok: true };
    }
    
    function formatoRecomendado(campos, necesitaValidarEnServidor) {
      if (necesitaValidarEnServidor) return "flow-con-endpoint";
      return campos >= 3 ? "flow" : "botones";
    }