Skip to content

Conversation

@Ricardoaar
Copy link

@Ricardoaar Ricardoaar commented Jan 22, 2026

Pull Request

📝 Descripción

Se agrego soporte para todos los events de V2

Se agregó un Event Emitter Panel interactivo para facilitar el testing de eventos de widgets en los ejemplos de desarrollo. Además, se crearon dos nuevos ejemplos completos que demuestran el uso de eventos de widgets.

Cambios principales:

  1. Soporte completo para eventos V2 - Implementación de todos los eventos con formato v2:*
  2. EventEmitterPanel Component - Panel lateral con 24 eventos organizados en 8 categorías (V1 y V2)
  3. Widget Events Basic - Ejemplo básico de eventos de widget con monitor en tiempo real
  4. Widget Events Multi - Ejemplo avanzado con múltiples widgets y monitor global
  5. Correcciones de ESLint - Todos los archivos ahora pasan lint sin errores

🔗 Issues Relacionados

https://app.clickup.com/t/9013371168/PD-284

🧪 Tipo de Cambio

  • 🐛 Bug fix
  • ✨ Nueva funcionalidad
  • 💥 Breaking change
  • 📚 Documentación
  • 🔧 Refactoring
  • ⚡ Mejora de performance
  • 🧹 Limpieza de código

🧪 Testing

  • Tests pasan (pnpm test)
  • Lint pasa (pnpm lint)
  • Type check pasa (pnpm typecheck)
  • Build funciona (pnpm build)
  • Probado manualmente

Cómo probar

  1. Ejecutar pnpm dev:examples
  2. Abrir http://localhost:3000
  3. Selecciona un evento
  4. Usar el panel lateral para emitir eventos
  5. Verificar que el monitor muestra los eventos recibidos

📸 Screenshots

✅ Checklist

  • El código sigue las convenciones del proyecto
  • Revisé mi propio código
  • Agregué comentarios donde era necesario
  • No hay nuevos warnings ni errores
  • Agregué/actualicé tests
  • Actualicé la documentación si era necesario

📋 Notas Adicionales

Archivos nuevos:

  • examples/shared/EventEmitterPanel.jsx - Componente reutilizable del panel
  • examples/shared/EventEmitterPanel.css - Estilos del panel
  • examples/widget-events-basic/WidgetEventsBasic.jsx - Ejemplo básico
  • examples/widget-events-basic/WidgetEventsBasic.css - Estilos del ejemplo básico
  • examples/widget-events-multi/WidgetEventsMulti.jsx - Ejemplo multi-widget
  • examples/widget-events-multi/WidgetEventsMulti.css - Estilos del ejemplo multi

Archivos modificados:

  • examples/dev/main.jsx - Agregados nuevos ejemplos al router + correcciones ESLint
  • examples/event-versioning/EventVersioningExample.jsx - Correcciones ESLint

Eventos V2 implementados:

Autenticación:

  • v2:auth:token - Token de autenticación V2
  • v2:auth:jwt - JWT token V2

Dashboard:

  • v2:dashboard:self - Información del dashboard
  • v2:dashboard:devices:self - Device propio
  • v2:dashboard:devices:selected - Devices seleccionados
  • v2:dashboard:settings:daterange - Rango de fechas
  • v2:dashboard:settings:rt - Estado de real-time
  • v2:dashboard:settings:filters - Filtros aplicados
  • v2:dashboard:settings:refreshed - Dashboard refrescado

Widget:

  • v2:widget:ready:${widgetId} - Widget listo
  • v2:widget:loaded:${widgetId} - Widget cargado
  • v2:widget:error:${widgetId} - Error en widget
  • v2:widget:customAction:${widgetId} - Acción personalizada

Características del EventEmitterPanel:

  • ✅ 24 eventos totales organizados en 8 categorías
  • Soporte completo para eventos V1 y V2 - Incluye ambas versiones del sistema de eventos
  • ✅ Widget ID dinámico (detecta automáticamente el widgetId del contexto)
  • ✅ Formato correcto de mensajes: { event, payload }
  • ✅ Payloads realistas para cada tipo de evento
  • ✅ Feedback visual del último evento emitido
  • ✅ Panel colapsable/expandible
  • ✅ Responsive y accesible (keyboard navigation, ARIA labels)
  • ✅ Console logs para debugging

Compatibilidad:

  • ✅ Mantiene soporte completo para eventos V1 (legacy)
  • ✅ Implementa todos los eventos V2 con el formato correcto
  • ✅ Permite testing de ambas versiones simultáneamente

Todos los archivos pasan pnpm lint sin errores ni warnings.

Summary by CodeRabbit

  • New Features

    • Dual V1/V2 event compatibility with automatic cross-emission and a widget-scoped event API for isolated widget messaging
    • Hook to emit/listen widget events and provider support for optional widgetId
    • Interactive Event Emitter panel included across examples and a local examples dev server (auto-opens at localhost:3000)
  • Documentation

    • Detailed Event Versioning guide and READMEs for widget-events (basic & multi) and examples
  • Tests

    • Expanded coverage for event flows, actions, and widget-event behaviors

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

📝 Walkthrough

Walkthrough

Adds V2 event namespaces and dual V1/V2 emission, a widget-scoped event system (useWidgetEvents) with widgetId propagation, updates message handlers/actions for V2 compatibility, and introduces multiple example apps, docs, tests, and an examples dev server.

Changes

Cohort / File(s) Summary
Core Event Versioning - Constants & Types
src/context/constants.ts, src/types/index.ts
Added INBOUND_EVENTS_V2 and OUTBOUND_EVENTS_V2; introduced ReadyEventV1/ReadyEventV2 union; added widgetId to UbidotsState and SET_WIDGET_ID action type.
Event Handler & Action Updates
src/context/messageHandlers.ts, src/context/actions.ts
Message handlers emit/accept V2 events in addition to V1; added emitV2Event/postMessageWithV2 helpers; actions now post both V1 and V2 outbound events with payload conversions (devices, date ranges, realtime, etc.).
State & Provider Enhancements
src/context/UbidotsReducer.ts, src/context/ubidots.tsx
Added widgetId to reducer state and handler; UbidotsProvider accepts optional widgetId prop, sets window.widgetId, and dispatches SET_WIDGET_ID.
Hooks & Public API
src/hooks/useWidgetEvents.ts, src/hooks/useUbidotsSelections.ts, src/hooks/index.ts
New useWidgetEvents hook (emitWidgetEvent, onWidgetEvent, onAnyWidgetEvent, lifecycle ready emission); added useUbidotsWidgetId and re-exported new hook.
Examples - New & Updated Components
examples/event-versioning/*, examples/widget-events-basic/*, examples/widget-events-multi/*, examples/shared/EventEmitterPanel.*, examples/*/*.jsx
Added event-versioning, widget-events-basic, widget-events-multi examples with components, styles, and READMEs; introduced shared EventEmitterPanel and injected it into multiple existing example pages.
Examples Dev Server & Gallery
examples/dev/index.html, examples/dev/main.jsx, vite.examples.config.ts, package.json
New examples dev app (gallery UI and per-example routing), Vite config for examples, and dev:examples npm script.
Documentation
README.md, docs/EventVersioning.md, examples/README.md, various example READMEs
Added "Running Examples Locally" instructions, full EventVersioning doc and READMEs for new examples.
Tests
src/context/__tests__/*, src/hooks/__tests__/*
Expanded test coverage for actions (V1/V2 emissions), message handlers (V2 mapping and handling), reducer SET_WIDGET_ID, and comprehensive tests for useWidgetEvents and useUbidotsWidgetId.
Styling
examples/shared/EventEmitterPanel.css, examples/widget-events-*/styles.css
New CSS for EventEmitterPanel and widget examples (layout, monitors, badges, responsive rules).

Sequence Diagram(s)

