Saltar al contenido principal

Recursos para desarrolladores

El plugin es extensible de principio a fin solo con PHP. Acciones, categorías, disparadores, integraciones, condiciones, variables de texto, pestañas de configuración, rutas REST y canales de entrega son todos registrables mediante filtros de WordPress — sin editar el núcleo y sin escribir JavaScript.

Hay dos caminos equivalentes:

  1. Helpers (recomendado) — funciones globales joinotify_register_*(), cargadas con el plugin.
  2. Filtros — los apply_filters() que hay debajo, cuando necesitas control total.

Registra tus extensiones en un hook normal, como plugins_loaded o init.

Haz una copia de seguridad antes

Vale la advertencia de siempre para el código en producción: respalda el sitio antes de instalar código PHP personalizado. UpdraftPlus lo hace gratis.

Helpers en tiempo de ejecución​

Se pueden llamar desde cualquier sitio — dentro de un handler de acción, de tu propio hook, de un callback REST.

HelperQué hace
joinotify_send_whatsapp_message_text( $sender, $receiver, $message, $delay )Envía un mensaje de texto; devuelve el código HTTP
joinotify_send_whatsapp_message_media( $sender, $receiver, $media_type, $media, $caption, $delay )Envía un mensaje multimedia; devuelve el código HTTP
joinotify_dispatch_notification( $args )Envía por cualquier canal registrado; devuelve un Channel_Result
joinotify_replace_placeholders( $message, $payload, $mode )Resuelve las variables {{ … }} contra un payload
joinotify_prepare_message( $message, $payload )Resuelve variables y tokens de IA
joinotify_convert_html_to_whatsapp( $message )Convierte HTML de texto enriquecido al formato de WhatsApp
joinotify_prepare_receiver( $receiver, $payload )Resuelve variables y normaliza el teléfono
joinotify_format_phone( $phone )Valida y formatea un teléfono
joinotify_get_placeholders( $integration, $trigger, $context )Lista las variables disponibles
joinotify_get_senders()Lista los remitentes registrados
joinotify_get_first_sender()Primer remitente conectado
joinotify_is_valid_sender( $sender )Comprueba si el remitente está permitido
joinotify_get_setting( $key )Lee una opción del plugin
joinotify_get_message_history( $args )Consulta el historial de mensajes
joinotify_get_workflows( $args )Lista los flujos
joinotify_get_workflow_content( $post_id )Contenido (árbol de nodos) de un flujo
joinotify_get_workflow_context( $post_id )Contexto del flujo, por ejemplo woocommerce
joinotify_find_workflow_item( $content, $item_id )Encuentra un nodo dentro del flujo
joinotify_workflow_has_content( $post_id, $type )Comprueba si el flujo tiene nodos de un tipo
joinotify_proxy_api_text_message_text_endpoint()URL del endpoint de texto de la Proxy API
joinotify_proxy_api_media_message_text_endpoint()URL del endpoint multimedia de la Proxy API
joinotify_get_proxy_api_key()Clave configurada de la Proxy API
// Resuelve las variables contra el payload y envía por el primer remitente conectado.
$texto = joinotify_replace_placeholders( '{{ wc_billing_first_name }}, ¡tu pedido va en camino!', $payload );
$sender = joinotify_get_first_sender();

if ( $sender && joinotify_is_valid_sender( $sender ) ) {
joinotify_send_whatsapp_message_text( $sender, joinotify_format_phone( '11999998888' ), $texto );
}

Registrar extensiones​

HelperFiltro correspondiente
joinotify_register_action_category( $categoria )Joinotify/Builder/Action_Categories
joinotify_register_action( $definicion )Joinotify/Builder/Actions
joinotify_register_action_handler( $slug, $cb )Joinotify/Workflow_Processor/Handle_Actions
joinotify_register_action_description( $slug, $cb )Joinotify/Builder/Action_Description
joinotify_register_integration( $integracion )Joinotify/Settings/Tabs/Integrations
joinotify_register_trigger( $contexto, $disparador )Joinotify/Builder/Get_All_Triggers
joinotify_dispatch_trigger( $hook, $integracion, $payload )disparo en tiempo de ejecución
joinotify_register_conditions( $disparador, $conds )Joinotify/Validations/Get_Action_Conditions
joinotify_register_condition_operators( $tipo, $ops )Joinotify/Conditions/Check_Condition_Type
joinotify_register_condition_value( $tipo, $cb )Joinotify/Conditions/Get_Compare_Value
joinotify_register_placeholders( $integracion, $vars )Joinotify/Builder/Placeholders_List
joinotify_register_dynamic_placeholder( $regex, $cb )Joinotify/Builder/Resolve_Dynamic_Token
joinotify_register_settings_tab( $pestana )Joinotify/Admin/Settings/Section_Tabs
joinotify_register_settings_section( $seccion )Joinotify/Admin/Settings/Schema
joinotify_register_rest_route( $ruta )Joinotify/Rest/Routes
joinotify_register_notification_channel( $id, $clase )Joinotify/Notifications/Channels

