Skip to content

Commit c1c976a

Browse files
committed
feat: capture parent origin when possible
1 parent 4873fdf commit c1c976a

File tree

2 files changed

+97
-2
lines changed

2 files changed

+97
-2
lines changed

src/ui/hooks/useRenderData.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ interface UseRenderDataResult<T> {
1313
data: T | null;
1414
isLoading: boolean;
1515
error: string | null;
16+
/** The origin of the parent window, captured from the first valid message */
17+
parentOrigin: string | null;
1618
}
1719

1820
/**
@@ -24,6 +26,7 @@ interface UseRenderDataResult<T> {
2426
* - data: The received render data (or null if not yet received)
2527
* - isLoading: Whether data is still being loaded
2628
* - error: Error message if message validation failed
29+
* - parentOrigin: The origin of the parent window (for secure postMessage calls)
2730
*
2831
* @example
2932
* ```tsx
@@ -32,15 +35,17 @@ interface UseRenderDataResult<T> {
3235
* }
3336
*
3437
* function MyComponent() {
35-
* const { data, isLoading, error } = useRenderData<MyData>();
36-
* // ...
38+
* const { data, isLoading, error, parentOrigin } = useRenderData<MyData>();
39+
* const { intent } = useUIActions({ targetOrigin: parentOrigin ?? undefined });
40+
* return <button onClick={() => intent("my-action")}>Click</button>;
3741
* }
3842
* ```
3943
*/
4044
export function useRenderData<T = unknown>(): UseRenderDataResult<T> {
4145
const [data, setData] = useState<T | null>(null);
4246
const [isLoading, setIsLoading] = useState(true);
4347
const [error, setError] = useState<string | null>(null);
48+
const [parentOrigin, setParentOrigin] = useState<string | null>(null);
4449

4550
useEffect(() => {
4651
const handleMessage = (event: MessageEvent<RenderDataMessage>): void => {
@@ -49,6 +54,8 @@ export function useRenderData<T = unknown>(): UseRenderDataResult<T> {
4954
return;
5055
}
5156

57+
setParentOrigin((current) => current ?? event.origin);
58+
5259
if (!event.data.payload || typeof event.data.payload !== "object") {
5360
const errorMsg = "Invalid payload structure received";
5461
setError(errorMsg);
@@ -88,5 +95,6 @@ export function useRenderData<T = unknown>(): UseRenderDataResult<T> {
8895
data,
8996
isLoading,
9097
error,
98+
parentOrigin,
9199
};
92100
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2+
import { createElement, type FunctionComponent } from "react";
3+
import { renderToString } from "react-dom/server";
4+
import { useRenderData } from "../../../src/ui/hooks/useRenderData.js";
5+
6+
type UseRenderDataResult<T> = ReturnType<typeof useRenderData<T>>;
7+
8+
interface TestData {
9+
items: string[];
10+
}
11+
12+
/**
13+
* Simple hook testing utility that renders a component using the hook
14+
* and captures the result for assertions.
15+
*/
16+
function testHook<T = unknown>(): UseRenderDataResult<T> {
17+
let hookResult: UseRenderDataResult<T> | undefined;
18+
19+
const TestComponent: FunctionComponent = () => {
20+
hookResult = useRenderData<T>();
21+
return null;
22+
};
23+
24+
renderToString(createElement(TestComponent));
25+
26+
if (!hookResult) {
27+
throw new Error("Hook did not return a result");
28+
}
29+
30+
return hookResult;
31+
}
32+
33+
describe("useRenderData", () => {
34+
let postMessageMock: ReturnType<typeof vi.fn>;
35+
let originalWindow: typeof globalThis.window;
36+
37+
beforeEach(() => {
38+
originalWindow = globalThis.window;
39+
postMessageMock = vi.fn();
40+
41+
globalThis.window = {
42+
parent: {
43+
postMessage: postMessageMock,
44+
},
45+
addEventListener: vi.fn(),
46+
removeEventListener: vi.fn(),
47+
} as unknown as typeof globalThis.window;
48+
});
49+
50+
afterEach(() => {
51+
globalThis.window = originalWindow;
52+
vi.restoreAllMocks();
53+
});
54+
55+
it("returns initial state with isLoading true", () => {
56+
const result = testHook<TestData>();
57+
58+
expect(result.data).toBeNull();
59+
expect(result.isLoading).toBe(true);
60+
expect(result.error).toBeNull();
61+
});
62+
63+
it("returns parentOrigin as null initially", () => {
64+
const result = testHook<TestData>();
65+
66+
expect(result.parentOrigin).toBeNull();
67+
});
68+
69+
it("includes parentOrigin in return type", () => {
70+
const result = testHook<TestData>();
71+
72+
// Verify the hook returns the expected shape with parentOrigin
73+
expect(result).toHaveProperty("data");
74+
expect(result).toHaveProperty("isLoading");
75+
expect(result).toHaveProperty("error");
76+
expect(result).toHaveProperty("parentOrigin");
77+
});
78+
79+
it("returns a stable object shape for destructuring", () => {
80+
const { data, isLoading, error, parentOrigin } = testHook<TestData>();
81+
82+
expect(data).toBeNull();
83+
expect(isLoading).toBe(true);
84+
expect(error).toBeNull();
85+
expect(parentOrigin).toBeNull();
86+
});
87+
});

0 commit comments

Comments
 (0)