415 lines
18 KiB
TypeScript
Raw Normal View History

import { useReducer, useEffect, memo } from "react";
2021-03-02 01:05:15 +01:00
import type { ReactNode } from "react";
2021-03-07 15:37:37 +01:00
import { useKcMessage } from "../i18n/useKcMessage";
import { useKcLanguageTag } from "../i18n/useKcLanguageTag";
2021-06-23 08:16:51 +02:00
import type { KcContextBase } from "../getKcContext/KcContextBase";
2021-03-04 21:14:54 +01:00
import { assert } from "../tools/assert";
2021-03-02 23:48:31 +01:00
import type { KcLanguageTag } from "../i18n/KcLanguageTag";
import { getBestMatchAmongKcLanguageTag } from "../i18n/KcLanguageTag";
2021-03-02 23:48:31 +01:00
import { getKcLanguageTagLabel } from "../i18n/KcLanguageTag";
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
2021-03-04 13:56:51 +01:00
import { appendHead } from "../tools/appendHead";
2021-03-02 01:05:15 +01:00
import { join as pathJoin } from "path";
import { useConstCallback } from "powerhooks/useConstCallback";
2021-03-06 14:42:56 +01:00
import type { KcTemplateProps } from "./KcProps";
2021-08-20 17:03:50 +02:00
import { useCssAndCx } from "tss-react";
2021-03-02 12:17:24 +01:00
2021-03-02 23:48:31 +01:00
export type TemplateProps = {
2021-03-02 12:17:24 +01:00
displayInfo?: boolean;
2021-03-02 22:48:36 +01:00
displayMessage?: boolean;
displayRequiredFields?: boolean;
displayWide?: boolean;
showAnotherWayIfPresent?: boolean;
2021-03-02 01:05:15 +01:00
headerNode: ReactNode;
2021-03-04 18:15:48 +01:00
showUsernameNode?: ReactNode;
2021-03-02 01:05:15 +01:00
formNode: ReactNode;
2021-03-07 14:57:53 +01:00
infoNode?: ReactNode;
/** If you write your own page you probably want
* to avoid pulling the default theme assets.
*/
doFetchDefaultThemeResources: boolean;
} & { kcContext: KcContextBase } & KcTemplateProps;
2021-03-02 01:05:15 +01:00
2021-03-02 23:48:31 +01:00
export const Template = memo((props: TemplateProps) => {
2021-03-02 01:05:15 +01:00
const {
displayInfo = false,
displayMessage = true,
displayRequiredFields = false,
displayWide = false,
showAnotherWayIfPresent = true,
headerNode,
2021-03-04 18:15:48 +01:00
showUsernameNode = null,
2021-03-02 01:05:15 +01:00
formNode,
2021-03-08 00:09:52 +01:00
infoNode = null,
kcContext,
doFetchDefaultThemeResources,
2021-03-02 01:05:15 +01:00
} = props;
2021-08-20 17:03:50 +02:00
const { cx } = useCssAndCx();
useEffect(() => {
console.log("Rendering this page with react using keycloakify");
}, []);
2021-03-07 15:37:37 +01:00
const { msg } = useKcMessage();
2021-03-02 01:05:15 +01:00
2021-03-02 23:48:31 +01:00
const { kcLanguageTag, setKcLanguageTag } = useKcLanguageTag();
2021-03-02 01:05:15 +01:00
const onChangeLanguageClickFactory = useCallbackFactory(
([languageTag]: [KcLanguageTag]) => setKcLanguageTag(languageTag),
2021-03-02 01:05:15 +01:00
);
const onTryAnotherWayClick = useConstCallback(
() => (
document.forms["kc-select-try-another-way-form" as never].submit(),
false
),
);
const { realm, locale, auth, url, message, isAppInitiatedAction } =
kcContext;
useEffect(() => {
if (!realm.internationalizationEnabled) {
return;
}
2021-03-02 01:05:15 +01:00
assert(locale !== undefined);
2021-03-02 01:05:15 +01:00
if (kcLanguageTag === getBestMatchAmongKcLanguageTag(locale.current)) {
return;
}
2021-03-02 01:05:15 +01:00
window.location.href = locale.supported.find(
({ languageTag }) => languageTag === kcLanguageTag,
)!.url;
}, [kcLanguageTag]);
2021-03-02 01:05:15 +01:00
2021-03-04 13:56:51 +01:00
const [isExtraCssLoaded, setExtraCssLoaded] = useReducer(() => true, false);
2021-03-02 01:05:15 +01:00
useEffect(() => {
if (!doFetchDefaultThemeResources) {
setExtraCssLoaded();
return;
}
2021-03-04 13:56:51 +01:00
let isUnmounted = false;
const cleanups: (() => void)[] = [];
2021-03-02 01:05:15 +01:00
const toArr = (x: string | readonly string[] | undefined) =>
typeof x === "string" ? x.split(" ") : x ?? [];
2021-03-06 14:42:56 +01:00
2021-03-04 13:56:51 +01:00
Promise.all(
[
...toArr(props.stylesCommon).map(relativePath =>
pathJoin(url.resourcesCommonPath, relativePath),
),
...toArr(props.styles).map(relativePath =>
pathJoin(url.resourcesPath, relativePath),
),
].map(href =>
appendHead({
"type": "css",
href,
}),
),
).then(() => {
if (isUnmounted) {
return;
}
2021-03-04 13:56:51 +01:00
setExtraCssLoaded();
});
2021-03-02 01:05:15 +01:00
toArr(props.scripts).forEach(relativePath =>
appendHead({
2021-03-04 13:56:51 +01:00
"type": "javascript",
"src": pathJoin(url.resourcesPath, relativePath),
}),
2021-03-02 01:05:15 +01:00
);
2021-03-20 02:54:15 +01:00
if (props.kcHtmlClass !== undefined) {
const htmlClassList =
document.getElementsByTagName("html")[0].classList;
const tokens = cx(props.kcHtmlClass).split(" ");
htmlClassList.add(...tokens);
cleanups.push(() => htmlClassList.remove(...tokens));
2021-03-20 02:54:15 +01:00
}
2021-03-03 03:17:56 +01:00
return () => {
isUnmounted = true;
cleanups.forEach(f => f());
};
2021-03-21 22:25:47 +01:00
}, [props.kcHtmlClass]);
2021-03-02 01:05:15 +01:00
2021-03-04 13:56:51 +01:00
if (!isExtraCssLoaded) {
return null;
}
2021-03-02 01:05:15 +01:00
return (
2021-03-06 14:42:56 +01:00
<div className={cx(props.kcLoginClass)}>
<div id="kc-header" className={cx(props.kcHeaderClass)}>
<div
id="kc-header-wrapper"
className={cx(props.kcHeaderWrapperClass)}
>
2021-03-07 15:37:37 +01:00
{msg("loginTitleHtml", realm.displayNameHtml)}
2021-03-02 01:05:15 +01:00
</div>
</div>
<div
className={cx(
props.kcFormCardClass,
displayWide && props.kcFormCardAccountClass,
)}
>
2021-03-06 14:42:56 +01:00
<header className={cx(props.kcFormHeaderClass)}>
{realm.internationalizationEnabled &&
(assert(locale !== undefined), true) &&
locale.supported.length > 1 && (
<div id="kc-locale">
<div
id="kc-locale-wrapper"
className={cx(props.kcLocaleWrapperClass)}
>
<div
className="kc-dropdown"
id="kc-locale-dropdown"
>
<a href="#" id="kc-current-locale-link">
{getKcLanguageTagLabel(
kcLanguageTag,
)}
</a>
<ul>
{locale.supported.map(
({ languageTag }) => (
<li
key={languageTag}
className="kc-dropdown-item"
>
<a
href="#"
onClick={onChangeLanguageClickFactory(
languageTag,
)}
>
{getKcLanguageTagLabel(
languageTag,
)}
2021-03-02 01:05:15 +01:00
</a>
</li>
),
)}
</ul>
</div>
2021-03-02 01:05:15 +01:00
</div>
</div>
)}
{!(
auth !== undefined &&
auth.showUsername &&
!auth.showResetCredentials
) ? (
displayRequiredFields ? (
<div className={cx(props.kcContentWrapperClass)}>
<div
className={cx(
props.kcLabelWrapperClass,
"subtitle",
)}
>
<span className="subtitle">
<span className="required">*</span>
{msg("requiredFields")}
</span>
</div>
<div className="col-md-10">
<h1 id="kc-page-title">{headerNode}</h1>
</div>
</div>
) : (
<h1 id="kc-page-title">{headerNode}</h1>
)
) : displayRequiredFields ? (
<div className={cx(props.kcContentWrapperClass)}>
<div
className={cx(
props.kcLabelWrapperClass,
"subtitle",
)}
>
<span className="subtitle">
<span className="required">*</span>{" "}
{msg("requiredFields")}
</span>
</div>
<div className="col-md-10">
{showUsernameNode}
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-username">
<label id="kc-attempted-username">
{auth?.attemptedUsername}
</label>
<a
id="reset-login"
href={url.loginRestartFlowUrl}
>
<div className="kc-login-tooltip">
<i
className={cx(
props.kcResetFlowIcon,
)}
></i>
<span className="kc-tooltip-text">
{msg("restartLoginTooltip")}
2021-03-02 01:05:15 +01:00
</span>
</div>
</a>
2021-03-02 01:05:15 +01:00
</div>
</div>
</div>
</div>
) : (
<>
{showUsernameNode}
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-username">
<label id="kc-attempted-username">
{auth?.attemptedUsername}
</label>
<a
id="reset-login"
href={url.loginRestartFlowUrl}
>
<div className="kc-login-tooltip">
<i
className={cx(
props.kcResetFlowIcon,
)}
></i>
<span className="kc-tooltip-text">
{msg("restartLoginTooltip")}
</span>
2021-03-20 02:54:15 +01:00
</div>
</a>
</div>
</div>
</>
)}
2021-03-02 01:05:15 +01:00
</header>
<div id="kc-content">
<div id="kc-content-wrapper">
{/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
{displayMessage &&
message !== undefined &&
(message.type !== "warning" ||
!isAppInitiatedAction) && (
<div
className={cx(
"alert",
`alert-${message.type}`,
)}
>
{message.type === "success" && (
<span
className={cx(
props.kcFeedbackSuccessIcon,
)}
></span>
)}
{message.type === "warning" && (
<span
className={cx(
props.kcFeedbackWarningIcon,
)}
></span>
)}
{message.type === "error" && (
<span
className={cx(
props.kcFeedbackErrorIcon,
)}
></span>
)}
{message.type === "info" && (
<span
className={cx(
props.kcFeedbackInfoIcon,
)}
></span>
)}
<span
className="kc-feedback-text"
dangerouslySetInnerHTML={{
"__html": message.summary,
}}
/>
</div>
)}
2021-03-02 01:05:15 +01:00
{formNode}
{auth !== undefined &&
auth.showTryAnotherWayLink &&
showAnotherWayIfPresent && (
<form
id="kc-select-try-another-way-form"
action={url.loginAction}
method="post"
className={cx(
displayWide &&
props.kcContentWrapperClass,
)}
>
<div
className={cx(
displayWide && [
props.kcFormSocialAccountContentClass,
props.kcFormSocialAccountClass,
],
)}
>
<div
className={cx(
props.kcFormGroupClass,
)}
>
<input
type="hidden"
name="tryAnotherWay"
value="on"
/>
<a
href="#"
id="try-another-way"
onClick={onTryAnotherWayClick}
>
{msg("doTryAnotherWay")}
</a>
</div>
2021-03-02 01:05:15 +01:00
</div>
</form>
)}
{displayInfo && (
<div
id="kc-info"
className={cx(props.kcSignUpClass)}
>
<div
id="kc-info-wrapper"
className={cx(props.kcInfoAreaWrapperClass)}
>
2021-03-07 14:57:53 +01:00
{infoNode}
2021-03-02 01:05:15 +01:00
</div>
</div>
)}
2021-03-02 23:48:31 +01:00
</div>
</div>
</div>
</div>
2021-03-02 01:05:15 +01:00
);
2021-03-02 22:48:36 +01:00
});