feat: add browser auth shell
This commit is contained in:
@@ -10,6 +10,8 @@ export interface ModuleUiFrameProps {
|
||||
readonly config: EkklesiaHostConfig;
|
||||
readonly descriptor: HostModuleUiDescriptor;
|
||||
readonly principal: ModuleUiPrincipal;
|
||||
/** Host-internal session signal. It never enters the module-facing context. */
|
||||
readonly onAuthorizationResponse?: (status: 401 | 403) => void;
|
||||
/** Test-only seam, forwarded to the generic loader so a test can hold an import pending and prove
|
||||
* the stale-load rule. Production composition leaves it unset and the loader's own
|
||||
* `(url) => import(url)` runs. */
|
||||
@@ -49,7 +51,7 @@ export interface ModuleUiFrameProps {
|
||||
export function ModuleUiFrame(props: ModuleUiFrameProps): ReactElement {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [status, setStatus] = useState<FrameStatus>("loading");
|
||||
const { config, descriptor, importModule, principal } = props;
|
||||
const { config, descriptor, importModule, onAuthorizationResponse, principal } = props;
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
@@ -60,11 +62,27 @@ export function ModuleUiFrame(props: ModuleUiFrameProps): ReactElement {
|
||||
let mounted: Awaited<ReturnType<typeof loadModuleUi>> | undefined;
|
||||
setStatus("loading");
|
||||
|
||||
const reportAuthorizationResponse = (status: 401 | 403): void => {
|
||||
// A module may retain an in-flight request while logout or another auth transition unmounts
|
||||
// this frame. Its eventual response still belongs to that module call, but must not be allowed
|
||||
// to cancel or overwrite the newer host transition.
|
||||
if (!cancelled) {
|
||||
onAuthorizationResponse?.(status);
|
||||
}
|
||||
};
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const instance = await loadModuleUi({
|
||||
container,
|
||||
context: createModuleUiContext({ config, descriptor, principal }),
|
||||
context: createModuleUiContext({
|
||||
config,
|
||||
descriptor,
|
||||
principal,
|
||||
...(onAuthorizationResponse
|
||||
? { onAuthorizationResponse: reportAuthorizationResponse }
|
||||
: {}),
|
||||
}),
|
||||
...(importModule ? { importModule } : {}),
|
||||
source: {
|
||||
moduleId: descriptor.id,
|
||||
@@ -94,7 +112,7 @@ export function ModuleUiFrame(props: ModuleUiFrameProps): ReactElement {
|
||||
// already in progress, and a module that fails to clean up must not take the shell with it.
|
||||
void mounted?.unmount().catch(() => undefined);
|
||||
};
|
||||
}, [config, descriptor, importModule, principal]);
|
||||
}, [config, descriptor, importModule, onAuthorizationResponse, principal]);
|
||||
|
||||
return (
|
||||
<section aria-live="polite" style={{ display: "grid", gap: "0.75rem" }}>
|
||||
|
||||
Reference in New Issue
Block a user