mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
fix(sharing): resolve gate strings on the server so the share view renders
The passphrase gate is a client component, but the page passed it a `t` translator function. A function cannot cross the server→client boundary, so a fresh (passphrase-protected) link threw a generic server-components render error instead of painting the unlock form. Pre-resolve the gate copy on the server into a plain, serializable `strings` object and pass that instead. Locale resolution stays server-authoritative. The page test mocked the gate away and only checked the token prop, so the regression slipped through. Tighten it to assert the gate receives a serializable contract: no prop value is a function, and the copy arrives as a pre-resolved plain string object.
This commit is contained in:
1 parent
cc41b6680a
commit
be37a87fa7
3 files changed
+56
-18
No files matched your search
@@ -72,6 +72,15 @@ function isGate(out: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the props the page handed to the gate element. A client component
|
||||
* boundary can only carry serializable props — issue #374 was a `t` FUNCTION
|
||||
* crossing it, which throws at render.
|
||||
*/
|
||||
function gateProps(out: unknown): Record<string, unknown> {
|
||||
return (out as { props: Record<string, unknown> }).props;
|
||||
}
|
||||
|
||||
function resolvedContext() {
|
||||
return {
|
||||
shareLinkId: "link-1",
|
||||
@@ -126,6 +135,23 @@ describe("clinician share page", () => {
|
||||
expect(isGate(out)).toBe(true);
|
||||
expect(resolve).not.toHaveBeenCalled();
|
||||
expect(loadData).not.toHaveBeenCalled();
|
||||
|
||||
// Issue #374 contract: every prop crossing the server→client boundary must
|
||||
// be serializable. NO prop value may be a function (a `t` function here
|
||||
// throws "Server Components render error"), and the copy must arrive as a
|
||||
// pre-resolved plain object.
|
||||
const props = gateProps(out);
|
||||
for (const value of Object.values(props)) {
|
||||
expect(typeof value).not.toBe("function");
|
||||
}
|
||||
expect(typeof props.token).toBe("string");
|
||||
const strings = props.strings as Record<string, unknown>;
|
||||
expect(strings).toBeTypeOf("object");
|
||||
expect(strings).not.toBeNull();
|
||||
expect(Object.values(strings).length).toBeGreaterThan(0);
|
||||
for (const value of Object.values(strings)) {
|
||||
expect(typeof value).toBe("string");
|
||||
}
|
||||
});
|
||||
|
||||
it("renders a protected link WITH a valid unlock cookie", async () => {
|
||||
|
||||
@@ -82,7 +82,17 @@ export default async function ClinicianSharePage({
|
||||
if (!verifyUnlockValue(unlockValue, gate.tokenHash)) {
|
||||
const locale = await resolveLocale();
|
||||
const { t } = getServerTranslator(locale);
|
||||
return <ShareUnlockGate t={(key, vars) => t(key, vars)} token={token} />;
|
||||
// Resolve the gate copy on the server (it owns the locale) into a plain,
|
||||
// serializable object. A `t` function cannot cross the RSC boundary into
|
||||
// the client gate — passing one throws at render (issue #374).
|
||||
const strings = {
|
||||
title: t("clinicianView.unlock.title"),
|
||||
description: t("clinicianView.unlock.description"),
|
||||
label: t("clinicianView.unlock.label"),
|
||||
error: t("clinicianView.unlock.error"),
|
||||
submit: t("clinicianView.unlock.submit"),
|
||||
};
|
||||
return <ShareUnlockGate strings={strings} token={token} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user