-
Notifications
You must be signed in to change notification settings - Fork 0
Front/pd 284 events v2 #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
📝 WalkthroughWalkthroughAdds 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
🔍 PR Quality Check Summary
📋 Checks Performed:
|
There was a problem hiding this 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 tosatisfiedEventsRef, breaking readiness checks when V2 event names are used inreadyEventsconfiguration.The V2 handlers (lines 173–244) all add V1 event names (
'receivedToken','selectedDevices', etc.) tosatisfiedEventsRef, but theReadyEventtype explicitly allows V2 event names like'v2:auth:token'. When a consumer configuresreadyEventswith V2 names, those events will never be satisfied because the handlers only track V1 names. ThecheckReadyStatefunction checks whether each event inreadyEventsexists insatisfiedEventsRef(line 291–293), so this mismatch causes readiness to never be reached.Add the corresponding V2 event name to
satisfiedEventsRefin 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
radds 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
textorplaintext.♻️ Suggested fix
-``` +```text v2:widget:<eventType>:<widgetId>src/context/ubidots.tsx (1)
71-77: Consider cleanup forwindow.widgetIdon unmount or prop change.When
widgetIdchanges to a falsy value or the component unmounts,window.widgetIdremains 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_DEVICEandSET_DASHBOARD_MULTIPLE_DEVICESpoint 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;
| 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'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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" | sortRepository: 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
doneRepository: 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:
DeviceSelectorshould import{ DeviceSelectorExample }RealTimeDashboardshould import{ RealTimeDashboardExample }EventVersioningExamplecorrectly uses default import (exportsdefault 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.
| 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.
| // 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'; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.jsxRepository: 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 jsRepository: 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 jsxRepository: 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/nullRepository: 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"
fiRepository: 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"
fiRepository: 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 -20Repository: 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.jsxRepository: 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.mdRepository: 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/*/*.jsxRepository: 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.jsxRepository: 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/*/*.jsxRepository: 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.
| const emitEvent = (eventName, payload) => { | ||
| const message = { event: eventName, payload }; | ||
| // eslint-disable-next-line no-undef | ||
| window.parent.postMessage(message, '*'); | ||
| setLastEmitted(`${eventName} - ${new Date().toLocaleTimeString()}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, locate and read the EventEmitterPanel.jsx file
find . -name "EventEmitterPanel.jsx" -type fRepository: 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.jsxRepository: 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" />).
| ```bash | ||
| pnpm install | ||
| pnpm dev | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| ```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.
| function emitV2Event(event: string, payload?: unknown): void { | ||
| if (typeof window !== 'undefined' && window.parent) { | ||
| window.parent.postMessage({ event, payload }, '*'); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.tsRepository: 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 2Repository: 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 2Repository: 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:
-
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]
-
On receive, validate event.origin exactly (===) against a whitelist of allowed origins — do not use substring/indexOf/loose regex matching. [2][4]
-
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]
-
Treat message data as untrusted: validate shape/types, reject unexpected values, and avoid eval/automatic execution of incoming content. [2][4]
-
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]
-
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]
-
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 -35Repository: ubidots/react-html-canvas
Length of output: 1106
🏁 Script executed:
rg "postMessage" --type ts -B 2 -A 2Repository: ubidots/react-html-canvas
Length of output: 15666
🏁 Script executed:
rg "validateOrigin|origin" --type ts -B 1 -A 1 | head -50Repository: 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 -60Repository: ubidots/react-html-canvas
Length of output: 3353
🏁 Script executed:
cat -n src/context/actions.ts | head -50Repository: 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.
| 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); | ||
| }; | ||
| }, | ||
| [] | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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'), | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Find and read package.json to confirm ESM setup
fd -t f package.json | head -1 | xargs catRepository: 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 -nRepository: 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.tsRepository: ubidots/react-html-canvas
Length of output: 1237
🏁 Script executed:
# Also check vitest.config.ts
cat vitest.config.tsRepository: 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 10Repository: 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 -20Repository: 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 catRepository: 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.jsonRepository: 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 5Repository: 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 jsRepository: 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.
| 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.
There was a problem hiding this 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
useWidgetEventsand plumbwidgetIdthrough 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.
| * 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 |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
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>).
| * 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. |
| [INBOUND_EVENTS_V2.TOKEN]: (payload, dispatch, satisfiedEventsRef) => { | ||
| dispatch({ type: ACTION_TYPES.RECEIVED_TOKEN, payload: payload as string }); | ||
| satisfiedEventsRef.current.add('receivedToken'); | ||
| }, |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
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.
| 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', |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
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.
| 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', |
| // Emit to parent window | ||
| window.parent.postMessage( | ||
| { | ||
| event: eventName, | ||
| payload, | ||
| }, | ||
| '*' | ||
| ); |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
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).
| // 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, | |
| }, | |
| '*' | |
| ); | |
| } |
| export function useUbidotsWidgetId() { | ||
| const { state } = useUbidots(); | ||
| return state.widgetId; | ||
| } |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
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).
| - **[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)** |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
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).
| 📖 **[Full Guide: How to Run Examples](./examples/RUNNING_EXAMPLES.md)** | |
| 📖 **[Full Guide: How to Run Examples](./examples/README.md)** |
| tsconfigPaths(), | ||
| ], | ||
| root: 'examples/dev', | ||
| publicDir: '../../public', |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
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.
| publicDir: '../../public', | |
| publicDir: false, |
| [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); |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
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).
| // 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 }); |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
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.
| 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> |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
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.
| 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> |
🔍 PR Quality Check Summary
📋 Checks Performed:
|
There was a problem hiding this 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: Redundantconsole.warnspy.
console.warnis already mocked inbeforeEach(line 12). Creating another spy here is unnecessary—you can usevi.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
afterEachrestores 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
waitFordirectly.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({
🔍 PR Quality Check Summary
📋 Checks Performed:
🎉 All checks passed! This PR is ready for review. |
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:
v2:*🔗 Issues Relacionados
https://app.clickup.com/t/9013371168/PD-284
🧪 Tipo de Cambio
🧪 Testing
pnpm test)pnpm lint)pnpm typecheck)pnpm build)Cómo probar
pnpm dev:exampleshttp://localhost:3000📸 Screenshots
✅ Checklist
📋 Notas Adicionales
Archivos nuevos:
examples/shared/EventEmitterPanel.jsx- Componente reutilizable del panelexamples/shared/EventEmitterPanel.css- Estilos del panelexamples/widget-events-basic/WidgetEventsBasic.jsx- Ejemplo básicoexamples/widget-events-basic/WidgetEventsBasic.css- Estilos del ejemplo básicoexamples/widget-events-multi/WidgetEventsMulti.jsx- Ejemplo multi-widgetexamples/widget-events-multi/WidgetEventsMulti.css- Estilos del ejemplo multiArchivos modificados:
examples/dev/main.jsx- Agregados nuevos ejemplos al router + correcciones ESLintexamples/event-versioning/EventVersioningExample.jsx- Correcciones ESLintEventos V2 implementados:
Autenticación:
v2:auth:token- Token de autenticación V2v2:auth:jwt- JWT token V2Dashboard:
v2:dashboard:self- Información del dashboardv2:dashboard:devices:self- Device propiov2:dashboard:devices:selected- Devices seleccionadosv2:dashboard:settings:daterange- Rango de fechasv2:dashboard:settings:rt- Estado de real-timev2:dashboard:settings:filters- Filtros aplicadosv2:dashboard:settings:refreshed- Dashboard refrescadoWidget:
v2:widget:ready:${widgetId}- Widget listov2:widget:loaded:${widgetId}- Widget cargadov2:widget:error:${widgetId}- Error en widgetv2:widget:customAction:${widgetId}- Acción personalizadaCaracterísticas del EventEmitterPanel:
{ event, payload }Compatibilidad:
Todos los archivos pasan
pnpm lintsin errores ni warnings.Summary by CodeRabbit
New Features
Documentation
Tests
✏️ Tip: You can customize this high-level summary in your review settings.