Una acción personalizada​

Una acción es un paso que el usuario arrastra al canvas. Con el settings_schema declarado, el formulario de configuración del nodo se renderiza de forma genérica — sin tocar el frontend.

joinotify_register_action( array(
'action' => 'mi_app_enviar_sms',
'title' => __( 'Mi App: enviar SMS', 'mi-textdomain' ),
'description' => __( 'Envía un SMS por Mi App.', 'mi-textdomain' ),
'category' => 'mi_app',
'category_label' => __( 'Mi App', 'mi-textdomain' ), // crea la pestaña automáticamente
'has_settings' => true,
'is_expansible' => true, // permite encadenar acciones después de esta
'context' => array(), // vacío = disponible en todos los disparadores
'default_data' => array(
'action' => 'mi_app_enviar_sms',
'title' => __( 'Mi App: enviar SMS', 'mi-textdomain' ),
'to' => '{{ wc_billing_phone }}',
'message' => '',
),
'settings_schema' => array(
array( 'key' => 'to', 'label' => __( 'Destinatario', 'mi-textdomain' ), 'component' => 'input', 'required' => true ),
array( 'key' => 'message', 'label' => __( 'Mensaje', 'mi-textdomain' ), 'component' => 'textarea', 'required' => true, 'rows' => 4 ),
),
'handler' => function( $action_data, $action, $post_id, $event_data ) {
$to = joinotify_replace_placeholders( $action_data['to'] ?? '', $event_data );
$msg = joinotify_replace_placeholders( $action_data['message'] ?? '', $event_data );

// ... llama aquí a tu pasarela de SMS ...

return true; // el handler debe devolver bool
},
) );

// La descripción que aparece bajo el título del nodo en el canvas.
joinotify_register_action_description( 'mi_app_enviar_sms', function( $data, $workflow_action ) {
return esc_html( sprintf( __( 'SMS a %s', 'mi-textdomain' ), $data['to'] ?? '' ) );
} );

Componentes aceptados en settings_schema[].component: input, textarea, number, select (con options), date, time, code, switch, attachments, además de group y repeater anidados. La clave condition controla la visibilidad condicional, con los operadores eq, neq, in, not_in, truthy y falsy.

Un disparador y su integración​

Un disparador solo aparece en el constructor cuando la integración correspondiente está registrada y activada (su opción en setting_key en yes).

joinotify_register_integration( array(
'slug' => 'mi_app',
'title' => __( 'Mi App', 'mi-textdomain' ),
'description' => __( 'Automatiza mensajes a partir de eventos de Mi App.', 'mi-textdomain' ),
'setting_key' => 'enable_mi_app_integration',
) );

joinotify_register_trigger( 'mi_app', array(
'data_trigger' => 'mi_app_pedido_pagado',
'title' => __( 'Mi App: pedido pagado', 'mi-textdomain' ),
'description' => __( 'Se dispara cuando se paga un pedido de Mi App.', 'mi-textdomain' ),
) );

// Dispáralo desde tu propio hook.
add_action( 'mi_app_pedido_pagado', function( $order_id ) {
joinotify_dispatch_trigger( 'mi_app_pedido_pagado', 'mi_app', array( 'order_id' => $order_id ) );
} );

El $payload que pasas en el disparo queda disponible para variables y condiciones en el momento del envío.

Nuevas variables de texto​

joinotify_register_placeholders( 'woocommerce', array(
'{{ order_total }}' => array(
'triggers' => array(), // vacío = todos los disparadores de esta integración
'description' => esc_html__( 'Total del pedido', 'mi-textdomain' ),
'replacement' => array(
'sandbox' => '99,99 €',
'production' => function( $payload ) {
$order = isset( $payload['order_id'] ) ? wc_get_order( $payload['order_id'] ) : null;

return $order ? wc_price( $order->get_total() ) : '';
},
),
),
) );
  • sandbox es el valor que se muestra en la vista previa del constructor; production es el que se usa en el envío, y puede ser un callable resuelto en el momento.
  • triggers lista los data_trigger en los que vale la variable. Esto se aplica también en el envío desde la versión 2.1.0: un slug equivocado hace que la variable simplemente no se resuelva.

Para variables con argumento — {{ mi_app_campo=[total] }} — registra un resolvedor con PCRE:

joinotify_register_dynamic_placeholder( '/\{\{\s*mi_app_campo=\[(.+?)\]\s*\}\}/', function( $matches, $payload ) {
return $payload['fields'][ $matches[1] ] ?? null; // null mantiene el token original
} );

Los resolvedores registrados se ejecutan antes que los nativos, así que puedes sobrescribir {{ field_id=[…] }}, {{ wc_checkout_field=[…] }}, {{ wc_download_link=[…] }} y {{ user_meta[…] }}.

Condiciones​

joinotify_register_conditions( 'mi_app_pedido_pagado', array(
'mi_app_plan' => array(
'title' => __( 'Plan de la suscripción', 'mi-textdomain' ),
'description' => __( 'Comprueba el plan del cliente.', 'mi-textdomain' ),
),
) );

joinotify_register_condition_operators( 'mi_app_plan', array( 'is', 'is_not' ) );

joinotify_register_condition_value( 'mi_app_plan', function( $value_map, $type, $payload ) {
return get_post_meta( $payload['order_id'] ?? 0, '_mi_app_plan', true );
} );

Una ruta REST​

joinotify_register_rest_route( array(
'route' => '/admin/mi-app/ping',
'methods' => 'GET',
'callback' => function ( WP_REST_Request $request ) {
return rest_ensure_response( array( 'status' => 'success', 'pong' => true ) );
},
// 'permission' => fn() => current_user_can( 'manage_options' ), // predeterminado
) );
// → GET /wp-json/joinotify/v1/admin/mi-app/ping

Un canal de entrega​

Un canal implementa Channel_Interface y pasa a recibir los mensajes de los flujos. Así es como WhatsApp Cloud, Telegram y el correo conviven en el mismo plugin.

use MeuMouse\Joinotify\Notifications\Channel_Interface;
use MeuMouse\Joinotify\Notifications\Notification_Message;
use MeuMouse\Joinotify\Notifications\Channel_Result;

class Mi_Canal implements Channel_Interface {

public function get_id() {
return 'mi_canal';
}

public function get_label() {
return __( 'Mi canal', 'mi-textdomain' );
}

public function is_configured() {
return '' !== get_option( 'mi_canal_token', '' );
}

public function get_capabilities() {
return array( 'text', 'media' );
}

public function supports( Notification_Message $message ) {
return '' !== trim( (string) $message->receiver )
&& in_array( $message->type, $this->get_capabilities(), true );
}

public function send( Notification_Message $message ) {
$ok = mi_canal_enviar( $message->receiver, $message->content );

return $ok
? Channel_Result::success( $this->get_id() )
: Channel_Result::failure( $this->get_id(), 'mi_canal_fallo', true );
}
}

joinotify_register_notification_channel( 'mi_canal', Mi_Canal::class );

Notification_Message lleva channel, type, sender, receiver, content, media_type, media_url, caption, attachments, delay, un array context y un meta libre para campos propios del servicio. Lee el meta con $message->get_meta( $clave ).

Los adjuntos no son un tipo de mensaje

Los adjuntos modifican un mensaje existente. No los incluyas en get_capabilities() — eso haría que supports() rechazara los mensajes normales. Comprueba $message->attachments en su lugar.

Y nunca descargues un enlace de permiso de descarga de WooCommerce para reenviar los bytes: cada descarga consumida es una descarga menos para el cliente. Usa el path ya resuelto, o manda el link.

El mismo diseño vale para los códigos de inicio de sesión sin contraseña, en el filtro Joinotify/Otp_Login/Channels.

Payload de los disparadores nativos​

Cada disparador nativo entrega un conjunto de parámetros en el array $payload, disponible para variables, condiciones y handlers de acción. Para leer el ID del pedido en el disparador woocommerce_new_order, por ejemplo:

$order_id = $payload['order_id'];

Tres claves existen en todos los disparadores: type (siempre trigger), hook (el gancho que disparó) e integration (el contexto — woocommerce, wordpress, elementor, wpforms…).

WooCommerce​

