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>
-)) => {
- const { kcContext } = getKcContext({ mockPageId: mockData.pageId, mockData: [mockData] })
- return { kcContext: kcContext as NonNullable }
-}
-
-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>;
+
+ const Template = (mockData: MockData) => {
+ const finalMockData = {
+ message: undefined,
+ pageId,
+ ...mockData
+ } as MockData
+ if (!("message" in mockData)) mockData["message"] = undefined
+ const {kcContext} = getKcContext({mockPageId: pageId, mockData: [finalMockData]})
+ return }/>
+ }
+
+ 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;
+
+
+export const Default = bind({})
+export const InFrench = bind({locale: {currentLanguageTag: 'fr'}})
+export const RealmDisplayNameIsHtml = bind({
+ realm: {
+ displayNameHtml: ''
+ }
+})
+
+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;
+
+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, I18n>) {
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
+
+ const { message, client } = kcContext;
+
+ const { msg } = i18n;
+
+ return (
+
+ {message.summary}
+ {client !== undefined && client.baseUrl !== undefined && (
+
+
+ {msg("backToApplication")}
+
+
+ )}
+
+ }
+ />
+ );
+}
\ 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;
+
+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, I18n>) {
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
+
+ const { msg, msgStr } = i18n;
+
+ const { url } = kcContext;
+
+ const [isFomSubmittable, setIsFomSubmittable] = useState(false);
+
+ return (
+
+
+
+
+
+ }
+ />
+ );
+}
\ 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;
+
+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, 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 (
+ {messageHeader}> : <>{message.summary}>}
+ formNode={
+
+
+ {message.summary}
+
+ {requiredActions !== undefined && (
+ {requiredActions.map(requiredAction => msgStr(`requiredAction.${requiredAction}` as const)).join(",")}
+ )}
+
+ {!skipLink && pageRedirectUri !== undefined ? (
+
+ {msg("backToApplication")}
+
+ ) : actionUri !== undefined ? (
+
+ {msg("proceedWithAction")}
+
+ ) : (
+ client.baseUrl !== undefined && (
+
+ {msg("backToApplication")}
+
+ )
+ )}
+
+ }
+ />
+ );
+}
\ 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;
-const pageId = 'login.ftl'
+const bind = template('login.ftl');
-export const Default = () => {
- const { kcContext } = useKcStoryData({ pageId, message: undefined })
- return
-}
-
-export const InFrench = () => {
- const { kcContext } = useKcStoryData({ pageId, message: undefined, locale: { currentLanguageTag: 'fr' } })
- return
-}
-
-export const RealmDisplayNameIsHtml = () => {
- const { kcContext } = useKcStoryData({
- pageId, message: undefined, realm: {
- displayNameHtml: ''
- }
- })
- return
-}
-
-export const NoInternationalization = () => {
- const { kcContext } = useKcStoryData({
- pageId, message: undefined, realm: {
- internationalizationEnabled: false,
- }
- })
- return
-}
-
-export const NoPasswordField = () => {
- const { kcContext } = useKcStoryData({ pageId, message: undefined, realm: { password: false } })
- return
-}
-
-export const RegistrationNotAllowed = () => {
- const { kcContext } = useKcStoryData({ pageId, message: undefined, realm: { registrationAllowed: false } })
- return
-}
-
-export const RememberMeNotAllowed = () => {
- const { kcContext } = useKcStoryData({ pageId, message: undefined, realm: { rememberMe: false } })
- return
-}
-
-export const PasswordResetNotAllowed = () => {
- const { kcContext } = useKcStoryData({ pageId, message: undefined, realm: { resetPasswordAllowed: false } })
- return
-}
-
-export const EmailIsUsername = () => {
- const { kcContext } = useKcStoryData({ pageId, message: undefined, realm: { loginWithEmailAllowed: false } })
- return
-}
-
-export const TryAnotherWay = () => {
- const { kcContext } = useKcStoryData({ pageId, message: undefined, auth: { showTryAnotherWayLink: true } })
- return
-}
-
-export const PresetUsername = () => {
- const { kcContext } = useKcStoryData({ pageId, message: undefined, login: { username: 'max.mustermann@mail.com' } })
- return
-}
-
-export const ReadOnlyPresetUsername = () => {
- const { kcContext } = useKcStoryData({ pageId, message: undefined, login: { username: 'max.mustermann@mail.com' }, usernameEditDisabled: true })
- return
-}
-
-export const WithSocialProviders = () => {
- const { kcContext } = useKcStoryData({
- pageId, message: undefined, social: {
- displayInfo: true,
- providers: socialProviders
- }
- })
- return
-}
-
-export const WithError = () => {
- const { kcContext } = useKcStoryData({ pageId, message: { type: "error", summary: "This is an error" } })
- return
-}
+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: 'max.mustermann@mail.com'}})
+export const WithImmutablePresetUsername = bind({
+ login: {username: 'max.mustermann@mail.com'},
+ 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;
+
+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: (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, I18n>) {
+ const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props;
+
+ const {url, isAppInitiatedAction, totp, mode, messagesPerField} = kcContext;
+
+ const {msg, msgStr} = i18n;
+
+ const algToKeyUriAlg: Record = {
+ "HmacSHA1": "SHA1",
+ "HmacSHA256": "SHA256",
+ "HmacSHA512": "SHA512"
+ };
+
+ return (
+
+
+ -
+
{msg("loginTotpStep1")}
+
+
+ {totp.policy.supportedApplications.map(app => (
+ - {app}
+ ))}
+
+
+
+ {mode && mode == "manual" ? (
+ <>
+ -
+
{msg("loginTotpManualStep2")}
+
+ {totp.totpSecretEncoded}
+
+
+
+ {msg("loginTotpScanBarcode")}
+
+
+
+ -
+
{msg("loginTotpManualStep3")}
+
+
+ -
+ {msg("loginTotpType")}: {msg(`loginTotp.${totp.policy.type}`)}
+
+ -
+ {msg("loginTotpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
+
+ -
+ {msg("loginTotpDigits")}: {totp.policy.digits}
+
+ {totp.policy.type === "totp" ? (
+ -
+ {msg("loginTotpInterval")}: {totp.policy.period}
+
+ ) : (
+ -
+ {msg("loginTotpCounter")}: {totp.policy.initialCounter}
+
+ )}
+
+
+
+ >
+ ) : (
+
+ {msg("loginTotpStep2")}
+
+
+
+
+ {msg("loginTotpUnableToScan")}
+
+
+
+ )}
+
+ {msg("loginTotpStep3")}
+ {msg("loginTotpStep3DeviceName")}
+
+
+
+
+ >
+ }
+ />
+ );
+}
\ 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;
+
+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, I18n>) {
+ const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props;
+
+ const {url, idpAlias} = kcContext;
+
+ const {msg} = i18n;
+
+ return (
+
+
+
+
+
+
+ }
+ />
+ );
+}
\ 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;
+
+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, I18n>) {
+ const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props;
+
+ const {url, realm, brokerContext, idpAlias} = kcContext;
+
+ const {msg} = i18n;
+
+ return (
+
+
+ {msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
+
+
+ {msg("emailLinkIdp2")} {msg("doClickHere")} {msg("emailLinkIdp3")}
+
+
+ {msg("emailLinkIdp4")} {msg("doClickHere")} {msg("emailLinkIdp5")}
+
+ >
+ }
+ />
+ );
+}
\ 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;
+
+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, 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 (
+
+ {otpLogin.userOtpCredentials.length > 1 && (
+
+
+ {otpLogin.userOtpCredentials.map(otpCredential => (
+
+
+
+
+
{otpCredential.userLabel}
+
+
+ ))}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+ );
+}
+
+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;
+
+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, I18n>) {
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
+
+ const { url } = kcContext;
+
+ const { msg } = i18n;
+
+ return (
+
+
+ {msg("pageExpiredMsg1")}
+
+ {msg("doClickHere")}
+ {" "}
+ .
+ {msg("pageExpiredMsg2")}{" "}
+
+ {msg("doClickHere")}
+ {" "}
+ .
+
+ >
+ }
+ />
+ );
+}
\ 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;
-const pageId = 'my-extra-page-1.ftl'
+const bind = template('my-extra-page-1.ftl')
-export const Default = () => {
- const { kcContext } = useKcStoryData({ pageId })
- return
-}
-
-export const InFrench = () => {
- const { kcContext } = useKcStoryData({ pageId, locale: { currentLanguageTag: 'fr' } })
- return
-}
+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;
-const pageId = 'my-extra-page-2.ftl'
+const bind = template('my-extra-page-2.ftl')
-export const Default = () => {
- const { kcContext } = useKcStoryData({ pageId })
- return
-}
+export const Default = bind({})
-export const InFrench = () => {
- const { kcContext } = useKcStoryData({ pageId, locale: { currentLanguageTag: 'fr' } })
- return
-}
-
-export const WithCustomValue = () => {
- const { kcContext } = useKcStoryData({ pageId, someCustomValue: 'Foo Bar Baz' })
- return
-}
+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;
-const pageId = 'register.ftl'
+const bind = template('register.ftl')
-export const Default = () => {
- const { kcContext } = useKcStoryData({ pageId, message: undefined })
- return
-}
+export const Default = bind({})
-export const InFrench = () => {
- const { kcContext } = useKcStoryData({ pageId, message: undefined, locale: { currentLanguageTag: 'fr' } })
- return
-}
+export const WithFieldError = bind({
+ register: {
+ formData: {
+ email: 'max.mustermann@mail.com'
+ }
+ },
+ 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: (fieldName: string, x: T) => fieldName === "email" ? x : undefined,
+ }
+})
-export const WithError = () => {
- const { kcContext } = useKcStoryData({ pageId, message: { type: "error", summary: "This is an error" } })
- return
-}
+export const WithEmailAsUsername = bind({
+ realm: {registrationEmailAsUsername: true}
+})
-export const EmailIsUsername = () => {
- const { kcContext } = useKcStoryData({
- pageId, message: undefined,
- realm: { registrationEmailAsUsername: true }
- })
- return
-}
+export const WithoutPassword = bind({
+ passwordRequired: false
+})
-export const NoPassword = () => {
- const { kcContext } = useKcStoryData({
- pageId, message: undefined, passwordRequired: false
- })
- return
-}
-
-export const WithRecaptcha = () => {
- const { kcContext } = useKcStoryData({
- pageId, message: undefined,
+export const WithRecaptcha = bind({
recaptchaRequired: true,
recaptchaSiteKey: 'foobar'
- })
- return
-}
+})
-export const WithPresets = () => {
- const { kcContext } = useKcStoryData({
- pageId, message: undefined,
+export const WithPresets = bind({
register: {
- formData: {
- firstName: 'Max',
- lastName: 'Mustermann',
- email: 'max.mustermann@mail.com',
- username: 'max.mustermann'
- }
+ formData: {
+ firstName: 'Max',
+ lastName: 'Mustermann',
+ email: 'max.mustermann@mail.com',
+ username: 'max.mustermann'
+ }
}
- })
- return
-
-}
\ 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
+
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;
+
+export const Default = bind({})
+
+export const WithFieldError = bind({
+ profile: {
+ attributes: [
+ {
+ name: "email",
+ value: "max.mustermann@mail.com",
+ }
+ ]
+ }
+})
+
+export const WithPresets = bind({
+ profile: {
+ attributes: [
+ {
+ name: "username",
+ value: "max.mustermann"
+ },
+ {
+ name: "email",
+ value: "max.mustermann@gmail.com",
+ },
+ {
+ 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: "max.mustermann@gmail.com",
+ 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;
+
+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, 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 (
-
- {evtTermMarkdown.state && {evtTermMarkdown.state}}
-
-
- >
- }
- />
- );
+ if (evtTermMarkdown.state === undefined) {
+ return null;
+ }
+
+ return (
+
+ {evtTermMarkdown.state &&
+ {evtTermMarkdown.state}}
+
+
+ >
+ }
+ />
+ );
}