sequenceDiagram
    participant Widget as Widget Component
    participant Hook as useWidgetEvents Hook
    participant Parent as Parent Window / Dashboard
    participant Registry as Listener Registry

    Widget->>Hook: emitWidgetEvent('ready', payload)
    Hook->>Parent: postMessage({ event: "v2:widget:ready:widgetId", payload })
    Parent->>Hook: postMessage({ event: "v2:widget:message:otherWidgetId", payload })
    Hook->>Registry: match event -> invoke listeners
    Registry->>Widget: listener callback(payload)
    Widget->>Widget: update UI/state
Loading
sequenceDiagram
    participant App as Application
    participant MsgHandler as messageHandlers
    participant Store as Reducer/Store
    participant Parent as Parent Window / Dashboard

    App->>MsgHandler: receive V1 message (SELECTED_DEVICE, payload)
    MsgHandler->>Store: dispatch(SELECTED_DEVICE action)
    MsgHandler->>Parent: postMessage(V2 SELECTED_DEVICES, convertedPayload)
    Parent->>App: receives V1 or V2 as applicable
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰
I twitch my whiskers, tap the key,
V1 and V2 hop joyfully,
Widgets sing and panels gleam,
Examples parade in gallery dream,
I nibble docs and sprout a scheme. 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Front/pd 284 events v2' is vague and uses an issue reference; it does not clearly describe the main change (V2 event support and EventEmitterPanel component). Revise the title to be more descriptive, e.g. 'Add V2 event support and EventEmitterPanel component' or 'Implement V2 events with interactive testing panel'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is comprehensive and covers the main changes, includes testing confirmation, related issue, type of change, and implementation details.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

🔍 PR Quality Check Summary

Check Status
Code Quality & Tests ❌ failure
Security Audit ✅ success

📋 Checks Performed:

  • ✅ TypeScript compilation
  • ✅ ESLint code quality
  • ✅ Prettier code formatting
  • ✅ Unit tests with coverage (80%+ required)
  • ✅ Build verification
  • ✅ Security audit

⚠️ Some checks failed. Please review the details above.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/context/messageHandlers.ts (1)

171-244: V2 handlers add only V1 event names to satisfiedEventsRef, breaking readiness checks when V2 event names are used in readyEvents configuration.

The V2 handlers (lines 173–244) all add V1 event names ('receivedToken', 'selectedDevices', etc.) to satisfiedEventsRef, but the ReadyEvent type explicitly allows V2 event names like 'v2:auth:token'. When a consumer configures readyEvents with V2 names, those events will never be satisfied because the handlers only track V1 names. The checkReadyState function checks whether each event in readyEvents exists in satisfiedEventsRef (line 291–293), so this mismatch causes readiness to never be reached.

Add the corresponding V2 event name to satisfiedEventsRef in each V2 handler to support V2-only configurations:

[INBOUND_EVENTS_V2.TOKEN]: (payload, dispatch, satisfiedEventsRef) => {
  dispatch({ type: ACTION_TYPES.RECEIVED_TOKEN, payload: payload as string });
  satisfiedEventsRef.current.add('receivedToken');
  satisfiedEventsRef.current.add('v2:auth:token');
},

Apply the same pattern to all other V2 handlers.

🤖 Fix all issues with AI agents
In `@examples/dev/main.jsx`:
- Around line 5-12: The imports in main.jsx are using default imports but the
example files mostly export named components; update the import statements so
they use named imports matching the actual exports: replace default imports for
BasicUsage, CompleteWidget, WidgetEventsBasic, WidgetEventsMulti, and
WithHocsExample with named imports (e.g., { BasicUsage }, { CompleteWidget }, {
WidgetEventsBasic }, { WidgetEventsMulti }, { WithHocsExample }), and fix the
two misnamed imports by changing DeviceSelector to import {
DeviceSelectorExample } and RealTimeDashboard to import {
RealTimeDashboardExample } while keeping EventVersioningExample as the default
import since it exports default.

In `@examples/shared/EventEmitterPanel.jsx`:
- Around line 8-12: The WidgetEventsBasic component uses the wrong fallback
widgetId ('widget-demo-001') which mismatches EventEmitterPanel.jsx and the
README; update the fallback identifier in WidgetEventsBasic (the widgetId
constant/prop default used when window.widgetId is absent and any hardcoded
event strings like `v2:widget:ready:${widgetId}` or listener suffixes) to
'demo-widget-001' so emitted events and the event monitor/readme examples align
with EventEmitterPanel and documentation.
- Around line 13-17: The emitEvent function currently calls
window.parent.postMessage(message, '*') which weakens origin isolation; update
the EventEmitterPanel component to accept a targetOrigin prop (or add a required
prop) and replace the '*' with that prop when calling postMessage in emitEvent,
providing a sensible default (e.g., a deployment-specific origin or
throw/console.error if missing) and ensure the prop is validated/documented so
callers pass the explicit parent origin (e.g., when rendering <EventEmitterPanel
targetOrigin="https://trusted-parent.example.com" />).

In `@examples/widget-events-multi/README.md`:
- Around line 185-188: Update the README examples run command block that
currently shows "pnpm install" followed by "pnpm dev" to instead use "pnpm
dev:examples" so the docs launch the dedicated Vite-configured examples app;
locate the fenced bash code block containing those two commands and replace the
second line accordingly.

In `@src/context/messageHandlers.ts`:
- Around line 18-22: emitV2Event currently calls window.parent.postMessage with
'*' which leaks sensitive data to any parent; update emitV2Event (and the
postMessage usage in actions.ts) to accept or resolve a specific targetOrigin
instead of '*'—use the parent origin (e.g., window.parent.location.origin) as a
sensible default or read a configured outboundOrigin injected into the provider,
and wire this to the existing validateOrigin callback so outbound messages are
only sent to validated origins; change emitV2Event signature to accept an
optional targetOrigin or use the provider's configured outboundOrigin, and use
that value in postMessage calls.

In `@src/hooks/useWidgetEvents.ts`:
- Around line 52-65: onWidgetEvent currently stores listeners under the raw
event key (e.g., "ready") but incoming events use the full "v2:widget:...:ready"
format, so listeners never match; update onWidgetEvent to normalize the event
name to the same canonical form used by emitWidgetEvent (or accept both forms)
before storing/removing the callback, and emit a console/process warning if a
required widgetId is missing when resolving to the full "v2:widget:..." name;
look for listenersRef and onWidgetEvent in the file and ensure the same
normalization/matching logic is used when emitting in emitWidgetEvent so
callbacks registered via onWidgetEvent('ready', cb) will be invoked.

In `@vite.examples.config.ts`:
- Around line 4-18: The config currently uses __dirname (in the alias
path.resolve(__dirname, './src/index.ts')) which breaks in ESM; replace usage by
deriving a directory from import.meta.url (e.g., use fileURLToPath and
path.dirname on new URL(import.meta.url)) and pass that computed dirname into
path.resolve for the alias; update the top of the file to import or require the
helper (fileURLToPath) and compute const __dirname =
path.dirname(fileURLToPath(new URL(import.meta.url))) before calling
defineConfig so that the alias resolution in resolve.alias uses the correct
ESM-compatible directory.
🧹 Nitpick comments (4)
src/context/UbidotsReducer.ts (1)

95-96: Consider removing the unnecessary intermediate variable.

The intermediate variable r adds no value and slightly reduces readability. A direct return would be cleaner and consistent with typical reducer patterns.

♻️ Suggested simplification
-  const r = handler ? handler(state, action) : state;
-  return r;
+  return handler ? handler(state, action) : state;
examples/widget-events-basic/README.md (1)

16-18: Add language specifier to fenced code block.

The linter flags this code block as missing a language specifier. Since this shows a text pattern rather than executable code, consider using text or plaintext.

♻️ Suggested fix
-```
+```text
 v2:widget:<eventType>:<widgetId>
src/context/ubidots.tsx (1)

71-77: Consider cleanup for window.widgetId on unmount or prop change.