DisparadorCuándo se disparaParámetros adicionales
Nuevo pedido (woocommerce_new_order)Se crea un pedido, con cualquier estadoorder_id
Nuevo pedido — procesando (woocommerce_checkout_order_processed)El pedido entra en estado "procesando"order_id, status_transition
Pedido completado (woocommerce_order_status_completed)El estado pasa a "completado"order_id, status_transition
Estado cambiado (woocommerce_order_status_changed)Cualquier cambio de estado del pedidoorder_id, old_status, new_status
Reembolso total (woocommerce_order_fully_refunded)El pedido se reembolsa por completoorder_id, refund_id
Reembolso parcial (woocommerce_order_partially_refunded)El pedido se reembolsa parcialmenteorder_id, refund_id
Acceso digital concedido (woocommerce_grant_product_download_permissions)WooCommerce concede la descarga al clienteorder_id
Archivo digital descargado (woocommerce_download_product)El cliente descarga un archivo del pedidoorder_id, product_id, download_id, user_id, user_email
Entrega de productos digitales

Para flujos de entrega, usa Acceso digital concedido. Los enlaces de descarga solo existen a partir de ese momento — en disparadores anteriores las variables de descarga vienen vacías.

WooCommerce Subscriptions​

DisparadorParámetros adicionales
Suscripción creada (woocommerce_checkout_subscription_created)subscription_id, order_id, recurring_cart
Pago completado (woocommerce_subscription_payment_complete)subscription_id
Pago fallido (woocommerce_subscription_payment_failed)subscription_id
Estado cambiado (woocommerce_subscription_status_active, …_cancelled, …_expired)subscription_id, new_status

WordPress​

DisparadorCuándo se disparaParámetros adicionales
Nuevo registro (user_register)Un usuario se registrauser_id, user_data
Inicio de sesión (wp_login)Un usuario inicia sesiónuser_id, user_data
Solicitud de restablecimiento (retrieve_password)El usuario pide el enlace de restablecimientouser_id, reset_password_link
Contraseña restablecida (password_reset)El usuario completa el restablecimientouser_id
Estado de entrada cambiado (transition_post_status)Una entrada cambia de estadopost_id, post_type, post_status, old_post_status

Elementor​

DisparadorParámetros adicionales
Formulario enviado (elementor_pro/forms/new_record)id, fields, record, handler

WPForms​

DisparadorParámetros adicionales
Formulario enviado (wpforms_process_complete)id, fields, entry, form_data, entry_id
Pago por PayPal (wpforms_paypal_standard_process_complete)id, fields, entry, form_data, entry_id

Filtros útiles​

Nuevos formatos de archivo​

add_filter( 'Joinotify/Validations/Get_Mime_Types', function( $mime_types ) {
$mime_types['image'][] = 'image/svg+xml';
$mime_types['video'][] = 'video/webm';
$mime_types['document'][] = 'application/epub+zip';
$mime_types['audio'][] = 'audio/flac';

return $mime_types;
} );

Otros puntos​

FiltroPara qué sirve
Joinotify/Builder/Action_Settings_SchemaSobrescribir el esquema de una acción, incluso nativa
Joinotify/Builder/Action_Default_DataSobrescribir los datos predeterminados de una acción
Joinotify/Workflow_Processor/Delaying_ActionsRegistrar una acción que pausa el flujo
Joinotify/Workflow_Processor/Branching_ActionsRegistrar una acción que ramifica el flujo
Joinotify/Notifications/Default_ChannelCambiar el canal de entrega predeterminado
Joinotify/Notifications/Message_Before_SendModificar el mensaje antes del envío
Joinotify/Transport/ActiveForzar el transporte de WhatsApp resuelto
Joinotify/Settings/Integrations/CategoriesRegistrar una categoría propia en la pestaña Aplicaciones

Y la acción Joinotify/Notifications/Message_Sent, disparada tras cada intento de envío.

Precauciones​

  • Registra pronto. Usa plugins_loaded o init, antes del bootstrap del constructor.
  • El handler devuelve bool. Devolver otra cosa puede interrumpir el flujo sin avisar.
  • Contexto del disparador. El contexto solo aparece cuando la tarjeta de la integración está registrada y activada.
  • Todo es aditivo. Esquemas, datos predeterminados y resolvedores nativos siguen valiendo como respaldo cuando no los sobrescribes.

Referencia completa​

La referencia exhaustiva de la API de extensión, siempre alineada con la última versión, está en el repositorio del plugin: