aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Waldemar Reusch <[email protected]>2023-03-14 17:29:14 +0100
committerGravatar Waldemar Reusch <[email protected]>2023-03-14 17:29:14 +0100
commit1e1a638011d552034da94fd8b4e14786651b2743 (patch)
treeeb714eefa931d183934cda6acdfef29186f0f466
parentUpdate package.json (diff)
feat(storybook): progress on many more pages
-rw-r--r--.storybook/data.ts27
-rw-r--r--.storybook/main.js30
-rw-r--r--.storybook/preview.js16
-rw-r--r--.storybook/util.tsx41
-rwxr-xr-xpackage.json4
-rw-r--r--src/keycloak-theme/Template.stories.tsx32
-rw-r--r--src/keycloak-theme/pages/Error.stories.tsx16
-rw-r--r--src/keycloak-theme/pages/Error.tsx34
-rw-r--r--src/keycloak-theme/pages/IdpReviewUserProfile.stories.tsx17
-rw-r--r--src/keycloak-theme/pages/IdpReviewUserProfile.tsx47
-rw-r--r--src/keycloak-theme/pages/Info.stories.tsx37
-rw-r--r--src/keycloak-theme/pages/Info.tsx49
-rw-r--r--src/keycloak-theme/pages/Login.stories.tsx114
-rw-r--r--src/keycloak-theme/pages/LoginConfigTotp.stories.tsx27
-rw-r--r--src/keycloak-theme/pages/LoginConfigTotp.tsx186
-rw-r--r--src/keycloak-theme/pages/LoginIdpLinkConfirm.stories.tsx17
-rw-r--r--src/keycloak-theme/pages/LoginIdpLinkConfirm.tsx54
-rw-r--r--src/keycloak-theme/pages/LoginIdpLinkEmail.stories.tsx17
-rw-r--r--src/keycloak-theme/pages/LoginIdpLinkEmail.tsx32
-rw-r--r--src/keycloak-theme/pages/LoginOtp.stories.tsx16
-rw-r--r--src/keycloak-theme/pages/LoginOtp.tsx114
-rw-r--r--src/keycloak-theme/pages/LoginPageExpired.stories.tsx16
-rw-r--r--src/keycloak-theme/pages/LoginPageExpired.tsx36
-rw-r--r--src/keycloak-theme/pages/MyExtraPage1.stories.tsx28
-rw-r--r--src/keycloak-theme/pages/MyExtraPage2.stories.tsx33
-rw-r--r--src/keycloak-theme/pages/Register.stories.tsx93
-rw-r--r--src/keycloak-theme/pages/Register.tsx1
-rw-r--r--src/keycloak-theme/pages/RegisterUserProfile.stories.tsx79
-rw-r--r--src/keycloak-theme/pages/Terms.stories.tsx15
-rw-r--r--src/keycloak-theme/pages/Terms.tsx135
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: {
+ }
+ },
+ 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"/>
+ </>
+ }
+ />
+ );
}