diff options
| author | 2023-03-14 17:29:14 +0100 | |
|---|---|---|
| committer | 2023-03-14 17:29:14 +0100 | |
| commit | 1e1a638011d552034da94fd8b4e14786651b2743 (patch) | |
| tree | eb714eefa931d183934cda6acdfef29186f0f466 | |
| parent | Update package.json (diff) | |
feat(storybook): progress on many more pages
30 files changed, 1062 insertions, 301 deletions
diff --git a/.storybook/data.ts b/.storybook/data.ts deleted file mode 100644 index 337e207..0000000 --- a/.storybook/data.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { KcContextBase, getKcContext } from "keycloakify/lib/getKcContext"; -import type { DeepPartial } from "keycloakify/lib/tools/DeepPartial"; -import type { ExtendsKcContextBase } from "keycloakify/lib/getKcContext/getKcContextFromWindow"; -import type { KcContextExtension } from "keycloak-theme/kcContext"; - - -export const useKcStoryData = (mockData: ( - { pageId: KcContextBase['pageId'] | KcContextExtension['pageId'] } & DeepPartial<ExtendsKcContextBase<KcContextExtension>> -)) => { - const { kcContext } = getKcContext<KcContextExtension>({ mockPageId: mockData.pageId, mockData: [mockData] }) - return { kcContext: kcContext as NonNullable<typeof kcContext> } -} - -export const socialProviders = [ - { loginUrl: 'google', alias: 'google', providerId: 'google', displayName: 'Google' }, - { loginUrl: 'microsoft', alias: 'microsoft', providerId: 'microsoft', displayName: 'Microsoft' }, - { loginUrl: 'facebook', alias: 'facebook', providerId: 'facebook', displayName: 'Facebook' }, - { loginUrl: 'instagram', alias: 'instagram', providerId: 'instagram', displayName: 'Instagram' }, - { loginUrl: 'twitter', alias: 'twitter', providerId: 'twitter', displayName: 'Twitter' }, - { loginUrl: 'linkedin', alias: 'linkedin', providerId: 'linkedin', displayName: 'LinkedIn' }, - { loginUrl: 'stackoverflow', alias: 'stackoverflow', providerId: 'stackoverflow', displayName: 'Stackoverflow' }, - { loginUrl: 'github', alias: 'github', providerId: 'github', displayName: 'Github' }, - { loginUrl: 'gitlab', alias: 'gitlab', providerId: 'gitlab', displayName: 'Gitlab' }, - { loginUrl: 'bitbucket', alias: 'bitbucket', providerId: 'bitbucket', displayName: 'Bitbucket' }, - { loginUrl: 'paypal', alias: 'paypal', providerId: 'paypal', displayName: 'PayPal' }, - { loginUrl: 'openshift', alias: 'openshift', providerId: 'openshift', displayName: 'OpenShift' }, -]
\ No newline at end of file diff --git a/.storybook/main.js b/.storybook/main.js index ae1673f..bf619ea 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,18 +1,16 @@ module.exports = { - "stories": [ - "../src/keycloak-theme/pages/Login.stories.tsx", - "../src/keycloak-theme/pages/Register.stories.tsx", - "../src/keycloak-theme/pages/MyExtraPage1.stories.tsx", - "../src/keycloak-theme/pages/MyExtraPage2.stories.tsx", - ], - "addons": [ - "@storybook/addon-links", - "@storybook/addon-essentials", - "@storybook/addon-interactions", - "@storybook/preset-create-react-app" - ], - "framework": "@storybook/react", - "core": { - "builder": "@storybook/builder-webpack5" - } + "stories": [ + "../src/keycloak-theme/**/*.stories.tsx", + ], + "addons": [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-interactions", + "@storybook/preset-create-react-app" + ], + "framework": "@storybook/react", + "core": { + "builder": "@storybook/builder-webpack5" + }, + "staticDirs": ['../public'] }
\ No newline at end of file diff --git a/.storybook/preview.js b/.storybook/preview.js index 48afd56..b6de5f6 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,9 +1,13 @@ export const parameters = { - actions: { argTypesRegex: "^on[A-Z].*" }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/, + actions: {argTypesRegex: "^on[A-Z].*"}, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + options: { + storySort: (a, b) => + a[1].kind === b[1].kind ? 0 : a[1].id.localeCompare(b[1].id, undefined, {numeric: true}), }, - }, }
\ No newline at end of file diff --git a/.storybook/util.tsx b/.storybook/util.tsx new file mode 100644 index 0000000..4180dee --- /dev/null +++ b/.storybook/util.tsx @@ -0,0 +1,41 @@ +import type {KcContextExtension} from "keycloak-theme/kcContext"; +import KcApp from "../src/keycloak-theme/KcApp"; +import {KcContextBase} from "keycloakify"; +import {getKcContext} from "keycloakify/lib/getKcContext"; +import {ExtendsKcContextBase} from "keycloakify/src/lib/getKcContext/getKcContextFromWindow"; +import {DeepPartial} from "keycloakify/src/lib/tools/DeepPartial"; + + +export const socialProviders = [ + {loginUrl: 'google', alias: 'google', providerId: 'google', displayName: 'Google'}, + {loginUrl: 'microsoft', alias: 'microsoft', providerId: 'microsoft', displayName: 'Microsoft'}, + {loginUrl: 'facebook', alias: 'facebook', providerId: 'facebook', displayName: 'Facebook'}, + {loginUrl: 'instagram', alias: 'instagram', providerId: 'instagram', displayName: 'Instagram'}, + {loginUrl: 'twitter', alias: 'twitter', providerId: 'twitter', displayName: 'Twitter'}, + {loginUrl: 'linkedin', alias: 'linkedin', providerId: 'linkedin', displayName: 'LinkedIn'}, + {loginUrl: 'stackoverflow', alias: 'stackoverflow', providerId: 'stackoverflow', displayName: 'Stackoverflow'}, + {loginUrl: 'github', alias: 'github', providerId: 'github', displayName: 'Github'}, + {loginUrl: 'gitlab', alias: 'gitlab', providerId: 'gitlab', displayName: 'Gitlab'}, + {loginUrl: 'bitbucket', alias: 'bitbucket', providerId: 'bitbucket', displayName: 'Bitbucket'}, + {loginUrl: 'paypal', alias: 'paypal', providerId: 'paypal', displayName: 'PayPal'}, + {loginUrl: 'openshift', alias: 'openshift', providerId: 'openshift', displayName: 'OpenShift'}, +] + +type PageId = (KcContextExtension | KcContextBase)['pageId'] +export const template = (pageId: PageId) => { + type MockData = DeepPartial<ExtendsKcContextBase<KcContextExtension>>; + + const Template = (mockData: MockData) => { + const finalMockData = { + message: undefined, + pageId, + ...mockData + } as MockData + if (!("message" in mockData)) mockData["message"] = undefined + const {kcContext} = getKcContext<KcContextExtension>({mockPageId: pageId, mockData: [finalMockData]}) + return <KcApp kcContext={kcContext as NonNullable<typeof kcContext>}/> + } + + return (args: MockData) => Object.assign(Template.bind({}), {args}) +} + diff --git a/package.json b/package.json index eb6772b..11a0e3c 100755 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "build": "react-scripts build", "build-keycloak-theme": "yarn build && keycloakify", "download-builtin-keycloak-theme": "download-builtin-keycloak-theme 15.0.2", - "storybook": "start-storybook -p 6006 -s public", - "build-storybook": "build-storybook -s public" + "storybook": "start-storybook -p 6006", + "build-storybook": "build-storybook" }, "keycloakify": { "extraPages": [ diff --git a/src/keycloak-theme/Template.stories.tsx b/src/keycloak-theme/Template.stories.tsx new file mode 100644 index 0000000..d8660ed --- /dev/null +++ b/src/keycloak-theme/Template.stories.tsx @@ -0,0 +1,32 @@ +import type {ComponentMeta} from '@storybook/react'; +import KcApp from './KcApp'; +import {template} from '../../.storybook/util' + +const bind = template('my-extra-page-1.ftl'); + +export default { + title: 'Theme/Template', + component: KcApp, + parameters: { + layout: 'fullscreen', + }, +} as ComponentMeta<typeof KcApp>; + + +export const Default = bind({}) +export const InFrench = bind({locale: {currentLanguageTag: 'fr'}}) +export const RealmDisplayNameIsHtml = bind({ + realm: { + displayNameHtml: '<marquee>my realm</marquee>' + } +}) + +export const NoInternationalization = bind({ + realm: { + internationalizationEnabled: false, + } +}) + +export const WithGlobalError = bind({ + message: {type: "error", summary: "This is an error"} +}) diff --git a/src/keycloak-theme/pages/Error.stories.tsx b/src/keycloak-theme/pages/Error.stories.tsx new file mode 100644 index 0000000..ec176d8 --- /dev/null +++ b/src/keycloak-theme/pages/Error.stories.tsx @@ -0,0 +1,16 @@ +import {ComponentMeta} from '@storybook/react'; +import KcApp from '../KcApp'; +import {template} from '../../../.storybook/util' + +const bind = template('error.ftl'); + +export default { + kind: 'Page', + title: 'Theme/Pages/Error', + component: KcApp, + parameters: { + layout: 'fullscreen', + }, +} as ComponentMeta<typeof KcApp>; + +export const Default = bind({message: {type: 'error', summary: 'Something went wrong'}}) diff --git a/src/keycloak-theme/pages/Error.tsx b/src/keycloak-theme/pages/Error.tsx new file mode 100644 index 0000000..b28ff06 --- /dev/null +++ b/src/keycloak-theme/pages/Error.tsx @@ -0,0 +1,34 @@ +// copied and adapted from https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/Error.tsx +import React from "react"; +import type { PageProps } from "keycloakify" +import type { KcContext } from "../kcContext"; +import type { I18n } from "../i18n"; + + +export default function Error(props: PageProps<Extract<KcContext, { pageId: "error.ftl" }>, I18n>) { + const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props; + + const { message, client } = kcContext; + + const { msg } = i18n; + + return ( + <Template + {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }} + displayMessage={false} + headerNode={msg("errorTitle")} + formNode={ + <div id="kc-error-message"> + <p className="instruction">{message.summary}</p> + {client !== undefined && client.baseUrl !== undefined && ( + <p> + <a id="backToApplication" href={client.baseUrl}> + {msg("backToApplication")} + </a> + </p> + )} + </div> + } + /> + ); +}
\ No newline at end of file diff --git a/src/keycloak-theme/pages/IdpReviewUserProfile.stories.tsx b/src/keycloak-theme/pages/IdpReviewUserProfile.stories.tsx new file mode 100644 index 0000000..a468855 --- /dev/null +++ b/src/keycloak-theme/pages/IdpReviewUserProfile.stories.tsx @@ -0,0 +1,17 @@ +import {ComponentMeta} from '@storybook/react'; +import KcApp from '../KcApp'; +import {template} from '../../../.storybook/util' + +export default { + kind: 'Page', + title: 'Theme/Pages/Login/Review IDP User Profile', + component: KcApp, + parameters: { + layout: 'fullscreen', + }, +} as ComponentMeta<typeof KcApp>; + +const bind = template('idp-review-user-profile.ftl'); + +export const Default = bind({}) + diff --git a/src/keycloak-theme/pages/IdpReviewUserProfile.tsx b/src/keycloak-theme/pages/IdpReviewUserProfile.tsx new file mode 100644 index 0000000..ec73693 --- /dev/null +++ b/src/keycloak-theme/pages/IdpReviewUserProfile.tsx @@ -0,0 +1,47 @@ +import React, { useState } from "react"; +import { clsx } from "keycloakify/lib/tools/clsx"; +import { UserProfileFormFields } from "./shared/UserProfileCommons"; +import type { PageProps } from "keycloakify"; +import type { KcContext } from "../kcContext"; +import type { I18n } from "../i18n"; + +export default function IdpReviewUserProfile(props: PageProps<Extract<KcContext, { pageId: "idp-review-user-profile.ftl" }>, I18n>) { + const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props; + + const { msg, msgStr } = i18n; + + const { url } = kcContext; + + const [isFomSubmittable, setIsFomSubmittable] = useState(false); + + return ( + <Template + {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }} + headerNode={msg("loginIdpReviewProfileTitle")} + formNode={ + <form id="kc-idp-review-profile-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post"> + <UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...kcProps} /> + + <div className={clsx(kcProps.kcFormGroupClass)}> + <div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}> + <div className={clsx(kcProps.kcFormOptionsWrapperClass)} /> + </div> + <div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}> + <input + className={clsx( + kcProps.kcButtonClass, + kcProps.kcButtonPrimaryClass, + kcProps.kcButtonBlockClass, + kcProps.kcButtonLargeClass + )} + type="submit" + value={msgStr("doSubmit")} + disabled={!isFomSubmittable} + /> + </div> + </div> + </form> + } + /> + ); +}
\ No newline at end of file diff --git a/src/keycloak-theme/pages/Info.stories.tsx b/src/keycloak-theme/pages/Info.stories.tsx new file mode 100644 index 0000000..3c9cdd5 --- /dev/null +++ b/src/keycloak-theme/pages/Info.stories.tsx @@ -0,0 +1,37 @@ +import {ComponentMeta} from '@storybook/react'; +import KcApp from '../KcApp'; +import {template} from '../../../.storybook/util' + +export default { + kind: 'Page', + title: 'Theme/Pages/Info', + component: KcApp, + parameters: { + layout: 'fullscreen', + }, +} as ComponentMeta<typeof KcApp>; + +const bind = template('info.ftl'); + +export const Default = bind({ + messageHeader: 'Yo, get this:', + message: { + summary: 'You look good today' + } +}) + +export const WithLinkBack = bind({ + messageHeader: 'Yo, get this:', + message: { + summary: 'You look good today' + }, + actionUri: undefined +}) + +export const WithRequiredActions = bind({ + messageHeader: 'Yo, get this:', + message: { + summary: 'Before you can carry on, you need to do this: ' + }, + requiredActions: ["CONFIGURE_TOTP", "UPDATE_PROFILE", "VERIFY_EMAIL"] +}) diff --git a/src/keycloak-theme/pages/Info.tsx b/src/keycloak-theme/pages/Info.tsx new file mode 100644 index 0000000..5f921d0 --- /dev/null +++ b/src/keycloak-theme/pages/Info.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import {assert} from "keycloakify/lib/tools/assert"; +import type {PageProps} from "keycloakify"; +import type {KcContext} from "../kcContext"; +import type {I18n} from "../i18n"; + +export default function Info(props: PageProps<Extract<KcContext, { pageId: "info.ftl" }>, I18n>) { + const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props; + + const {msgStr, msg} = i18n; + + assert(kcContext.message !== undefined); + + const {messageHeader, message, requiredActions, skipLink, pageRedirectUri, actionUri, client} = kcContext; + + return ( + <Template + {...{kcContext, i18n, doFetchDefaultThemeResources, ...kcProps}} + displayMessage={false} + headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>} + formNode={ + <div id="kc-info-message"> + <p className="instruction"> + {message.summary} + + {requiredActions !== undefined && ( + <b>{requiredActions.map(requiredAction => msgStr(`requiredAction.${requiredAction}` as const)).join(",")}</b> + )} + </p> + {!skipLink && pageRedirectUri !== undefined ? ( + <p> + <a href={pageRedirectUri}>{msg("backToApplication")}</a> + </p> + ) : actionUri !== undefined ? ( + <p> + <a href={actionUri}>{msg("proceedWithAction")}</a> + </p> + ) : ( + client.baseUrl !== undefined && ( + <p> + <a href={client.baseUrl}>{msg("backToApplication")}</a> + </p> + ) + )} + </div> + } + /> + ); +}
\ No newline at end of file diff --git a/src/keycloak-theme/pages/Login.stories.tsx b/src/keycloak-theme/pages/Login.stories.tsx index 1d52e6d..a2e9f8d 100644 --- a/src/keycloak-theme/pages/Login.stories.tsx +++ b/src/keycloak-theme/pages/Login.stories.tsx @@ -1,97 +1,27 @@ -import { ComponentMeta } from '@storybook/react'; +import {ComponentMeta} from '@storybook/react'; import KcApp from '../KcApp'; - -import { useKcStoryData, socialProviders } from '../../../.storybook/data' +import {socialProviders, template} from '../../../.storybook/util' export default { - title: 'Login', - component: KcApp, - parameters: { - layout: 'fullscreen', - }, + kind: 'Page', + title: 'Theme/Pages/Login/Login', + component: KcApp, + parameters: { + layout: 'fullscreen', + }, } as ComponentMeta<typeof KcApp>; -const pageId = 'login.ftl' - -export const Default = () => { - const { kcContext } = useKcStoryData({ pageId, message: undefined }) - return <KcApp kcContext={kcContext} /> -} - -export const InFrench = () => { - const { kcContext } = useKcStoryData({ pageId, message: undefined, locale: { currentLanguageTag: 'fr' } }) - return <KcApp kcContext={kcContext} /> -} - -export const RealmDisplayNameIsHtml = () => { - const { kcContext } = useKcStoryData({ - pageId, message: undefined, realm: { - displayNameHtml: '<marquee>my realm</marquee>' - } - }) - return <KcApp kcContext={kcContext} /> -} - -export const NoInternationalization = () => { - const { kcContext } = useKcStoryData({ - pageId, message: undefined, realm: { - internationalizationEnabled: false, - } - }) - return <KcApp kcContext={kcContext} /> -} - -export const NoPasswordField = () => { - const { kcContext } = useKcStoryData({ pageId, message: undefined, realm: { password: false } }) - return <KcApp kcContext={kcContext} /> -} - -export const RegistrationNotAllowed = () => { - const { kcContext } = useKcStoryData({ pageId, message: undefined, realm: { registrationAllowed: false } }) - return <KcApp kcContext={kcContext} /> -} - -export const RememberMeNotAllowed = () => { - const { kcContext } = useKcStoryData({ pageId, message: undefined, realm: { rememberMe: false } }) - return <KcApp kcContext={kcContext} /> -} - -export const PasswordResetNotAllowed = () => { - const { kcContext } = useKcStoryData({ pageId, message: undefined, realm: { resetPasswordAllowed: false } }) - return <KcApp kcContext={kcContext} /> -} - -export const EmailIsUsername = () => { - const { kcContext } = useKcStoryData({ pageId, message: undefined, realm: { loginWithEmailAllowed: false } }) - return <KcApp kcContext={kcContext} /> -} - -export const TryAnotherWay = () => { - const { kcContext } = useKcStoryData({ pageId, message: undefined, auth: { showTryAnotherWayLink: true } }) - return <KcApp kcContext={kcContext} /> -} - -export const PresetUsername = () => { - const { kcContext } = useKcStoryData({ pageId, message: undefined, login: { username: '[email protected]' } }) - return <KcApp kcContext={kcContext} /> -} - -export const ReadOnlyPresetUsername = () => { - const { kcContext } = useKcStoryData({ pageId, message: undefined, login: { username: '[email protected]' }, usernameEditDisabled: true }) - return <KcApp kcContext={kcContext} /> -} - -export const WithSocialProviders = () => { - const { kcContext } = useKcStoryData({ - pageId, message: undefined, social: { - displayInfo: true, - providers: socialProviders - } - }) - return <KcApp kcContext={kcContext} /> -} - -export const WithError = () => { - const { kcContext } = useKcStoryData({ pageId, message: { type: "error", summary: "This is an error" } }) - return <KcApp kcContext={kcContext} /> -} +const bind = template('login.ftl'); + +export const Default = bind({}) +export const WithoutPasswordField = bind({realm: {password: false}}) +export const WithoutRegistration = bind({realm: {registrationAllowed: false}}) +export const WithoutRememberMe = bind({realm: {rememberMe: false}}) +export const WithoutPasswordReset = bind({realm: {resetPasswordAllowed: false}}) +export const WithEmailAsUsername = bind({realm: {loginWithEmailAllowed: false}}) +export const WithPresetUsername = bind({login: {username: '[email protected]'}}) +export const WithImmutablePresetUsername = bind({ + login: {username: '[email protected]'}, + usernameEditDisabled: true +}) +export const WithSocialProviders = bind({social: {displayInfo: true, providers: socialProviders}}) diff --git a/src/keycloak-theme/pages/LoginConfigTotp.stories.tsx b/src/keycloak-theme/pages/LoginConfigTotp.stories.tsx new file mode 100644 index 0000000..a3a4781 --- /dev/null +++ b/src/keycloak-theme/pages/LoginConfigTotp.stories.tsx @@ -0,0 +1,27 @@ +import {ComponentMeta} from '@storybook/react'; +import KcApp from '../KcApp'; +import {template} from '../../../.storybook/util' + +export default { + kind: 'Page', + title: 'Theme/Pages/Login/Configure TOTP', + component: KcApp, + parameters: { + layout: 'fullscreen', + }, +} as ComponentMeta<typeof KcApp>; + +const bind = template('login-config-totp.ftl'); + +export const Default = bind({}) + +export const WithManualSetUp = bind({mode: 'manual'}) +export const WithError = bind({ + messagesPerField: { + get: (fieldName: string) => fieldName === 'totp' ? 'Invalid TOTP' : undefined, + exists: (fieldName: string) => fieldName === 'totp', + existsError: (fieldName: string) => fieldName === 'totp', + printIfExists: <T, >(fieldName: string, x: T) => fieldName === 'totp' ? x : undefined + } +}) + diff --git a/src/keycloak-theme/pages/LoginConfigTotp.tsx b/src/keycloak-theme/pages/LoginConfigTotp.tsx new file mode 100644 index 0000000..fc473c9 --- /dev/null +++ b/src/keycloak-theme/pages/LoginConfigTotp.tsx @@ -0,0 +1,186 @@ +import React from "react"; +import {clsx} from "keycloakify/lib/tools/clsx"; +import type {PageProps, KcContextBase} from "keycloakify"; +import type {KcContext} from "../kcContext"; +import type {I18n} from "../i18n"; + +export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pageId: "login-config-totp.ftl" }>, I18n>) { + const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props; + + const {url, isAppInitiatedAction, totp, mode, messagesPerField} = kcContext; + + const {msg, msgStr} = i18n; + + const algToKeyUriAlg: Record<KcContextBase.LoginConfigTotp["totp"]["policy"]["algorithm"], string> = { + "HmacSHA1": "SHA1", + "HmacSHA256": "SHA256", + "HmacSHA512": "SHA512" + }; + + return ( + <Template + {...{kcContext, i18n, doFetchDefaultThemeResources, ...kcProps}} + headerNode={msg("loginTotpTitle")} + formNode={ + <> + <ol id="kc-totp-settings"> + <li> + <p>{msg("loginTotpStep1")}</p> + + <ul id="kc-totp-supported-apps"> + {totp.policy.supportedApplications.map(app => ( + <li>{app}</li> + ))} + </ul> + </li> + + {mode && mode == "manual" ? ( + <> + <li> + <p>{msg("loginTotpManualStep2")}</p> + <p> + <span id="kc-totp-secret-key">{totp.totpSecretEncoded}</span> + </p> + <p> + <a href={totp.qrUrl} id="mode-barcode"> + {msg("loginTotpScanBarcode")} + </a> + </p> + </li> + <li> + <p>{msg("loginTotpManualStep3")}</p> + <p> + <ul> + <li id="kc-totp-type"> + {msg("loginTotpType")}: {msg(`loginTotp.${totp.policy.type}`)} + </li> + <li id="kc-totp-algorithm"> + {msg("loginTotpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm} + </li> + <li id="kc-totp-digits"> + {msg("loginTotpDigits")}: {totp.policy.digits} + </li> + {totp.policy.type === "totp" ? ( + <li id="kc-totp-period"> + {msg("loginTotpInterval")}: {totp.policy.period} + </li> + ) : ( + <li id="kc-totp-counter"> + {msg("loginTotpCounter")}: {totp.policy.initialCounter} + </li> + )} + </ul> + </p> + </li> + </> + ) : ( + <li> + <p>{msg("loginTotpStep2")}</p> + <img id="kc-totp-secret-qr-code" src={`data:image/png;base64, ${totp.totpSecretQrCode}`} + alt="Figure: Barcode"/> + <br/> + <p> + <a href={totp.manualUrl} id="mode-manual"> + {msg("loginTotpUnableToScan")} + </a> + </p> + </li> + )} + <li> + <p>{msg("loginTotpStep3")}</p> + <p>{msg("loginTotpStep3DeviceName")}</p> + </li> + </ol> + + <form action={url.loginAction} className={clsx(kcProps.kcFormClass)} id="kc-totp-settings-form" + method="post"> + <div className={clsx(kcProps.kcFormGroupClass)}> + <div className={clsx(kcProps.kcInputWrapperClass)}> + <label htmlFor="totp" className={clsx(kcProps.kcLabelClass)}> + {msg("authenticatorCode")} + </label>{" "} + <span className="required">*</span> + </div> + <div className={clsx(kcProps.kcInputWrapperClass)}> + <input + type="text" + id="totp" + name="totp" + autoComplete="off" + className={clsx(kcProps.kcInputClass)} + aria-invalid={messagesPerField.existsError("totp")} + /> + + {messagesPerField.existsError("totp") && ( + <span id="input-error-otp-code" className={clsx(kcProps.kcInputErrorMessageClass)} + aria-live="polite"> + {messagesPerField.get("totp")} + </span> + )} + </div> + <input type="hidden" id="totpSecret" name="totpSecret" value={totp.totpSecret}/> + {mode && <input type="hidden" id="mode" value={mode}/>} + </div> + + <div className={clsx(kcProps.kcFormGroupClass)}> + <div className={clsx(kcProps.kcInputWrapperClass)}> + <label htmlFor="userLabel" className={clsx(kcProps.kcLabelClass)}> + {msg("loginTotpDeviceName")} + </label>{" "} + {totp.otpCredentials.length >= 1 && <span className="required">*</span>} + </div> + <div className={clsx(kcProps.kcInputWrapperClass)}> + <input + type="text" + id="userLabel" + name="userLabel" + autoComplete="off" + className={clsx(kcProps.kcInputClass)} + aria-invalid={messagesPerField.existsError("userLabel")} + /> + {messagesPerField.existsError("userLabel") && ( + <span id="input-error-otp-label" className={clsx(kcProps.kcInputErrorMessageClass)} + aria-live="polite"> + {messagesPerField.get("userLabel")} + </span> + )} + </div> + </div> + + {isAppInitiatedAction ? ( + <> + <input + type="submit" + className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)} + id="saveTOTPBtn" + value={msgStr("doSubmit")} + /> + <button + type="submit" + className={clsx( + kcProps.kcButtonClass, + kcProps.kcButtonDefaultClass, + kcProps.kcButtonLargeClass, + kcProps.kcButtonLargeClass + )} + id="cancelTOTPBtn" + name="cancel-aia" + value="true" + > + ${msg("doCancel")} + </button> + </> + ) : ( + <input + type="submit" + className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)} + id="saveTOTPBtn" + value={msgStr("doSubmit")} + /> + )} + </form> + </> + } + /> + ); +}
\ No newline at end of file diff --git a/src/keycloak-theme/pages/LoginIdpLinkConfirm.stories.tsx b/src/keycloak-theme/pages/LoginIdpLinkConfirm.stories.tsx new file mode 100644 index 0000000..901b5ca --- /dev/null +++ b/src/keycloak-theme/pages/LoginIdpLinkConfirm.stories.tsx @@ -0,0 +1,17 @@ +import {ComponentMeta} from '@storybook/react'; +import KcApp from '../KcApp'; +import {template} from '../../../.storybook/util' + +export default { + kind: 'Page', + title: 'Theme/Pages/Login/Confirm IDP Link', + component: KcApp, + parameters: { + layout: 'fullscreen', + }, +} as ComponentMeta<typeof KcApp>; + +const bind = template('login-idp-link-confirm.ftl'); + +export const Default = bind({}) + diff --git a/src/keycloak-theme/pages/LoginIdpLinkConfirm.tsx b/src/keycloak-theme/pages/LoginIdpLinkConfirm.tsx new file mode 100644 index 0000000..66fca26 --- /dev/null +++ b/src/keycloak-theme/pages/LoginIdpLinkConfirm.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import {clsx} from "keycloakify/lib/tools/clsx"; +import type {PageProps} from "keycloakify"; +import type {KcContext} from "../kcContext"; +import type {I18n} from "../i18n"; + +export default function LoginIdpLinkConfirm(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-confirm.ftl" }>, I18n>) { + const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props; + + const {url, idpAlias} = kcContext; + + const {msg} = i18n; + + return ( + <Template + {...{kcContext, i18n, doFetchDefaultThemeResources, ...kcProps}} + headerNode={msg("confirmLinkIdpTitle")} + formNode={ + <form id="kc-register-form" action={url.loginAction} method="post"> + <div className={clsx(kcProps.kcFormGroupClass)}> + <button + type="submit" + className={clsx( + kcProps.kcButtonClass, + kcProps.kcButtonDefaultClass, + kcProps.kcButtonBlockClass, + kcProps.kcButtonLargeClass + )} + name="submitAction" + id="updateProfile" + value="updateProfile" + > + {msg("confirmLinkIdpReviewProfile")} + </button> + <button + type="submit" + className={clsx( + kcProps.kcButtonClass, + kcProps.kcButtonDefaultClass, + kcProps.kcButtonBlockClass, + kcProps.kcButtonLargeClass + )} + name="submitAction" + id="linkAccount" + value="linkAccount" + > + {msg("confirmLinkIdpContinue", idpAlias)} + </button> + </div> + </form> + } + /> + ); +}
\ No newline at end of file diff --git a/src/keycloak-theme/pages/LoginIdpLinkEmail.stories.tsx b/src/keycloak-theme/pages/LoginIdpLinkEmail.stories.tsx new file mode 100644 index 0000000..f7aac4e --- /dev/null +++ b/src/keycloak-theme/pages/LoginIdpLinkEmail.stories.tsx @@ -0,0 +1,17 @@ +import {ComponentMeta} from '@storybook/react'; +import KcApp from '../KcApp'; +import {template} from '../../../.storybook/util' + +export default { + kind: 'Page', + title: 'Theme/Pages/Login/Confirm IDP Email', + component: KcApp, + parameters: { + layout: 'fullscreen', + }, +} as ComponentMeta<typeof KcApp>; + +const bind = template('login-idp-link-email.ftl'); + +export const Default = bind({}) + diff --git a/src/keycloak-theme/pages/LoginIdpLinkEmail.tsx b/src/keycloak-theme/pages/LoginIdpLinkEmail.tsx new file mode 100644 index 0000000..f73aead --- /dev/null +++ b/src/keycloak-theme/pages/LoginIdpLinkEmail.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import type {PageProps} from "keycloakify"; +import type {KcContext} from "../kcContext"; +import type {I18n} from "../i18n"; + +export default function LoginIdpLinkEmail(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-email.ftl" }>, I18n>) { + const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props; + + const {url, realm, brokerContext, idpAlias} = kcContext; + + const {msg} = i18n; + + return ( + <Template + {...{kcContext, i18n, doFetchDefaultThemeResources, ...kcProps}} + headerNode={msg("emailLinkIdpTitle", idpAlias)} + formNode={ + <> + <p id="instruction1" className="instruction"> + {msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)} + </p> + <p id="instruction2" className="instruction"> + {msg("emailLinkIdp2")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp3")} + </p> + <p id="instruction3" className="instruction"> + {msg("emailLinkIdp4")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp5")} + </p> + </> + } + /> + ); +}
\ No newline at end of file diff --git a/src/keycloak-theme/pages/LoginOtp.stories.tsx b/src/keycloak-theme/pages/LoginOtp.stories.tsx new file mode 100644 index 0000000..b9d8a48 --- /dev/null +++ b/src/keycloak-theme/pages/LoginOtp.stories.tsx @@ -0,0 +1,16 @@ +import {ComponentMeta} from '@storybook/react'; +import KcApp from '../KcApp'; +import {socialProviders, template} from '../../../.storybook/util' + +export default { + kind: 'Page', + title: 'Theme/Pages/Login/Login OTP', + component: KcApp, + parameters: { + layout: 'fullscreen', + }, +} as ComponentMeta<typeof KcApp>; + +const bind = template('login-otp.ftl'); + +export const Default = bind({}) diff --git a/src/keycloak-theme/pages/LoginOtp.tsx b/src/keycloak-theme/pages/LoginOtp.tsx new file mode 100644 index 0000000..64bac17 --- /dev/null +++ b/src/keycloak-theme/pages/LoginOtp.tsx @@ -0,0 +1,114 @@ +import React, { useEffect } from "react"; +import { headInsert } from "keycloakify/lib/tools/headInsert"; +import { pathJoin } from "keycloakify/bin/tools/pathJoin"; +import { clsx } from "keycloakify/lib/tools/clsx"; +import type { PageProps } from "keycloakify"; +import type { KcContext } from "../kcContext"; +import type { I18n } from "../i18n"; + +export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "login-otp.ftl" }>, I18n>) { + const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props; + + const { otpLogin, url } = kcContext; + + const { msg, msgStr } = i18n; + + useEffect(() => { + let isCleanedUp = false; + + headInsert({ + "type": "javascript", + "src": pathJoin(kcContext.url.resourcesCommonPath, "node_modules/jquery/dist/jquery.min.js") + }).then(() => { + if (isCleanedUp) return; + + evaluateInlineScript(); + }); + + return () => { + isCleanedUp = true; + }; + }, []); + + return ( + <Template + {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }} + headerNode={msg("doLogIn")} + formNode={ + <form id="kc-otp-login-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post"> + {otpLogin.userOtpCredentials.length > 1 && ( + <div className={clsx(kcProps.kcFormGroupClass)}> + <div className={clsx(kcProps.kcInputWrapperClass)}> + {otpLogin.userOtpCredentials.map(otpCredential => ( + <div key={otpCredential.id} className={clsx(kcProps.kcSelectOTPListClass)}> + <input type="hidden" value="${otpCredential.id}" /> + <div className={clsx(kcProps.kcSelectOTPListItemClass)}> + <span className={clsx(kcProps.kcAuthenticatorOtpCircleClass)} /> + <h2 className={clsx(kcProps.kcSelectOTPItemHeadingClass)}>{otpCredential.userLabel}</h2> + </div> + </div> + ))} + </div> + </div> + )} + <div className={clsx(kcProps.kcFormGroupClass)}> + <div className={clsx(kcProps.kcLabelWrapperClass)}> + <label htmlFor="otp" className={clsx(kcProps.kcLabelClass)}> + {msg("loginOtpOneTime")} + </label> + </div> + + <div className={clsx(kcProps.kcInputWrapperClass)}> + <input id="otp" name="otp" autoComplete="off" type="text" className={clsx(kcProps.kcInputClass)} autoFocus /> + </div> + </div> + + <div className={clsx(kcProps.kcFormGroupClass)}> + <div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}> + <div className={clsx(kcProps.kcFormOptionsWrapperClass)} /> + </div> + + <div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}> + <input + className={clsx( + kcProps.kcButtonClass, + kcProps.kcButtonPrimaryClass, + kcProps.kcButtonBlockClass, + kcProps.kcButtonLargeClass + )} + name="login" + id="kc-login" + type="submit" + value={msgStr("doLogIn")} + /> + </div> + </div> + </form> + } + /> + ); +} + +declare const $: any; + +function evaluateInlineScript() { + $(document).ready(function () { + // Card Single Select + $(".card-pf-view-single-select").click(function (this: any) { + if ($(this).hasClass("active")) { + $(this).removeClass("active"); + $(this).children().removeAttr("name"); + } else { + $(".card-pf-view-single-select").removeClass("active"); + $(".card-pf-view-single-select").children().removeAttr("name"); + $(this).addClass("active"); + $(this).children().attr("name", "selectedCredentialId"); + } + }); + + var defaultCred = $(".card-pf-view-single-select")[0]; + if (defaultCred) { + defaultCred.click(); + } + }); +}
\ No newline at end of file diff --git a/src/keycloak-theme/pages/LoginPageExpired.stories.tsx b/src/keycloak-theme/pages/LoginPageExpired.stories.tsx new file mode 100644 index 0000000..c991a2f --- /dev/null +++ b/src/keycloak-theme/pages/LoginPageExpired.stories.tsx @@ -0,0 +1,16 @@ +import {ComponentMeta} from '@storybook/react'; +import KcApp from '../KcApp'; +import {socialProviders, template} from '../../../.storybook/util' + +export default { + kind: 'Page', + title: 'Theme/Pages/Login/Login Page Expired', + component: KcApp, + parameters: { + layout: 'fullscreen', + }, +} as ComponentMeta<typeof KcApp>; + +const bind = template('login-page-expired.ftl'); + +export const Default = bind({}) diff --git a/src/keycloak-theme/pages/LoginPageExpired.tsx b/src/keycloak-theme/pages/LoginPageExpired.tsx new file mode 100644 index 0000000..e44811b --- /dev/null +++ b/src/keycloak-theme/pages/LoginPageExpired.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import type { PageProps } from "keycloakify"; +import type { KcContext } from "../kcContext"; +import type { I18n } from "../i18n"; + +export default function LoginPageExpired(props: PageProps<Extract<KcContext, { pageId: "login-page-expired.ftl" }>, I18n>) { + const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props; + + const { url } = kcContext; + + const { msg } = i18n; + + return ( + <Template + {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }} + displayMessage={false} + headerNode={msg("pageExpiredTitle")} + formNode={ + <> + <p id="instruction1" className="instruction"> + {msg("pageExpiredMsg1")} + <a id="loginRestartLink" href={url.loginRestartFlowUrl}> + {msg("doClickHere")} + </a>{" "} + .<br /> + {msg("pageExpiredMsg2")}{" "} + <a id="loginContinueLink" href={url.loginAction}> + {msg("doClickHere")} + </a>{" "} + . + </p> + </> + } + /> + ); +}
\ No newline at end of file diff --git a/src/keycloak-theme/pages/MyExtraPage1.stories.tsx b/src/keycloak-theme/pages/MyExtraPage1.stories.tsx index 9315151..8473785 100644 --- a/src/keycloak-theme/pages/MyExtraPage1.stories.tsx +++ b/src/keycloak-theme/pages/MyExtraPage1.stories.tsx @@ -1,24 +1,16 @@ -import { ComponentMeta } from '@storybook/react'; +import {ComponentMeta} from '@storybook/react'; import KcApp from '../KcApp'; - -import { useKcStoryData } from '../../../.storybook/data' +import {template} from '../../../.storybook/util' export default { - title: 'My Extra Page 1', - component: KcApp, - parameters: { - layout: 'fullscreen', - }, + kind: 'Page', + title: 'Theme/Pages/My Extra Page 1', + component: KcApp, + parameters: { + layout: 'fullscreen', + }, } as ComponentMeta<typeof KcApp>; -const pageId = 'my-extra-page-1.ftl' - -export const Default = () => { - const { kcContext } = useKcStoryData({ pageId }) - return <KcApp kcContext={kcContext} /> -} +const bind = template('my-extra-page-1.ftl') -export const InFrench = () => { - const { kcContext } = useKcStoryData({ pageId, locale: { currentLanguageTag: 'fr' } }) - return <KcApp kcContext={kcContext} /> -} +export const Default = bind({}) diff --git a/src/keycloak-theme/pages/MyExtraPage2.stories.tsx b/src/keycloak-theme/pages/MyExtraPage2.stories.tsx index 4fb29d3..1d0f004 100644 --- a/src/keycloak-theme/pages/MyExtraPage2.stories.tsx +++ b/src/keycloak-theme/pages/MyExtraPage2.stories.tsx @@ -1,29 +1,18 @@ -import { ComponentMeta } from '@storybook/react'; +import {ComponentMeta} from '@storybook/react'; import KcApp from '../KcApp'; - -import { useKcStoryData } from '../../../.storybook/data' +import {template} from '../../../.storybook/util' export default { - title: 'My Extra Page 2', - component: KcApp, - parameters: { - layout: 'fullscreen', - }, + kind: 'Page', + title: 'Theme/Pages/My Extra Page 2', + component: KcApp, + parameters: { + layout: 'fullscreen', + }, } as ComponentMeta<typeof KcApp>; -const pageId = 'my-extra-page-2.ftl' - -export const Default = () => { - const { kcContext } = useKcStoryData({ pageId }) - return <KcApp kcContext={kcContext} /> -} +const bind = template('my-extra-page-2.ftl') -export const InFrench = () => { - const { kcContext } = useKcStoryData({ pageId, locale: { currentLanguageTag: 'fr' } }) - return <KcApp kcContext={kcContext} /> -} +export const Default = bind({}) -export const WithCustomValue = () => { - const { kcContext } = useKcStoryData({ pageId, someCustomValue: 'Foo Bar Baz' }) - return <KcApp kcContext={kcContext} /> -} +export const WithCustomValue = bind({someCustomValue: 'Foo Bar Baz'}) diff --git a/src/keycloak-theme/pages/Register.stories.tsx b/src/keycloak-theme/pages/Register.stories.tsx index 2fac548..5bbbd2d 100644 --- a/src/keycloak-theme/pages/Register.stories.tsx +++ b/src/keycloak-theme/pages/Register.stories.tsx @@ -1,69 +1,54 @@ -import { ComponentMeta } from '@storybook/react'; +import {ComponentMeta} from '@storybook/react'; import KcApp from '../KcApp'; - -import { useKcStoryData, socialProviders } from '../../../.storybook/data' +import {template} from '../../../.storybook/util' export default { - title: 'Register', - component: KcApp, - parameters: { - layout: 'fullscreen', - }, + kind: 'Page', + title: 'Theme/Pages/Register', + component: KcApp, + parameters: { + layout: 'fullscreen', + }, } as ComponentMeta<typeof KcApp>; -const pageId = 'register.ftl' - -export const Default = () => { - const { kcContext } = useKcStoryData({ pageId, message: undefined }) - return <KcApp kcContext={kcContext} /> -} +const bind = template('register.ftl') -export const InFrench = () => { - const { kcContext } = useKcStoryData({ pageId, message: undefined, locale: { currentLanguageTag: 'fr' } }) - return <KcApp kcContext={kcContext} /> -} +export const Default = bind({}) -export const WithError = () => { - const { kcContext } = useKcStoryData({ pageId, message: { type: "error", summary: "This is an error" } }) - return <KcApp kcContext={kcContext} /> -} +export const WithFieldError = bind({ + register: { + formData: { + email: '[email protected]' + } + }, + messagesPerField: { + existsError: (fieldName: string) => fieldName === "email", + exists: (fieldName: string) => fieldName === "email", + get: (fieldName: string) => fieldName === "email" ? "I don't like your email address" : undefined, + printIfExists: <T, >(fieldName: string, x: T) => fieldName === "email" ? x : undefined, + } +}) -export const EmailIsUsername = () => { - const { kcContext } = useKcStoryData({ - pageId, message: undefined, - realm: { registrationEmailAsUsername: true } - }) - return <KcApp kcContext={kcContext} /> -} +export const WithEmailAsUsername = bind({ + realm: {registrationEmailAsUsername: true} +}) -export const NoPassword = () => { - const { kcContext } = useKcStoryData({ - pageId, message: undefined, passwordRequired: false - }) - return <KcApp kcContext={kcContext} /> -} +export const WithoutPassword = bind({ + passwordRequired: false +}) -export const WithRecaptcha = () => { - const { kcContext } = useKcStoryData({ - pageId, message: undefined, +export const WithRecaptcha = bind({ recaptchaRequired: true, recaptchaSiteKey: 'foobar' - }) - return <KcApp kcContext={kcContext} /> -} +}) -export const WithPresets = () => { - const { kcContext } = useKcStoryData({ - pageId, message: undefined, +export const WithPresets = bind({ register: { - formData: { - firstName: 'Max', - lastName: 'Mustermann', - email: '[email protected]', - username: 'max.mustermann' - } + formData: { + firstName: 'Max', + lastName: 'Mustermann', + email: '[email protected]', + username: 'max.mustermann' + } } - }) - return <KcApp kcContext={kcContext} /> - -}
\ No newline at end of file +})
\ No newline at end of file diff --git a/src/keycloak-theme/pages/Register.tsx b/src/keycloak-theme/pages/Register.tsx index 1dcf216..dd19bba 100644 --- a/src/keycloak-theme/pages/Register.tsx +++ b/src/keycloak-theme/pages/Register.tsx @@ -54,6 +54,7 @@ export default function Register(props: PageProps<Extract<KcContext, { pageId: " name="lastName" defaultValue={register.formData.lastName ?? ""} /> + </div> </div> diff --git a/src/keycloak-theme/pages/RegisterUserProfile.stories.tsx b/src/keycloak-theme/pages/RegisterUserProfile.stories.tsx new file mode 100644 index 0000000..1f2149c --- /dev/null +++ b/src/keycloak-theme/pages/RegisterUserProfile.stories.tsx @@ -0,0 +1,79 @@ +import {ComponentMeta} from '@storybook/react'; +import KcApp from '../KcApp'; +import {template} from '../../../.storybook/util' + +const bind = template('register-user-profile.ftl') + +export default { + kind: 'Page', + title: 'Theme/Pages/Register User Profile', + component: KcApp, + parameters: {layout: 'fullscreen'}, +} as ComponentMeta<typeof KcApp>; + +export const Default = bind({}) + +export const WithFieldError = bind({ + profile: { + attributes: [ + { + name: "email", + value: "[email protected]", + } + ] + } +}) + +export const WithPresets = bind({ + profile: { + attributes: [ + { + name: "username", + value: "max.mustermann" + }, + { + name: "email", + value: "[email protected]", + }, + { + name: "firstName", + required: false, + value: "Max" + }, + { + name: "lastName", + required: false, + value: "Mustermann" + } + ] + } +}) + +export const WithImmutablePresets = bind({ + profile: { + attributes: [ + { + name: "username", + value: "max.mustermann", + readOnly: true, + }, + { + name: "email", + value: "[email protected]", + readOnly: true, + }, + { + name: "firstName", + required: true, + value: "Max", + readOnly: true, + }, + { + name: "lastName", + required: true, + value: "Mustermann", + readOnly: true, + } + ] + } +}) diff --git a/src/keycloak-theme/pages/Terms.stories.tsx b/src/keycloak-theme/pages/Terms.stories.tsx new file mode 100644 index 0000000..1fd20ac --- /dev/null +++ b/src/keycloak-theme/pages/Terms.stories.tsx @@ -0,0 +1,15 @@ +import {ComponentMeta} from '@storybook/react'; +import KcApp from '../KcApp'; +import {template} from '../../../.storybook/util' + +const bind = template('terms.ftl'); +export default { + kind: 'Page', + title: 'Theme/Pages/Terms', + component: KcApp, + parameters: { + layout: 'fullscreen', + }, +} as ComponentMeta<typeof KcApp>; + +export const Default = bind({}) diff --git a/src/keycloak-theme/pages/Terms.tsx b/src/keycloak-theme/pages/Terms.tsx index c579d1a..69d1b99 100644 --- a/src/keycloak-theme/pages/Terms.tsx +++ b/src/keycloak-theme/pages/Terms.tsx @@ -1,81 +1,88 @@ /** - * NOTE: Yo do not need to do all this to put your own Terms and conditions - * this is if you want component level customization. - * If the default works for you you can just use the useDownloadTerms hook + * NOTE: You do not need to do all this to put your own Terms and conditions + * this is if you want component level customization. + * If the default works for you, you can just use the useDownloadTerms hook * in the KcApp.tsx * Example: https://github.com/garronej/keycloakify-starter/blob/a20c21b2aae7c6dc6dbea294f3d321955ddf9355/src/KcApp/KcApp.tsx#L14-L30 */ -import { clsx } from "keycloakify/lib/tools/clsx"; -import { useRerenderOnStateChange } from "evt/hooks"; -import { Markdown } from "keycloakify/lib/tools/Markdown"; -import { evtTermMarkdown, useDownloadTerms } from "keycloakify/lib/pages/Terms"; +import {clsx} from "keycloakify/lib/tools/clsx"; +import {useRerenderOnStateChange} from "evt/hooks"; +import {Markdown} from "keycloakify/lib/tools/Markdown"; +import {evtTermMarkdown, useDownloadTerms} from "keycloakify/lib/pages/Terms"; import tos_en_url from "../assets/tos_en.md"; import tos_fr_url from "../assets/tos_fr.md"; -import type { PageProps } from "keycloakify/lib/KcProps"; -import type { KcContext } from "../kcContext"; -import type { I18n } from "../i18n"; +import type {PageProps} from "keycloakify/lib/KcProps"; +import type {KcContext} from "../kcContext"; +import type {I18n} from "../i18n"; export default function Terms(props: PageProps<Extract<KcContext, { pageId: "terms.ftl"; }>, I18n>) { - const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props; + const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props; - const { msg, msgStr } = i18n; + const {msg, msgStr} = i18n; - useDownloadTerms({ - kcContext, - "downloadTermMarkdown": async ({ currentLanguageTag }) => { + useDownloadTerms({ + kcContext, + "downloadTermMarkdown": async ({currentLanguageTag}) => { - const markdownString = await fetch((() => { - switch (currentLanguageTag) { - case "fr": return tos_fr_url; - default: return tos_en_url; - } - })()).then(response => response.text()); + const resource = (() => { + switch (currentLanguageTag) { + case "fr": + return tos_fr_url; + default: + return tos_en_url; + } + })(); - return markdownString; - }, - }); + // webpack5 (used via storybook) loads markdown as string, not url + if (resource.includes("\n")) return resource - useRerenderOnStateChange(evtTermMarkdown); + const response = await fetch(resource); + return response.text(); + }, + }); - const { url } = kcContext; + useRerenderOnStateChange(evtTermMarkdown); - if (evtTermMarkdown.state === undefined) { - return null; - } + const {url} = kcContext; - return ( - <Template - {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }} - displayMessage={false} - headerNode={msg("termsTitle")} - formNode={ - <> - <div id="kc-terms-text">{evtTermMarkdown.state && <Markdown>{evtTermMarkdown.state}</Markdown>}</div> - <form className="form-actions" action={url.loginAction} method="POST"> - <input - className={clsx( - kcProps.kcButtonClass, - kcProps.kcButtonClass, - kcProps.kcButtonClass, - kcProps.kcButtonPrimaryClass, - kcProps.kcButtonLargeClass - )} - name="accept" - id="kc-accept" - type="submit" - value={msgStr("doAccept")} - /> - <input - className={clsx(kcProps.kcButtonClass, kcProps.kcButtonDefaultClass, kcProps.kcButtonLargeClass)} - name="cancel" - id="kc-decline" - type="submit" - value={msgStr("doDecline")} - /> - </form> - <div className="clearfix" /> - </> - } - /> - ); + if (evtTermMarkdown.state === undefined) { + return null; + } + + return ( + <Template + {...{kcContext, i18n, doFetchDefaultThemeResources, ...kcProps}} + displayMessage={false} + headerNode={msg("termsTitle")} + formNode={ + <> + <div id="kc-terms-text">{evtTermMarkdown.state && + <Markdown>{evtTermMarkdown.state}</Markdown>}</div> + <form className="form-actions" action={url.loginAction} method="POST"> + <input + className={clsx( + kcProps.kcButtonClass, + kcProps.kcButtonClass, + kcProps.kcButtonClass, + kcProps.kcButtonPrimaryClass, + kcProps.kcButtonLargeClass + )} + name="accept" + id="kc-accept" + type="submit" + value={msgStr("doAccept")} + /> + <input + className={clsx(kcProps.kcButtonClass, kcProps.kcButtonDefaultClass, kcProps.kcButtonLargeClass)} + name="cancel" + id="kc-decline" + type="submit" + value={msgStr("doDecline")} + /> + </form> + <div className="clearfix"/> + </> + } + /> + ); } |