When widgetId changes to a falsy value or the component unmounts, window.widgetId remains set with the stale value. This could cause subtle bugs if multiple widgets or provider instances are used.

♻️ Suggested cleanup
  // Set widgetId on window and in state
  useEffect(() => {
    if (widgetId) {
      (window as unknown as Record<string, unknown>).widgetId = widgetId;
      dispatch({ type: ACTION_TYPES.SET_WIDGET_ID, payload: widgetId });
+     return () => {
+       (window as unknown as Record<string, unknown>).widgetId = undefined;
+     };
    }
  }, [widgetId]);
src/context/constants.ts (1)

50-59: Consider deduplicating the shared V2 selected-devices event string.
SET_DASHBOARD_DEVICE and SET_DASHBOARD_MULTIPLE_DEVICES point to the same literal; a shared constant makes the intent explicit and prevents drift.

♻️ Suggested tweak
+const DASHBOARD_DEVICES_SELECTED_V2 = 'v2:dashboard:devices:selected';
+
 export const OUTBOUND_EVENTS_V2 = {
   // Dashboard Events
-  SET_DASHBOARD_DEVICE: 'v2:dashboard:devices:selected',
-  SET_DASHBOARD_MULTIPLE_DEVICES: 'v2:dashboard:devices:selected',
+  SET_DASHBOARD_DEVICE: DASHBOARD_DEVICES_SELECTED_V2,
+  SET_DASHBOARD_MULTIPLE_DEVICES: DASHBOARD_DEVICES_SELECTED_V2,
   SET_DASHBOARD_DATE_RANGE: 'v2:dashboard:settings:daterange',
   SET_REAL_TIME: 'v2:dashboard:settings:rt',
   REFRESH_DASHBOARD: 'v2:dashboard:settings:refreshed',
   SET_FULL_SCREEN: 'v2:dashboard:settings:fullscreen',
   OPEN_DRAWER: 'v2:dashboard:drawer:open',
 } as const;

Comment on lines +5 to +12
import BasicUsage from '../basic-usage/BasicUsage.jsx';
import DeviceSelector from '../device-selector/DeviceSelector.jsx';
import RealTimeDashboard from '../real-time-dashboard/RealTimeDashboard.jsx';
import CompleteWidget from '../complete-widget/CompleteWidget.jsx';
import EventVersioningExample from '../event-versioning/EventVersioningExample.jsx';
import WidgetEventsBasic from '../widget-events-basic/WidgetEventsBasic.jsx';
import WidgetEventsMulti from '../widget-events-multi/WidgetEventsMulti.jsx';
import WithHocsExample from '../with-hocs/WithHocsExample.jsx';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the example files and check their exports
find examples -type f -name "BasicUsage.jsx" -o \
  -name "DeviceSelector.jsx" -o \
  -name "RealTimeDashboard.jsx" -o \
  -name "CompleteWidget.jsx" -o \
  -name "EventVersioningExample.jsx" -o \
  -name "WidgetEventsBasic.jsx" -o \
  -name "WidgetEventsMulti.jsx" -o \
  -name "WithHocsExample.jsx" | sort

Repository: ubidots/react-html-canvas

Length of output: 437


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check export patterns in each file
for f in BasicUsage.jsx DeviceSelector.jsx RealTimeDashboard.jsx CompleteWidget.jsx EventVersioningExample.jsx WidgetEventsBasic.jsx WidgetEventsMulti.jsx WithHocsExample.jsx; do
  echo "=== $f ==="
  fd -a "$f" examples | while read path; do
    echo "File: $path"
    rg -n "^export\s+(default\s+)?(function|const)" "$path" | head -20
  done
done

Repository: ubidots/react-html-canvas

Length of output: 1195


Fix import-export mismatches for example components.

Seven of the eight imports use default imports, but most files export named functions. Additionally, two imports have incorrect names:

  • DeviceSelector should import { DeviceSelectorExample }
  • RealTimeDashboard should import { RealTimeDashboardExample }
  • EventVersioningExample correctly uses default import (exports default function App())

Update the imports to match the actual named exports:

Import statement corrections
-import BasicUsage from '../basic-usage/BasicUsage.jsx';
-import DeviceSelector from '../device-selector/DeviceSelector.jsx';
-import RealTimeDashboard from '../real-time-dashboard/RealTimeDashboard.jsx';
-import CompleteWidget from '../complete-widget/CompleteWidget.jsx';
import EventVersioningExample from '../event-versioning/EventVersioningExample.jsx';
-import WidgetEventsBasic from '../widget-events-basic/WidgetEventsBasic.jsx';
-import WidgetEventsMulti from '../widget-events-multi/WidgetEventsMulti.jsx';
-import WithHocsExample from '../with-hocs/WithHocsExample.jsx';
+import { BasicUsage } from '../basic-usage/BasicUsage.jsx';
+import { DeviceSelectorExample as DeviceSelector } from '../device-selector/DeviceSelector.jsx';
+import { RealTimeDashboardExample as RealTimeDashboard } from '../real-time-dashboard/RealTimeDashboard.jsx';
+import { CompleteWidget } from '../complete-widget/CompleteWidget.jsx';
+import { WidgetEventsBasic } from '../widget-events-basic/WidgetEventsBasic.jsx';
+import { WidgetEventsMulti } from '../widget-events-multi/WidgetEventsMulti.jsx';
+import { WithHocsExample } from '../with-hocs/WithHocsExample.jsx';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import BasicUsage from '../basic-usage/BasicUsage.jsx';
import DeviceSelector from '../device-selector/DeviceSelector.jsx';
import RealTimeDashboard from '../real-time-dashboard/RealTimeDashboard.jsx';
import CompleteWidget from '../complete-widget/CompleteWidget.jsx';
import EventVersioningExample from '../event-versioning/EventVersioningExample.jsx';
import WidgetEventsBasic from '../widget-events-basic/WidgetEventsBasic.jsx';
import WidgetEventsMulti from '../widget-events-multi/WidgetEventsMulti.jsx';
import WithHocsExample from '../with-hocs/WithHocsExample.jsx';
import { BasicUsage } from '../basic-usage/BasicUsage.jsx';
import { DeviceSelectorExample as DeviceSelector } from '../device-selector/DeviceSelector.jsx';
import { RealTimeDashboardExample as RealTimeDashboard } from '../real-time-dashboard/RealTimeDashboard.jsx';
import { CompleteWidget } from '../complete-widget/CompleteWidget.jsx';
import EventVersioningExample from '../event-versioning/EventVersioningExample.jsx';
import { WidgetEventsBasic } from '../widget-events-basic/WidgetEventsBasic.jsx';
import { WidgetEventsMulti } from '../widget-events-multi/WidgetEventsMulti.jsx';
import { WithHocsExample } from '../with-hocs/WithHocsExample.jsx';
🤖 Prompt for AI Agents
In `@examples/dev/main.jsx` around lines 5 - 12, The imports in main.jsx are using
default imports but the example files mostly export named components; update the
import statements so they use named imports matching the actual exports: replace
default imports for BasicUsage, CompleteWidget, WidgetEventsBasic,
WidgetEventsMulti, and WithHocsExample with named imports (e.g., { BasicUsage },
{ CompleteWidget }, { WidgetEventsBasic }, { WidgetEventsMulti }, {
WithHocsExample }), and fix the two misnamed imports by changing DeviceSelector
to import { DeviceSelectorExample } and RealTimeDashboard to import {
RealTimeDashboardExample } while keeping EventVersioningExample as the default
import since it exports default.

Comment on lines +8 to +12
// Get widgetId from window if available, otherwise use a demo ID
const widgetId =
// eslint-disable-next-line no-undef
(typeof window !== 'undefined' && window.widgetId) || 'demo-widget-001';

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, locate the files mentioned in the review
find . -name "EventEmitterPanel.jsx" -o -name "WidgetEventsBasic.jsx"

Repository: ubidots/react-html-canvas

Length of output: 194


🏁 Script executed:

# Check the content at the specified lines
echo "=== EventEmitterPanel.jsx lines 8-12 ==="
sed -n '8,12p' examples/shared/EventEmitterPanel.jsx

echo "=== WidgetEventsBasic.jsx around line 169 ==="
sed -n '165,175p' examples/widget-events-basic/WidgetEventsBasic.jsx

Repository: ubidots/react-html-canvas

Length of output: 194


🏁 Script executed:

# Search for all widget ID patterns in the codebase
rg -n "window\.widgetId|widget-demo-001|demo-widget-001" --type jsx --type js

Repository: ubidots/react-html-canvas

Length of output: 96


🏁 Script executed:

# Also search for any HTML or setup files that might set window.widgetId
rg -n "widgetId" --type html --type js --type jsx

Repository: ubidots/react-html-canvas

Length of output: 96


🏁 Script executed:

#!/bin/bash
# First list files to understand the repository structure
find . -name "EventEmitterPanel.jsx" -o -name "WidgetEventsBasic.jsx" 2>/dev/null

Repository: ubidots/react-html-canvas

Length of output: 161


🏁 Script executed:

#!/bin/bash
# Read EventEmitterPanel.jsx to check lines 8-12
if [ -f "examples/shared/EventEmitterPanel.jsx" ]; then
  echo "=== EventEmitterPanel.jsx lines 8-12 ==="
  sed -n '8,12p' examples/shared/EventEmitterPanel.jsx
else
  echo "File not found"
  find . -name "EventEmitterPanel.jsx"
fi

Repository: ubidots/react-html-canvas

Length of output: 315


🏁 Script executed:

#!/bin/bash
# Read WidgetEventsBasic.jsx around line 169
if [ -f "examples/widget-events-basic/WidgetEventsBasic.jsx" ]; then
  echo "=== WidgetEventsBasic.jsx around line 169 ==="
  sed -n '165,175p' examples/widget-events-basic/WidgetEventsBasic.jsx
else
  echo "File not found"
  find . -name "WidgetEventsBasic.jsx"
fi

Repository: ubidots/react-html-canvas

Length of output: 333


🏁 Script executed:

#!/bin/bash
# Search for widget ID patterns without file type restrictions
echo "=== Searching for widget ID patterns ==="
grep -r "window\.widgetId\|widget-demo-001\|demo-widget-001" . 2>/dev/null | head -20

Repository: ubidots/react-html-canvas

Length of output: 1163


🏁 Script executed:

#!/bin/bash
# Check what's in the examples directory
ls -la examples/ 2>/dev/null || echo "examples dir not found"

Repository: ubidots/react-html-canvas

Length of output: 883


🏁 Script executed:

#!/bin/bash
# Let's look at the full WidgetEventsBasic.jsx file to understand the context better
echo "=== Full WidgetEventsBasic.jsx ==="
cat examples/widget-events-basic/WidgetEventsBasic.jsx

Repository: ubidots/react-html-canvas

Length of output: 5396


🏁 Script executed:

#!/bin/bash
# Check the README to understand what widget ID should be used
echo "=== widget-events-basic README content ==="
cat examples/widget-events-basic/README.md

Repository: ubidots/react-html-canvas

Length of output: 3386


🏁 Script executed:

#!/bin/bash
# Check other example files to see what widget ID pattern they use
echo "=== Searching for widgetId assignments in all examples ==="
grep -n "widgetId = " examples/*/*.jsx

Repository: ubidots/react-html-canvas

Length of output: 309


🏁 Script executed:

#!/bin/bash
# Let's look at the full EventEmitterPanel.jsx to see what events it emits
cat examples/shared/EventEmitterPanel.jsx

Repository: ubidots/react-html-canvas

Length of output: 7237


🏁 Script executed:

#!/bin/bash
# Let's also check other example files that might use EventEmitterPanel
grep -l "EventEmitterPanel" examples/*/*.jsx

Repository: ubidots/react-html-canvas

Length of output: 475


Align the fallback widgetId in WidgetEventsBasic with EventEmitterPanel and documentation.

WidgetEventsBasic.jsx (line 170) uses 'widget-demo-001', but EventEmitterPanel.jsx (line 11) defaults to 'demo-widget-001'. Since EventEmitterPanel is imported and used in this example to emit widget events like v2:widget:ready:${widgetId}, the event monitor won't receive these events—it only listens for events ending with :widget-demo-001. Additionally, the widget-events-basic README.md explicitly documents examples using 'demo-widget-001'. Change WidgetEventsBasic to use 'demo-widget-001' to match the fallback and the documented examples.

Suggested fix
export function WidgetEventsBasic() {
-  const widgetId = 'widget-demo-001';
+  const widgetId = 'demo-widget-001';
🤖 Prompt for AI Agents
In `@examples/shared/EventEmitterPanel.jsx` around lines 8 - 12, The
WidgetEventsBasic component uses the wrong fallback widgetId ('widget-demo-001')
which mismatches EventEmitterPanel.jsx and the README; update the fallback
identifier in WidgetEventsBasic (the widgetId constant/prop default used when
window.widgetId is absent and any hardcoded event strings like
`v2:widget:ready:${widgetId}` or listener suffixes) to 'demo-widget-001' so
emitted events and the event monitor/readme examples align with
EventEmitterPanel and documentation.

Comment on lines +13 to +17
const emitEvent = (eventName, payload) => {
const message = { event: eventName, payload };
// eslint-disable-next-line no-undef
window.parent.postMessage(message, '*');
setLastEmitted(`${eventName} - ${new Date().toLocaleTimeString()}`);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the EventEmitterPanel.jsx file
find . -name "EventEmitterPanel.jsx" -type f

Repository: ubidots/react-html-canvas

Length of output: 108


🌐 Web query:

MDN postMessage targetOrigin security best practices wildcard

💡 Result:

Short summary + best practices for postMessage targetOrigin (MDN + security guidance)

Key rule

  • Never use "*" as targetOrigin when sending sensitive data; always specify the exact origin of the intended receiver. [1]

Sender-side recommendations

  • Call postMessage with a literal/allowed origin (e.g. "https://app.example.com"), not "*". [1][2]
  • If the destination origin can vary, compute it from a trusted source (config, URL you constructed) and never echo unvalidated input. [3]

Receiver-side recommendations

  • Always validate event.origin against a strict allowlist (compare exact origins, not substrings or regexes). [2][3]
  • Also verify event.source when you expect a particular window reference. Treat event.data as untrusted and validate its shape/content. [1][2]

Operational / defensive measures

  • Keep allowlists small and remove obsolete domains regularly. Audit/rotate domains used in messaging. [3]
  • Add an application-level handshake/token in the message payload if you need extra assurance that sender and receiver are paired. [2][3]
  • Use CSP frame-ancestors / iframe-src to limit who can embed your pages where applicable. [3]
  • Detect insecure patterns in CI (lint/CodeQL/SAST rules that flag postMessage(..., "*") and missing origin checks). [3]

References

  • MDN: Window.postMessage() (security notes). [1]
  • OWASP Web Security Testing Guide — Web Messaging / postMessage origin testing. [2]
  • Microsoft MSRC / public writeups and security guidance on postMessage misuse (examples, remediation, CI detection). [3]

If you want, I can: (a) show safe code snippets for sender + receiver, or (b) scan a snippet of your code and flag insecure postMessage usage.


🏁 Script executed:

cat -n ./examples/shared/EventEmitterPanel.jsx

Repository: ubidots/react-html-canvas

Length of output: 9141


Replace wildcard postMessage targetOrigin with explicit origin.
Line 16 uses '*' which weakens origin isolation. Per MDN security guidance, always specify the exact intended recipient origin, especially when sending sensitive data (even demo tokens establish a bad security pattern).

For this example component, the simplest approach is to accept a targetOrigin prop with a required or sensible default based on your deployment:

Suggested fix
-export function EventEmitterPanel() {
+export function EventEmitterPanel({ targetOrigin = window.location.origin } = {}) {
   const [isOpen, setIsOpen] = useState(true);
   const [lastEmitted, setLastEmitted] = useState('');

   const emitEvent = (eventName, payload) => {
     const message = { event: eventName, payload };
     // eslint-disable-next-line no-undef
-    window.parent.postMessage(message, '*');
+    window.parent.postMessage(message, targetOrigin);
     setLastEmitted(`${eventName} - ${new Date().toLocaleTimeString()}`);
   };

If the parent origin differs, pass it explicitly: <EventEmitterPanel targetOrigin="https://trusted-parent.example.com" />

🤖 Prompt for AI Agents
In `@examples/shared/EventEmitterPanel.jsx` around lines 13 - 17, The emitEvent
function currently calls window.parent.postMessage(message, '*') which weakens
origin isolation; update the EventEmitterPanel component to accept a
targetOrigin prop (or add a required prop) and replace the '*' with that prop
when calling postMessage in emitEvent, providing a sensible default (e.g., a
deployment-specific origin or throw/console.error if missing) and ensure the
prop is validated/documented so callers pass the explicit parent origin (e.g.,
when rendering <EventEmitterPanel
targetOrigin="https://trusted-parent.example.com" />).

Comment on lines +185 to +188
```bash
pnpm install
pnpm dev
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update run command to match new examples dev script.

The examples now have a dedicated Vite config. The README should point to pnpm dev:examples so readers launch the correct app.

📝 Proposed doc fix
-pnpm dev
+pnpm dev:examples
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```bash
pnpm install
pnpm dev
```
🤖 Prompt for AI Agents
In `@examples/widget-events-multi/README.md` around lines 185 - 188, Update the
README examples run command block that currently shows "pnpm install" followed
by "pnpm dev" to instead use "pnpm dev:examples" so the docs launch the
dedicated Vite-configured examples app; locate the fenced bash code block
containing those two commands and replace the second line accordingly.

Comment on lines +18 to +22
function emitV2Event(event: string, payload?: unknown): void {
if (typeof window !== 'undefined' && window.parent) {
window.parent.postMessage({ event, payload }, '*');
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the file exists and read the relevant section
cd "$(git rev-parse --root)" && head -30 src/context/messageHandlers.ts

Repository: ubidots/react-html-canvas

Length of output: 150


🏁 Script executed:

# Search for all postMessage calls in the codebase to understand the pattern
rg "postMessage" --type ts --type tsx -B 2 -A 2

Repository: ubidots/react-html-canvas

Length of output: 96


🏁 Script executed:

# Check if there's any origin validation or configuration in the codebase
rg "origin|validateOrigin|targetOrigin" --type ts --type tsx -B 2 -A 2

Repository: ubidots/react-html-canvas

Length of output: 96


🌐 Web query:

postMessage targetOrigin security best practices wildcard

💡 Result:

Short best-practices for postMessage targetOrigin and avoiding wildcard (*) risks:

  1. Never use "*" for sensitive messages — always supply the exact recipient origin (scheme + host [+ port]). Example: iframe.contentWindow.postMessage(msg, "https://app.example.com"). [1][2]

  2. On receive, validate event.origin exactly (===) against a whitelist of allowed origins — do not use substring/indexOf/loose regex matching. [2][4]

  3. Combine origin checks with source validation (event.source === expectedWindow) and message-level authentication (nonce / short-lived token / origin-embedded identifier) for high-value messages. Don’t send secrets unless you’re certain of the target. [3][4]

  4. Treat message data as untrusted: validate shape/types, reject unexpected values, and avoid eval/automatic execution of incoming content. [2][4]

  5. Minimize data exposure: send only necessary data, clear/expire tokens after use, and prefer postMessage to convey references or non-sensitive handles rather than raw secrets. [1][3]

  6. Use framing protections (CSP frame-ancestors, X-Frame-Options) and limit embedding to trusted domains to reduce attack surface. Audit and remove unused postMessage code paths. [3][6]

  7. Test for common pitfalls: wildcard targetOrigin, lax origin checks, and regex-based bypasses. Log/monitor unexpected origins/messages. [4]

References:
[1] MDN Web Docs — Window.postMessage().
[2] WHATWG / HTML Standard — Web Messaging (security guidance).
[3] Microsoft Security Response Center — postMessage incidents & mitigations.
[4] SecureFlag / security writeups on origin-check pitfalls and mitigations.


🏁 Script executed:

cat -n src/context/messageHandlers.ts | head -35

Repository: ubidots/react-html-canvas

Length of output: 1106


🏁 Script executed:

rg "postMessage" --type ts -B 2 -A 2

Repository: ubidots/react-html-canvas

Length of output: 15666


🏁 Script executed:

rg "validateOrigin|origin" --type ts -B 1 -A 1 | head -50

Repository: ubidots/react-html-canvas

Length of output: 2895


🏁 Script executed:

rg "cross.origin|embed|widget|targetOrigin" --type ts --type md -B 2 -A 2 | head -60

Repository: ubidots/react-html-canvas

Length of output: 3353


🏁 Script executed:

cat -n src/context/actions.ts | head -50

Repository: ubidots/react-html-canvas

Length of output: 1575


🏁 Script executed:

cat -n src/context/ubidots.tsx | grep -A 20 "validateOrigin"

Repository: ubidots/react-html-canvas

Length of output: 1813


Security: Using '*' as targetOrigin in postMessage allows any parent window to receive sensitive data like tokens and device information.

The widget sends authentication tokens (receivedToken, receivedJWTToken), device IDs, and dashboard configuration to the parent window with a wildcard origin, exposing these to any script in a parent frame if the widget is embedded on a compromised page. This applies to both emitV2Event() and the postMessage() function in actions.ts.

The provider already has a validateOrigin callback for validating incoming messages—extend it to restrict outbound postMessage targets to a specific origin or make targetOrigin configurable with a sensible default (e.g., the origin of the parent window).

🤖 Prompt for AI Agents
In `@src/context/messageHandlers.ts` around lines 18 - 22, emitV2Event currently
calls window.parent.postMessage with '*' which leaks sensitive data to any
parent; update emitV2Event (and the postMessage usage in actions.ts) to accept
or resolve a specific targetOrigin instead of '*'—use the parent origin (e.g.,
window.parent.location.origin) as a sensible default or read a configured
outboundOrigin injected into the provider, and wire this to the existing
validateOrigin callback so outbound messages are only sent to validated origins;
change emitV2Event signature to accept an optional targetOrigin or use the
provider's configured outboundOrigin, and use that value in postMessage calls.

Comment on lines +52 to +65
const onWidgetEvent = useCallback(
(event: string, callback: (payload: unknown) => void) => {
if (!listenersRef.current.has(event)) {
listenersRef.current.set(event, new Set());
}
listenersRef.current.get(event)?.add(callback);

// Return cleanup function
return () => {
listenersRef.current.get(event)?.delete(callback);
};
},
[]
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Resolve event names in onWidgetEvent to avoid silent misses.
onWidgetEvent('ready', cb) won’t fire because incoming events are full v2:widget:* strings. Normalize the key (or warn when widgetId is missing) so the API aligns with emitWidgetEvent.

🐛 Proposed fix
-  const onWidgetEvent = useCallback(
-    (event: string, callback: (payload: unknown) => void) => {
-      if (!listenersRef.current.has(event)) {
-        listenersRef.current.set(event, new Set());
-      }
-      listenersRef.current.get(event)?.add(callback);
-
-      // Return cleanup function
-      return () => {
-        listenersRef.current.get(event)?.delete(callback);
-      };
-    },
-    []
-  );
+  const onWidgetEvent = useCallback(
+    (event: string, callback: (payload: unknown) => void) => {
+      const resolvedEvent =
+        event.startsWith('v2:widget:')
+          ? event
+          : widgetId
+          ? `v2:widget:${event}:${widgetId}`
+          : null;
+
+      if (!resolvedEvent) {
+        console.warn(
+          'Cannot register widget event listener: widgetId is not defined'
+        );
+        return () => {};
+      }
+
+      if (!listenersRef.current.has(resolvedEvent)) {
+        listenersRef.current.set(resolvedEvent, new Set());
+      }
+      listenersRef.current.get(resolvedEvent)?.add(callback);
+
+      return () => {
+        listenersRef.current.get(resolvedEvent)?.delete(callback);
+      };
+    },
+    [widgetId]
+  );
🤖 Prompt for AI Agents
In `@src/hooks/useWidgetEvents.ts` around lines 52 - 65, onWidgetEvent currently
stores listeners under the raw event key (e.g., "ready") but incoming events use
the full "v2:widget:...:ready" format, so listeners never match; update
onWidgetEvent to normalize the event name to the same canonical form used by
emitWidgetEvent (or accept both forms) before storing/removing the callback, and
emit a console/process warning if a required widgetId is missing when resolving
to the full "v2:widget:..." name; look for listenersRef and onWidgetEvent in the
file and ensure the same normalization/matching logic is used when emitting in
emitWidgetEvent so callbacks registered via onWidgetEvent('ready', cb) will be
invoked.

Comment on lines +4 to +18
import path from 'path';

export default defineConfig({
plugins: [
react({
jsxRuntime: 'classic',
}),
tsconfigPaths(),
],
root: 'examples/dev',
publicDir: '../../public',
resolve: {
alias: {
'@ubidots/react-html-canvas': path.resolve(__dirname, './src/index.ts'),
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and read package.json to confirm ESM setup
fd -t f package.json | head -1 | xargs cat

Repository: ubidots/react-html-canvas

Length of output: 3638


🏁 Script executed:

# Read the vite.examples.config.ts file entirely to see actual implementation
fd -t f vite.examples.config.ts | head -1 | xargs cat -n

Repository: ubidots/react-html-canvas

Length of output: 731


🏁 Script executed:

# Check if there are other vite config files for comparison
fd -t f "vite.*config\.(ts|js)"

Repository: ubidots/react-html-canvas

Length of output: 124


🏁 Script executed:

# Check vite.config.ts to see if it has similar patterns or how it handles __dirname
cat vite.config.ts

Repository: ubidots/react-html-canvas

Length of output: 1237


🏁 Script executed:

# Also check vitest.config.ts
cat vitest.config.ts

Repository: ubidots/react-html-canvas

Length of output: 900


🏁 Script executed:

# Check if there's any vite configuration that loads or transpiles config files
rg -t ts -t js "fileURLToPath|import.meta.url" --max-count 10

Repository: ubidots/react-html-canvas

Length of output: 51


🏁 Script executed:

# Check if there are any vite loaders or setup files that might handle config transpilation
fd -t f "vite" . | grep -E "\.(config|setup|loader)" | head -20

Repository: ubidots/react-html-canvas

Length of output: 148


🏁 Script executed:

# Check tsconfig.json to understand the TypeScript setup
fd -t f tsconfig.json | head -1 | xargs cat

Repository: ubidots/react-html-canvas

Length of output: 880


🏁 Script executed:

# Check if the repo uses tsx or ts-node for config loading
rg -t json "tsx|ts-node|esbuild" package.json

Repository: ubidots/react-html-canvas

Length of output: 174


🏁 Script executed:

# Test if the issue is real by checking what happens when Vite loads the config
# Look at how Vite CLI handles .ts config files - check if there are any special loaders
rg -t ts -t js "register|loader" . --max-count 5

Repository: ubidots/react-html-canvas

Length of output: 51


🏁 Script executed:

# Check if __dirname is used anywhere else in the codebase in ESM files
rg "__dirname" --type ts --type js

Repository: ubidots/react-html-canvas

Length of output: 171


🌐 Web query:

Vite TypeScript config files ESM __dirname not defined Node.js

💡 Result:

Problem

  • In ESM (what Vite uses for TS configs) CommonJS globals like __dirname/__filename are not provided by Node, so using them in vite.config.ts gives "ReferenceError: __dirname is not defined". [1][2]

Fix (recommended)

  • Use import.meta.url -> fileURLToPath -> path.dirname to recreate __filename/__dirname.

Example vite.config.ts:

import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { defineConfig } from 'vite';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

export default defineConfig({
// your config here, you can use __dirname
});

Why this works

  • ES modules expose import.meta.url (a file: URL for local modules); fileURLToPath converts it to a filesystem path and path.dirname yields the directory — the ESM-safe equivalent of __filename/__dirname. [1][3]

Notes

  • Vite loads/ preprocesses TS configs with an ESM loader, so treat vite.config.ts as ESM (and avoid relying on CommonJS globals). Vite docs mention config loading and that globals may be replaced during loading. [2]

Sources

  • Node.js ESM docs (import.meta.url / fileURLToPath). [1]
  • Vite config docs (TS/ESM config behavior). [2]
  • Common community guidance / examples (__dirname workaround). [3]

Fix __dirname usage in ESM config.

With "type": "module", __dirname is undefined and the config will crash with a ReferenceError when loaded. Derive it from import.meta.url.

🛠️ Proposed fix
-import path from 'path';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import path from 'path';
export default defineConfig({
plugins: [
react({
jsxRuntime: 'classic',
}),
tsconfigPaths(),
],
root: 'examples/dev',
publicDir: '../../public',
resolve: {
alias: {
'@ubidots/react-html-canvas': path.resolve(__dirname, './src/index.ts'),
},
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineConfig({
plugins: [
react({
jsxRuntime: 'classic',
}),
tsconfigPaths(),
],
root: 'examples/dev',
publicDir: '../../public',
resolve: {
alias: {
'@ubidots/react-html-canvas': path.resolve(__dirname, './src/index.ts'),
},
🤖 Prompt for AI Agents
In `@vite.examples.config.ts` around lines 4 - 18, The config currently uses
__dirname (in the alias path.resolve(__dirname, './src/index.ts')) which breaks
in ESM; replace usage by deriving a directory from import.meta.url (e.g., use
fileURLToPath and path.dirname on new URL(import.meta.url)) and pass that
computed dirname into path.resolve for the alias; update the top of the file to
import or require the helper (fileURLToPath) and compute const __dirname =
path.dirname(fileURLToPath(new URL(import.meta.url))) before calling
defineConfig so that the alias resolution in resolve.alias uses the correct
ESM-compatible directory.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds V2 (v2:*) event support to the library (including widget-scoped v2:widget:* events) and expands the examples app with an interactive Event Emitter panel plus new event-focused demos.

Changes:

  • Add V2 inbound/outbound event constants and update handlers/actions to emit V1+V2 for backward compatibility.
  • Introduce widget event support via useWidgetEvents and plumb widgetId through provider/state.
  • Add an examples gallery (new Vite config + dev entrypoint) and multiple new/updated examples + docs.

Reviewed changes

Copilot reviewed 30 out of 30 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
vite.examples.config.ts Adds a dedicated Vite config for running the examples gallery.
src/types/index.ts Extends ready event types to include V2 events and adds widgetId to state/actions.
src/hooks/useWidgetEvents.ts New hook for emitting/listening to v2:widget:<event>:<widgetId> messages.
src/hooks/useUbidotsSelections.ts Adds useUbidotsWidgetId selector hook.
src/hooks/index.ts Re-exports the new useWidgetEvents hook.
src/context/ubidots.tsx Adds widgetId prop and writes it into context state (and a window global).
src/context/messageHandlers.ts Adds V2 inbound support and V1→V2 “shadow emission” for inbound events.
src/context/constants.ts Adds V2 inbound/outbound event constant maps and new action type.
src/context/actions.ts Updates outbound actions to emit both V1 and V2 events.
src/context/UbidotsReducer.ts Adds SET_WIDGET_ID reducer handling.
package.json Adds dev:examples script to run examples via the new Vite config.
examples/shared/EventEmitterPanel.jsx New interactive panel to emit V1/V2/widget events for manual testing.
examples/shared/EventEmitterPanel.css Styles for the EventEmitterPanel UI.
examples/dev/index.html New examples gallery HTML entrypoint.
examples/dev/main.jsx New examples gallery/router to render selected demos via query param.
examples/widget-events-basic/WidgetEventsBasic.jsx New single-widget event example with live monitor.
examples/widget-events-basic/styles.css Styles for widget-events-basic example.
examples/widget-events-basic/README.md Documentation for widget-events-basic example.
examples/widget-events-multi/WidgetEventsMulti.jsx New multi-widget isolated-events example with global monitor.
examples/widget-events-multi/styles.css Styles for widget-events-multi example.
examples/widget-events-multi/README.md Documentation for widget-events-multi example.
examples/event-versioning/EventVersioningExample.jsx Updates/creates event versioning example and integrates the emitter panel.
examples/basic-usage/BasicUsage.jsx Integrates the emitter panel into the basic usage example.
examples/device-selector/DeviceSelector.jsx Integrates the emitter panel into the device selector example.
examples/real-time-dashboard/RealTimeDashboard.jsx Integrates the emitter panel into the real-time dashboard example.
examples/complete-widget/CompleteWidget.jsx Integrates the emitter panel into the complete widget example.
examples/with-hocs/WithHocsExample.jsx Integrates the emitter panel into the HOCs example.
examples/README.md Adds new example entries for event-versioning and widget-events demos.
docs/EventVersioning.md Adds a guide describing V1/V2 compatibility and mapping.
README.md Updates main README with local examples running instructions and links.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +47 to +50
* Listen to widget-specific events from other widgets
*
* @param event - The event name pattern to listen for
* @param callback - The callback to invoke when the event is received
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc for onWidgetEvent says it accepts an “event name pattern”, but the implementation only does exact-string matching on the provided event key. Either implement pattern/wildcard matching (e.g. prefix match) or update the docs/signature to clarify that callers must pass the full event name (like v2:widget:message:<widgetId>).

Suggested change
* Listen to widget-specific events from other widgets
*
* @param event - The event name pattern to listen for
* @param callback - The callback to invoke when the event is received
* Listen to widget-specific events from other widgets.
*
* @param event - The exact event name to listen for (e.g. "v2:widget:message:<widgetId>").
* Use "*" to listen to all widget events.
* @param callback - The callback to invoke when the event is received.

Copilot uses AI. Check for mistakes.
Comment on lines +173 to +176
[INBOUND_EVENTS_V2.TOKEN]: (payload, dispatch, satisfiedEventsRef) => {
dispatch({ type: ACTION_TYPES.RECEIVED_TOKEN, payload: payload as string });
satisfiedEventsRef.current.add('receivedToken');
},
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direct V2 handlers currently add V1 event names to satisfiedEventsRef (e.g. v2:auth:token adds receivedToken). This prevents readyEvents configured with V2 names from ever being satisfied. These handlers should add the V2 event name (or both V1+V2) to satisfiedEventsRef.

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +44
DEVICES_ALL: 'v2:dashboard:devices:self',
SELECTED_DEVICES: 'v2:dashboard:devices:selected',
SELECTED_DATE_RANGE: 'v2:dashboard:settings:daterange',
REFRESH_DASHBOARD: 'v2:dashboard:settings:refreshed',
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INBOUND_EVENTS_V2 includes DEVICES_ALL (v2:dashboard:devices:self) and REFRESH_DASHBOARD (v2:dashboard:settings:refreshed), but there are no inbound handlers for these events (so they’ll be ignored and can never satisfy readyEvents). Either add handlers (even if they only mark readiness) or remove them from the ready-event surface to avoid dead configurations.

Suggested change
DEVICES_ALL: 'v2:dashboard:devices:self',
SELECTED_DEVICES: 'v2:dashboard:devices:selected',
SELECTED_DATE_RANGE: 'v2:dashboard:settings:daterange',
REFRESH_DASHBOARD: 'v2:dashboard:settings:refreshed',
SELECTED_DEVICES: 'v2:dashboard:devices:selected',
SELECTED_DATE_RANGE: 'v2:dashboard:settings:daterange',

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +41
// Emit to parent window
window.parent.postMessage(
{
event: eventName,
payload,
},
'*'
);
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

emitWidgetEvent calls window.parent.postMessage without checking that window exists (or that window.parent is available). Other parts of the library guard this (typeof window !== 'undefined' && window.parent) to avoid crashes in non-browser/test environments; consider doing the same here (or reusing the shared postMessage helper).

Suggested change
// Emit to parent window
window.parent.postMessage(
{
event: eventName,
payload,
},
'*'
);
// Emit to parent window (guarded for non-browser environments)
if (
typeof window !== 'undefined' &&
window.parent &&
typeof window.parent.postMessage === 'function'
) {
window.parent.postMessage(
{
event: eventName,
payload,
},
'*'
);
}

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +61
export function useUbidotsWidgetId() {
const { state } = useUbidots();
return state.widgetId;
}
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This repo has thorough hook tests under src/hooks/__tests__, but the newly added useUbidotsWidgetId selector isn’t covered. Please add a unit test validating it returns the provider widgetId (and/or updates when the provider prop changes).

Copilot uses AI. Check for mistakes.
- **[Widget Events - Multi](./examples/widget-events-multi/)** ⭐ NEW - Multiple widgets with isolated event namespaces
- **[With HOCs](./examples/with-hocs/)** - Higher-Order Components usage

📖 **[Full Guide: How to Run Examples](./examples/RUNNING_EXAMPLES.md)**
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README links to ./examples/RUNNING_EXAMPLES.md, but that file doesn’t exist in the repo. This will be a broken link for users; either add the referenced guide or remove/update the link to an existing doc (e.g. examples/README.md).

Suggested change
📖 **[Full Guide: How to Run Examples](./examples/RUNNING_EXAMPLES.md)**
📖 **[Full Guide: How to Run Examples](./examples/README.md)**

Copilot uses AI. Check for mistakes.
tsconfigPaths(),
],
root: 'examples/dev',
publicDir: '../../public',
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

publicDir points to ../../public, but this repo doesn’t have a top-level public/ directory. This can cause Vite to warn or fail to serve static assets for the examples. Consider removing publicDir, setting it to false, or pointing it to an existing directory under examples/dev.

Suggested change
publicDir: '../../public',
publicDir: false,

Copilot uses AI. Check for mistakes.
Comment on lines 27 to +42
[INBOUND_EVENTS.RECEIVED_TOKEN]: (payload, dispatch, satisfiedEventsRef) => {
dispatch({ type: ACTION_TYPES.RECEIVED_TOKEN, payload: payload as string });
satisfiedEventsRef.current.add('receivedToken');
// Emit V2 event
emitV2Event(INBOUND_EVENTS_V2.TOKEN, payload);
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When handling the V1 receivedToken event you emit the V2 equivalent, but you only mark the V1 name as satisfied. If a consumer configures readyEvents with V2 names (as documented), readiness will never be reached in an iframe because the emitted V2 event is sent to the parent and won’t be re-processed here. Consider also adding the V2 event name to satisfiedEventsRef (and apply the same pattern to the other V1→V2 mappings).

Copilot uses AI. Check for mistakes.
Comment on lines +71 to +75
// Set widgetId on window and in state
useEffect(() => {
if (widgetId) {
(window as unknown as Record<string, unknown>).widgetId = widgetId;
dispatch({ type: ACTION_TYPES.SET_WIDGET_ID, payload: widgetId });
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The widgetId effect writes to window.widgetId but never cleans it up on unmount. In the examples (multiple providers/widgets on one page) this can leave stale global state and affect subsequent widgets. Consider restoring the previous value in a cleanup function or avoiding the global and relying on context/state only.

Copilot uses AI. Check for mistakes.
Comment on lines +210 to +239
const handleHeaderClick = () => setIsOpen(!isOpen);
const handleHeaderKeyDown = e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setIsOpen(!isOpen);
}
};

return (
<div className={`event-panel ${isOpen ? 'open' : 'closed'}`}>
<div
className='event-panel-header'
onClick={handleHeaderClick}
onKeyDown={handleHeaderKeyDown}
role='button'
tabIndex={0}
aria-label={isOpen ? 'Minimize panel' : 'Maximize panel'}
>
<h3>📡 Event Emitter</h3>
<button
className='toggle-btn'
onClick={e => {
e.stopPropagation();
setIsOpen(!isOpen);
}}
title={isOpen ? 'Minimize' : 'Maximize'}
>
{isOpen ? '−' : '+'}
</button>
</div>
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header wrapper is made keyboard-focusable with role='button' and an onClick, but it also contains a real <button> inside. Nested interactive controls like this can be confusing for screen readers/keyboard users. Consider making the header itself a <button> (with aria-expanded) and removing the inner button, or removing the wrapper’s button semantics and relying on a single control.

Suggested change
const handleHeaderClick = () => setIsOpen(!isOpen);
const handleHeaderKeyDown = e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setIsOpen(!isOpen);
}
};
return (
<div className={`event-panel ${isOpen ? 'open' : 'closed'}`}>
<div
className='event-panel-header'
onClick={handleHeaderClick}
onKeyDown={handleHeaderKeyDown}
role='button'
tabIndex={0}
aria-label={isOpen ? 'Minimize panel' : 'Maximize panel'}
>
<h3>📡 Event Emitter</h3>
<button
className='toggle-btn'
onClick={e => {
e.stopPropagation();
setIsOpen(!isOpen);
}}
title={isOpen ? 'Minimize' : 'Maximize'}
>
{isOpen ? '−' : '+'}
</button>
</div>
const handleHeaderClick = () => {
setIsOpen(prevIsOpen => !prevIsOpen);
};
return (
<div className={`event-panel ${isOpen ? 'open' : 'closed'}`}>
<button
type='button'
className='event-panel-header'
onClick={handleHeaderClick}
aria-label={isOpen ? 'Minimize panel' : 'Maximize panel'}
aria-expanded={isOpen}
>
<h3>📡 Event Emitter</h3>
<span
className='toggle-btn'
title={isOpen ? 'Minimize' : 'Maximize'}
>
{isOpen ? '−' : '+'}
</span>
</button>

Copilot uses AI. Check for mistakes.
@github-actions
Copy link

🔍 PR Quality Check Summary

Check Status
Code Quality & Tests ❌ failure
Security Audit ✅ success

📋 Checks Performed:

  • ✅ TypeScript compilation
  • ✅ ESLint code quality
  • ✅ Prettier code formatting
  • ✅ Unit tests with coverage (80%+ required)
  • ✅ Build verification
  • ✅ Security audit

⚠️ Some checks failed. Please review the details above.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/context/__tests__/actions.test.ts`:
- Around line 44-47: The TypeScript error comes from accessing properties on
elements of postMessageSpy.mock.calls which are typed as unknown[]; update the
v2Call lookup to assert the call shape (e.g., cast call[0] to a known type) or
introduce a small helper/type (e.g., PostMessageCall and findV2Call) and use
that when searching for OUTBOUND_EVENTS_V2.SET_DASHBOARD_DEVICE so the
expression (call[0] as PostMessageCall).event is valid and the subsequent
expect(v2Call![0].payload) remains type-safe.
- Around line 78-82: The TypeScript compiler cannot infer the tuple type from
postMessageSpy.mock.calls.find, so add a type assertion for v2Call to silence
the error and access payload safely: when locating the call where call[0].event
=== OUTBOUND_EVENTS_V2.SET_DASHBOARD_MULTIPLE_DEVICES, assert the result
(v2Call) to a compatible type (e.g., unknown/any or a typed tuple) before using
v2Call[0].payload so the test compiles; reference the v2Call variable and
OUTBOUND_EVENTS_V2.SET_DASHBOARD_MULTIPLE_DEVICES in your change.
🧹 Nitpick comments (3)
src/hooks/__tests__/useWidgetEvents.test.tsx (3)

85-116: Redundant console.warn spy.

console.warn is already mocked in beforeEach (line 12). Creating another spy here is unnecessary—you can use vi.mocked(console.warn) or simply reference the existing mock directly.

Suggested fix
     it('should warn and not emit when widgetId is not defined', async () => {
       let emitFn: ((event: string, payload?: unknown) => void) | null = null;
-      const consoleWarnSpy = vi.spyOn(console, 'warn');

       function TestComponent() {
         const { emitWidgetEvent } = useWidgetEvents();
         emitFn = emitWidgetEvent;
         return null;
       }

       render(
         <UbidotsProvider>
           <TestComponent />
         </UbidotsProvider>
       );

       await waitFor(() => {
         expect(emitFn).not.toBeNull();
       });

       // Clear any previous calls (like from ready event)
       postMessageSpy.mockClear();

       act(() => {
         emitFn!('some-event', { data: 'test' });
       });

-      expect(consoleWarnSpy).toHaveBeenCalledWith(
+      expect(console.warn).toHaveBeenCalledWith(
         'Cannot emit widget event: widgetId is not defined'
       );
       expect(postMessageSpy).not.toHaveBeenCalled();
     });

354-356: Missing cleanup for registered listener.

Unlike the test above (lines 305-329), this test doesn't store or invoke the cleanup function. While afterEach restores mocks, explicitly cleaning up listeners maintains consistency and prevents potential leaks if the test setup changes.

Suggested fix
+      let cleanup: (() => void) | null = null;
       act(() => {
-        onAnyFn!(callback);
+        cleanup = onAnyFn!(callback);
       });

       // Simulate receiving a non-widget event
       act(() => {
         window.dispatchEvent(
           new MessageEvent('message', {
             data: {
               event: 'receivedToken',
               payload: 'some-token',
             },
           })
         );
       });

       expect(callback).not.toHaveBeenCalled();
+
+      // Cleanup
+      act(() => {
+        cleanup!();
+      });
     });

449-476: Consider extracting duplicated call search logic.

The logic to find the ready event call is repeated twice (lines 451-458 and 461-467). You could extract it into a helper or use the result from waitFor directly.

Suggested simplification
       // Wait for ready event to be emitted
+      const findReadyCall = () =>
+        postMessageSpy.mock.calls.find(
+          call =>
+            call[0] &&
+            typeof call[0] === 'object' &&
+            'event' in call[0] &&
+            call[0].event === 'v2:widget:ready:lifecycle-widget'
+        );
+
       await waitFor(() => {
-        const readyCall = postMessageSpy.mock.calls.find(
-          call =>
-            call[0] &&
-            typeof call[0] === 'object' &&
-            'event' in call[0] &&
-            call[0].event === 'v2:widget:ready:lifecycle-widget'
-        );
-        expect(readyCall).toBeDefined();
+        expect(findReadyCall()).toBeDefined();
       });

-      const readyCall = postMessageSpy.mock.calls.find(
-        call =>
-          call[0] &&
-          typeof call[0] === 'object' &&
-          'event' in call[0] &&
-          call[0].event === 'v2:widget:ready:lifecycle-widget'
-      );
+      const readyCall = findReadyCall();

       expect(readyCall![0]).toMatchObject({

@github-actions
Copy link

🔍 PR Quality Check Summary

Check Status
Code Quality & Tests ✅ success
Security Audit ✅ success

📋 Checks Performed:

  • ✅ TypeScript compilation
  • ✅ ESLint code quality
  • ✅ Prettier code formatting
  • ✅ Unit tests with coverage (80%+ required)
  • ✅ Build verification
  • ✅ Security audit

🎉 All checks passed! This PR is ready for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants