Compare commits

...

27 Commits

Author SHA1 Message Date
a44f091878 Bump version 2022-09-09 12:56:31 +02:00
1b37ba5339 Feat idp-review-user-profile.ftl #164 2022-09-09 12:56:09 +02:00
bbaa90e997 Rename misnamed default exports, removing doInsertPasswordFields 2022-09-09 10:24:50 +02:00
86e6c4a419 Bump version (changelog ignore) 2022-09-09 02:10:18 +02:00
4159883791 Feature update-user-profile.ftl #164 2022-09-09 02:07:49 +02:00
d8b00da3a1 Update tss-react 2022-09-08 15:27:41 +02:00
a24945bc1b Bump version 2022-09-08 15:18:21 +02:00
158759493f Merge pull request #170 from Tasyp/feature/silent
feat: add silent flag
2022-09-08 15:17:45 +02:00
36e32d6ddc Update src/bin/download-builtin-keycloak-theme.ts 2022-09-08 15:13:21 +02:00
84908e2ec0 Update src/bin/generate-i18n-messages.ts 2022-09-08 15:13:15 +02:00
a2dc51d811 Update src/bin/create-keycloak-email-directory.ts 2022-09-08 15:13:09 +02:00
fb3b0e2c29 fix: make sure external assets flag is a boolean 2022-09-08 15:46:27 +03:00
1a3e4c68bb test: update tests to include silent flag 2022-09-08 15:43:03 +03:00
11b2342da0 feat: add silent flag 2022-09-08 13:52:10 +03:00
80d4a808d3 Update readme 2022-09-07 13:45:34 +02:00
da4146eb59 Bump version (changelog ignore) 2022-09-07 13:40:18 +02:00
a0be35db8b Fix compat with StrictMode react 18 2022-09-07 13:39:54 +02:00
14db9cd523 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2022-09-07 12:02:35 +02:00
0c315385dd Update fix tests 2022-09-07 12:00:23 +02:00
c0a0eb02fb Remove Open collectivity 2022-09-07 11:59:42 +02:00
ee407c32ad Create FUNDING.yaml 2022-09-07 11:53:18 +02:00
9262d21829 Bump version 2022-09-07 11:50:41 +02:00
a13f710325 Important fix for --external-assets 2022-09-07 11:50:24 +02:00
eac1a6036f Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2022-09-07 11:26:18 +02:00
987f3d7586 Bump version (changelog ignore) 2022-09-07 11:26:10 +02:00
875322669c Rename isAppAndKeycloakServerSharingSameDomain to areAppAndKeycloakServerSharingSameDomain #145 2022-09-07 11:25:46 +02:00
33a264b3d0 Update README.md 2022-09-07 00:32:38 +02:00
28 changed files with 600 additions and 322 deletions

4
.github/FUNDING.yaml vendored Normal file
View File

@ -0,0 +1,4 @@
# These are supported funding model platforms
github: [garronej]
custom: ['https://www.ringerhq.com/experts/garronej']

View File

@ -27,7 +27,14 @@
<a href="https://www.keycloakify.dev">Home</a>
-
<a href="https://docs.keycloakify.dev">Documentation</a>
</p>
</p>
<p align="center"> ---- Project starter / Demo setup ---- </p>
<p align="center">
<a href="https://github.com/garronej/keycloakify-starter">CSS Level customization</a>
-
<a href="https://github.com/garronej/keycloakify-advanced-starter">Component Level customization</a>
</p>
<p align="center"> ---- </p>
</p>
@ -37,7 +44,7 @@
</p>
> 🗣 V6 have been released 🎉
> [It features major improvements](/#600).
> [It features major improvements](https://github.com/InseeFrLab/keycloakify#600).
> Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6).
# Changelog highlights

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "6.0.0",
"version": "6.3.0",
"description": "Keycloak theme generator for Reacts app",
"repository": {
"type": "git",
@ -60,6 +60,7 @@
"devDependencies": {
"@emotion/react": "^11.4.1",
"@types/memoizee": "^0.4.7",
"@types/minimist": "^1.2.2",
"@types/node": "^17.0.25",
"@types/react": "18.0.9",
"copyfiles": "^2.4.1",
@ -78,12 +79,13 @@
"evt": "^2.4.0",
"memoizee": "^0.4.15",
"minimal-polyfills": "^2.2.2",
"minimist": "^1.2.6",
"path-browserify": "^1.0.1",
"powerhooks": "^0.20.15",
"react-markdown": "^5.0.3",
"scripting-tools": "^0.19.13",
"tsafe": "^1.0.1",
"tss-react": "^4.1.1",
"tss-react": "^4.1.3",
"zod": "^3.17.10"
}
}

View File

@ -6,11 +6,15 @@ import { join as pathJoin, basename as pathBasename } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import * as fs from "fs";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
if (require.main === module) {
(async () => {
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
if (fs.existsSync(keycloakThemeEmailDirPath)) {
console.log(`There is already a ./${pathBasename(keycloakThemeEmailDirPath)} directory in your project. Aborting.`);
logger.warn(`There is already a ./${pathBasename(keycloakThemeEmailDirPath)} directory in your project. Aborting.`);
process.exit(-1);
}
@ -21,7 +25,8 @@ if (require.main === module) {
downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": builtinKeycloakThemeTmpDirPath
"destDirPath": builtinKeycloakThemeTmpDirPath,
isSilent
});
transformCodebase({
@ -29,7 +34,7 @@ if (require.main === module) {
"destDirPath": keycloakThemeEmailDirPath
});
console.log(`./${pathBasename(keycloakThemeEmailDirPath)} ready to be customized`);
logger.log(`./${pathBasename(keycloakThemeEmailDirPath)} ready to be customized`);
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
})();

View File

@ -4,31 +4,37 @@ import { keycloakThemeBuildingDirPath } from "./keycloakify";
import { join as pathJoin } from "path";
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
export function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string }) {
const { keycloakVersion, destDirPath } = params;
export function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; isSilent: boolean }) {
const { keycloakVersion, destDirPath, isSilent } = params;
for (const ext of ["", "-community"]) {
downloadAndUnzip({
"destDirPath": destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`,
"cacheDirPath": pathJoin(keycloakThemeBuildingDirPath, ".cache")
"cacheDirPath": pathJoin(keycloakThemeBuildingDirPath, ".cache"),
isSilent
});
}
}
if (require.main === module) {
(async () => {
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
const { keycloakVersion } = await promptKeycloakVersion();
const destDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme");
console.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
downloadBuiltinKeycloakTheme({
keycloakVersion,
destDirPath
destDirPath,
isSilent
});
})();
}

View File

@ -5,6 +5,8 @@ import { crawl } from "./tools/crawl";
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
import { getProjectRoot } from "./tools/getProjectRoot";
import { rm_rf, rm_r } from "./tools/rm";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
//NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
// update the version array for generating for newer version.
@ -12,8 +14,11 @@ import { rm_rf, rm_r } from "./tools/rm";
//@ts-ignore
const propertiesParser = require("properties-parser");
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
console.log({ keycloakVersion });
logger.log(JSON.stringify({ keycloakVersion }));
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
@ -21,7 +26,8 @@ for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath
"destDirPath": tmpDirPath,
isSilent
});
type Dictionary = { [idiomId: string]: string };
@ -75,7 +81,7 @@ for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
)
);
console.log(`${filePath} wrote`);
logger.log(`${filePath} wrote`);
});
});
}

View File

@ -11,7 +11,7 @@ type ParsedPackageJson = {
keycloakify?: {
extraPages?: string[];
extraThemeProperties?: string[];
isAppAndKeycloakServerSharingSameDomain?: boolean;
areAppAndKeycloakServerSharingSameDomain?: boolean;
};
};
@ -23,7 +23,7 @@ const zParsedPackageJson = z.object({
.object({
"extraPages": z.array(z.string()).optional(),
"extraThemeProperties": z.array(z.string()).optional(),
"isAppAndKeycloakServerSharingSameDomain": z.boolean().optional()
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional()
})
.optional()
});
@ -35,6 +35,7 @@ export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets
export namespace BuildOptions {
export type Common = {
isSilent: boolean;
version: string;
themeName: string;
extraPages?: string[];
@ -56,11 +57,11 @@ export namespace BuildOptions {
};
export type SameDomain = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: true;
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: false;
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
@ -71,8 +72,9 @@ export function readBuildOptions(params: {
packageJson: string;
CNAME: string | undefined;
isExternalAssetsCliParamProvided: boolean;
isSilent: boolean;
}): BuildOptions {
const { packageJson, CNAME, isExternalAssetsCliParamProvided } = params;
const { packageJson, CNAME, isExternalAssetsCliParamProvided, isSilent } = params;
const parsedPackageJson = zParsedPackageJson.parse(JSON.parse(packageJson));
@ -130,7 +132,8 @@ export function readBuildOptions(params: {
})(),
"version": version,
extraPages,
extraThemeProperties
extraThemeProperties,
isSilent
};
})();
@ -140,10 +143,10 @@ export function readBuildOptions(params: {
"isStandalone": false
});
if (parsedPackageJson.keycloakify?.isAppAndKeycloakServerSharingSameDomain) {
if (parsedPackageJson.keycloakify?.areAppAndKeycloakServerSharingSameDomain) {
return id<BuildOptions.ExternalAssets.SameDomain>({
...commonExternalAssets,
"isAppAndKeycloakServerSharingSameDomain": true
"areAppAndKeycloakServerSharingSameDomain": true
});
} else {
assert(
@ -155,14 +158,14 @@ export function readBuildOptions(params: {
"public/CNAME file.",
"Alternatively, if your app and the Keycloak server are on the same domain, ",
"eg https://example.com is your app and https://example.com/auth is the keycloak",
'admin UI, you can set "keycloakify": { "isAppAndKeycloakServerSharingSameDomain": true }',
'admin UI, you can set "keycloakify": { "areAppAndKeycloakServerSharingSameDomain": true }',
"in your package.json"
].join(" ")
);
return id<BuildOptions.ExternalAssets.DifferentDomains>({
...commonExternalAssets,
"isAppAndKeycloakServerSharingSameDomain": false,
"areAppAndKeycloakServerSharingSameDomain": false,
"urlOrigin": url.origin,
"urlPathname": url.pathname
});

View File

@ -27,7 +27,9 @@ export const pageIds = [
"login-idp-link-email.ftl",
"login-page-expired.ftl",
"login-config-totp.ftl",
"logout-confirm.ftl"
"logout-confirm.ftl",
"update-user-profile.ftl",
"idp-review-user-profile.ftl"
] as const;
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
@ -46,11 +48,11 @@ export namespace BuildOptionsLike {
};
export type SameDomain = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: true;
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: false;
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
@ -76,7 +78,7 @@ export function generateFtlFilesCodeFactory(params: {
const $ = cheerio.load(indexHtmlCode);
fix_imports_statements: {
if (!buildOptions.isStandalone && buildOptions.isAppAndKeycloakServerSharingSameDomain) {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
break fix_imports_statements;
}

View File

@ -11,6 +11,7 @@ import { isInside } from "../tools/isInside";
import type { BuildOptions } from "./BuildOptions";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import { getLogger } from "../tools/logger";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
@ -19,6 +20,7 @@ export namespace BuildOptionsLike {
themeName: string;
extraPages?: string[];
extraThemeProperties?: string[];
isSilent: boolean;
};
export type Standalone = Common & {
@ -34,11 +36,11 @@ export namespace BuildOptionsLike {
};
export type SameDomain = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: true;
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: false;
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
@ -60,6 +62,7 @@ export function generateKeycloakThemeResources(params: {
}): { doBundlesEmailTemplate: boolean } {
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params;
const logger = getLogger({ isSilent: buildOptions.isSilent });
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, "login");
let allCssGlobalsToDefine: Record<string, string> = {};
@ -97,7 +100,7 @@ export function generateKeycloakThemeResources(params: {
}
if (/\.js?$/i.test(filePath)) {
if (!buildOptions.isStandalone && buildOptions.isAppAndKeycloakServerSharingSameDomain) {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
return undefined;
}
@ -117,7 +120,7 @@ export function generateKeycloakThemeResources(params: {
email: {
if (!fs.existsSync(keycloakThemeEmailDirPath)) {
console.log(
logger.log(
[
`Not bundling email template because ${pathBasename(keycloakThemeEmailDirPath)} does not exist`,
`To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈`
@ -154,7 +157,8 @@ export function generateKeycloakThemeResources(params: {
downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath
"destDirPath": tmpDirPath,
isSilent: buildOptions.isSilent
});
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");

View File

@ -5,6 +5,8 @@ import * as child_process from "child_process";
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
import * as fs from "fs";
import { readBuildOptions } from "./BuildOptions";
import { getLogger } from "../tools/logger";
import { getCliOptions } from "../tools/cliOptions";
const reactProjectDirPath = process.cwd();
@ -12,7 +14,9 @@ export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build
export const keycloakThemeEmailDirPath = pathJoin(keycloakThemeBuildingDirPath, "..", "keycloak_email");
export function main() {
console.log("🔏 Building the keycloak theme...⌚");
const { isSilent, hasExternalAssets } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
logger.log("🔏 Building the keycloak theme...⌚");
const buildOptions = readBuildOptions({
"packageJson": fs.readFileSync(pathJoin(reactProjectDirPath, "package.json")).toString("utf8"),
@ -25,7 +29,8 @@ export function main() {
return fs.readFileSync(cnameFilePath).toString("utf8");
})(),
"isExternalAssetsCliParamProvided": process.argv[2]?.toLowerCase() === "--external-assets"
"isExternalAssetsCliParamProvided": hasExternalAssets,
"isSilent": isSilent
});
const { doBundlesEmailTemplate } = generateKeycloakThemeResources({
@ -59,7 +64,7 @@ export function main() {
buildOptions
});
console.log(
logger.log(
[
"",
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`,

View File

@ -57,7 +57,7 @@ export function replaceImportsFromStaticInJsCode(params: { jsCode: string; build
: `
var p= "";
Object.defineProperty(${n}, "p", {
get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}" : p; },
get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : p; },
set: function (value){ p = value;}
});
`
@ -73,13 +73,13 @@ export function replaceImportsFromStaticInJsCode(params: { jsCode: string; build
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
buildOptions.isStandalone
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
: `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}" : ${group}) + "static/`
: `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group}) + "static/`
)
//TODO: Write a test case for this
.replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) =>
buildOptions.isStandalone
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}" : ${group2}) + ${group3},`
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group2}) + ${group3},`
);
return { fixedJsCode };

View File

@ -0,0 +1,15 @@
import parseArgv from "minimist";
export type CliOptions = {
isSilent: boolean;
hasExternalAssets: boolean;
};
export const getCliOptions = (processArgv: string[]): CliOptions => {
const argv = parseArgv(processArgv);
return {
isSilent: typeof argv["silent"] === "boolean" ? argv["silent"] : false,
hasExternalAssets: typeof argv["external-assets"] === "boolean" ? argv["external-assets"] : false
};
};

View File

@ -6,7 +6,13 @@ import { rm, rm_rf } from "./rm";
import * as crypto from "crypto";
/** assert url ends with .zip */
export function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string; cacheDirPath: string }) {
export function downloadAndUnzip(params: {
isSilent: boolean;
url: string;
destDirPath: string;
pathOfDirToExtractInArchive?: string;
cacheDirPath: string;
}) {
const { url, destDirPath, pathOfDirToExtractInArchive, cacheDirPath } = params;
const extractDirPath = pathJoin(
@ -54,7 +60,7 @@ export function downloadAndUnzip(params: { url: string; destDirPath: string; pat
const zipFileBasename = pathBasename(url);
execSync(`curl -L ${url} -o ${zipFileBasename}`, { "cwd": extractDirPath });
execSync(`curl -L ${url} -o ${zipFileBasename} ${params.isSilent ? "-s" : ""}`, { "cwd": extractDirPath });
execSync(`unzip -o ${zipFileBasename}${pathOfDirToExtractInArchive === undefined ? "" : ` "${pathOfDirToExtractInArchive}/**/*"`}`, {
"cwd": extractDirPath

27
src/bin/tools/logger.ts Normal file
View File

@ -0,0 +1,27 @@
type LoggerOpts = {
force?: boolean;
};
type Logger = {
log: (message: string, opts?: LoggerOpts) => void;
warn: (message: string) => void;
error: (message: string) => void;
};
export const getLogger = ({ isSilent }: { isSilent?: boolean } = {}): Logger => {
return {
log: (message, { force } = {}) => {
if (isSilent && !force) {
return;
}
console.log(message);
},
warn: message => {
console.warn(message);
},
error: message => {
console.error(message);
}
};
};

View File

@ -0,0 +1,46 @@
import React, { useState, memo } from "react";
import Template from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
import { UserProfileFormFields } from "./shared/UserProfileCommons";
const IdpReviewUserProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.IdpReviewUserProfile; i18n: I18n } & KcProps) => {
const { cx } = useCssAndCx();
const { msg, msgStr } = i18n;
const { url } = kcContext;
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
headerNode={msg("loginIdpReviewProfileTitle")}
formNode={
<form id="kc-idp-review-profile-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...props} />
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)} />
</div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
<input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
type="submit"
value={msgStr("doSubmit")}
disabled={!isFomSubmittable}
/>
</div>
</div>
</form>
}
/>
);
});
export default IdpReviewUserProfile;

View File

@ -20,6 +20,8 @@ const LoginPageExpired = lazy(() => import("./LoginPageExpired"));
const LoginIdpLinkEmail = lazy(() => import("./LoginIdpLinkEmail"));
const LoginConfigTotp = lazy(() => import("./LoginConfigTotp"));
const LogoutConfirm = lazy(() => import("./LogoutConfirm"));
const UpdateUserProfile = lazy(() => import("./UpdateUserProfile"));
const IdpReviewUserProfile = lazy(() => import("./IdpReviewUserProfile"));
const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...kcProps }: { kcContext: KcContextBase; i18n?: I18n } & KcProps) => {
const i18n = (function useClosure() {
@ -74,6 +76,10 @@ const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...kcProps }: { kcConte
return <LoginConfigTotp {...{ kcContext, ...props }} />;
case "logout-confirm.ftl":
return <LogoutConfirm {...{ kcContext, ...props }} />;
case "update-user-profile.ftl":
return <UpdateUserProfile {...{ kcContext, ...props }} />;
case "idp-review-user-profile.ftl":
return <IdpReviewUserProfile {...{ kcContext, ...props }} />;
}
})()}
</Suspense>

View File

@ -1,12 +1,10 @@
import React, { useMemo, memo, useEffect, useState, Fragment } from "react";
import React, { useMemo, memo, useState } from "react";
import Template from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase, Attribute } from "../getKcContext/KcContextBase";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { ReactComponent } from "../tools/ReactComponent";
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
import { useFormValidationSlice } from "../useFormValidationSlice";
import type { I18n } from "../i18n";
import { UserProfileFormFields } from "./shared/UserProfileCommons";
const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps) => {
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
@ -66,155 +64,4 @@ const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: K
);
});
type UserProfileFormFieldsProps = { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps &
Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
};
const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange, i18n, ...props }: UserProfileFormFieldsProps) => {
const { cx, css } = useCssAndCx();
const { advancedMsg } = i18n;
const {
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
formValidationReducer,
attributesWithPassword
} = useFormValidationSlice({
kcContext,
i18n
});
useEffect(() => {
onIsFormSubmittableValueChange(isFormSubmittable);
}, [isFormSubmittable]);
const onChangeFactory = useCallbackFactory(
(
[name]: [string],
[
{
target: { value }
}
]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>]
) =>
formValidationReducer({
"action": "update value",
name,
"newValue": value
})
);
const onBlurFactory = useCallbackFactory(([name]: [string]) =>
formValidationReducer({
"action": "focus lost",
name
})
);
let currentGroup = "";
return (
<>
{attributesWithPassword.map((attribute, i) => {
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
const formGroupClassName = cx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
return (
<Fragment key={i}>
{group !== currentGroup && (currentGroup = group) !== "" && (
<div className={formGroupClassName}>
<div className={cx(props.kcContentWrapperClass)}>
<label id={`header-${group}`} className={cx(props.kcFormGroupHeader)}>
{advancedMsg(groupDisplayHeader) || currentGroup}
</label>
</div>
{groupDisplayDescription !== "" && (
<div className={cx(props.kcLabelWrapperClass)}>
<label id={`description-${group}`} className={`${cx(props.kcLabelClass)}`}>
{advancedMsg(groupDisplayDescription)}
</label>
</div>
)}
</div>
)}
<div className={formGroupClassName}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor={attribute.name} className={cx(props.kcLabelClass)}>
{advancedMsg(attribute.displayName ?? "")}
</label>
{attribute.required && <>*</>}
</div>
<div className={cx(props.kcInputWrapperClass)}>
{(() => {
const { options } = attribute.validators;
if (options !== undefined) {
return (
<select
id={attribute.name}
name={attribute.name}
onChange={onChangeFactory(attribute.name)}
onBlur={onBlurFactory(attribute.name)}
value={value}
>
{options.options.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
);
}
return (
<input
type={(() => {
switch (attribute.name) {
case "password-confirm":
case "password":
return "password";
default:
return "text";
}
})()}
id={attribute.name}
name={attribute.name}
value={value}
onChange={onChangeFactory(attribute.name)}
className={cx(props.kcInputClass)}
aria-invalid={displayableErrors.length !== 0}
disabled={attribute.readOnly}
autoComplete={attribute.autocomplete}
onBlur={onBlurFactory(attribute.name)}
/>
);
})()}
{displayableErrors.length !== 0 && (
<span
id={`input-error-${attribute.name}`}
className={cx(
props.kcInputErrorMessageClass,
css({
"position": displayableErrors.length === 1 ? "absolute" : undefined,
"& > span": { "display": "block" }
})
)}
aria-live="polite"
>
{displayableErrors.map(({ errorMessage }) => errorMessage)}
</span>
)}
</div>
</div>
</Fragment>
);
})}
</>
);
});
export default RegisterUserProfile;

View File

@ -0,0 +1,71 @@
import React, { useState, memo } from "react";
import Template from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
import { UserProfileFormFields } from "./shared/UserProfileCommons";
const UpdateUserProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.UpdateUserProfile; i18n: I18n } & KcProps) => {
const { cx } = useCssAndCx();
const { msg, msgStr } = i18n;
const { url, isAppInitiatedAction } = kcContext;
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
headerNode={msg("loginProfileTitle")}
formNode={
<form id="kc-update-profile-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...props} />
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)}></div>
</div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
{isAppInitiatedAction ? (
<>
<input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
type="submit"
value={msgStr("doSubmit")}
/>
<button
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
type="submit"
name="cancel-aia"
value="true"
formNoValidate
>
{msg("doCancel")}
</button>
</>
) : (
<input
className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
type="submit"
defaultValue={msgStr("doSubmit")}
disabled={!isFomSubmittable}
/>
)}
</div>
</div>
</form>
}
/>
);
});
export default UpdateUserProfile;

View File

@ -0,0 +1,170 @@
import React, { memo, useEffect, Fragment } from "react";
import type { KcProps } from "../KcProps";
import type { Attribute } from "../../getKcContext/KcContextBase";
import { useCssAndCx } from "../../tools/useCssAndCx";
import type { ReactComponent } from "../../tools/ReactComponent";
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
import { useFormValidationSlice } from "../../useFormValidationSlice";
import type { I18n } from "../../i18n";
import type { Param0 } from "tsafe/Param0";
export type UserProfileFormFieldsProps = {
kcContext: Param0<typeof useFormValidationSlice>["kcContext"];
i18n: I18n;
} & KcProps &
Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
};
export const UserProfileFormFields = memo(
({ kcContext, onIsFormSubmittableValueChange, i18n, BeforeField, AfterField, ...props }: UserProfileFormFieldsProps) => {
const { cx, css } = useCssAndCx();
const { advancedMsg } = i18n;
const {
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
formValidationReducer,
attributesWithPassword
} = useFormValidationSlice({
kcContext,
i18n
});
useEffect(() => {
onIsFormSubmittableValueChange(isFormSubmittable);
}, [isFormSubmittable]);
const onChangeFactory = useCallbackFactory(
(
[name]: [string],
[
{
target: { value }
}
]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>]
) =>
formValidationReducer({
"action": "update value",
name,
"newValue": value
})
);
const onBlurFactory = useCallbackFactory(([name]: [string]) =>
formValidationReducer({
"action": "focus lost",
name
})
);
let currentGroup = "";
return (
<>
{attributesWithPassword.map((attribute, i) => {
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
const formGroupClassName = cx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
return (
<Fragment key={i}>
{group !== currentGroup && (currentGroup = group) !== "" && (
<div className={formGroupClassName}>
<div className={cx(props.kcContentWrapperClass)}>
<label id={`header-${group}`} className={cx(props.kcFormGroupHeader)}>
{advancedMsg(groupDisplayHeader) || currentGroup}
</label>
</div>
{groupDisplayDescription !== "" && (
<div className={cx(props.kcLabelWrapperClass)}>
<label id={`description-${group}`} className={`${cx(props.kcLabelClass)}`}>
{advancedMsg(groupDisplayDescription)}
</label>
</div>
)}
</div>
)}
{BeforeField && <BeforeField attribute={attribute} />}
<div className={formGroupClassName}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor={attribute.name} className={cx(props.kcLabelClass)}>
{advancedMsg(attribute.displayName ?? "")}
</label>
{attribute.required && <>*</>}
</div>
<div className={cx(props.kcInputWrapperClass)}>
{(() => {
const { options } = attribute.validators;
if (options !== undefined) {
return (
<select
id={attribute.name}
name={attribute.name}
onChange={onChangeFactory(attribute.name)}
onBlur={onBlurFactory(attribute.name)}
value={value}
>
{options.options.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
);
}
return (
<input
type={(() => {
switch (attribute.name) {
case "password-confirm":
case "password":
return "password";
default:
return "text";
}
})()}
id={attribute.name}
name={attribute.name}
value={value}
onChange={onChangeFactory(attribute.name)}
className={cx(props.kcInputClass)}
aria-invalid={displayableErrors.length !== 0}
disabled={attribute.readOnly}
autoComplete={attribute.autocomplete}
onBlur={onBlurFactory(attribute.name)}
/>
);
})()}
{displayableErrors.length !== 0 && (
<span
id={`input-error-${attribute.name}`}
className={cx(
props.kcInputErrorMessageClass,
css({
"position": displayableErrors.length === 1 ? "absolute" : undefined,
"& > span": { "display": "block" }
})
)}
aria-live="polite"
>
{displayableErrors.map(({ errorMessage }) => errorMessage)}
</span>
)}
</div>
</div>
{AfterField && <AfterField attribute={attribute} />}
</Fragment>
);
})}
</>
);
}
);

View File

@ -25,7 +25,9 @@ export type KcContextBase =
| KcContextBase.LoginIdpLinkEmail
| KcContextBase.LoginPageExpired
| KcContextBase.LoginConfigTotp
| KcContextBase.LogoutConfirm;
| KcContextBase.LogoutConfirm
| KcContextBase.UpdateUserProfile
| KcContextBase.IdpReviewUserProfile;
export declare namespace KcContextBase {
export type Common = {
@ -270,6 +272,23 @@ export declare namespace KcContextBase {
skipLink?: boolean;
};
};
export type UpdateUserProfile = Common & {
pageId: "update-user-profile.ftl";
profile: {
context: "REGISTRATION_PROFILE";
attributes: Attribute[];
attributesByName: Record<string, Attribute>;
};
};
export type IdpReviewUserProfile = Common & {
pageId: "idp-review-user-profile.ftl";
profile: {
attributes: Attribute[];
attributesByName: Record<string, Attribute>;
};
};
}
export type Attribute = {

View File

@ -47,8 +47,16 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
"source": partialKcContextCustomMock
});
if (partialKcContextCustomMock.pageId === "register-user-profile.ftl") {
assert(kcContextDefaultMock?.pageId === "register-user-profile.ftl");
if (
partialKcContextCustomMock.pageId === "register-user-profile.ftl" ||
partialKcContextCustomMock.pageId === "update-user-profile.ftl" ||
partialKcContextCustomMock.pageId === "idp-review-user-profile.ftl"
) {
assert(
kcContextDefaultMock?.pageId === "register-user-profile.ftl" ||
kcContextDefaultMock?.pageId === "update-user-profile.ftl" ||
kcContextDefaultMock?.pageId === "idp-review-user-profile.ftl"
);
const { attributes } = kcContextDefaultMock.profile;
@ -60,8 +68,6 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
].filter(exclude(undefined));
attributes.forEach(attribute => {
console.log("====>", attribute);
const partialAttribute = partialAttributes.find(({ name }) => name === attribute.name);
const augmentedAttribute: Attribute = {} as any;

View File

@ -7,6 +7,100 @@ import { pathJoin } from "../../../bin/tools/pathJoin";
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
const attributes: Attribute[] = [
{
"validators": {
"username-prohibited-characters": {
"ignore.empty.value": true
},
"up-username-has-value": {},
"length": {
"ignore.empty.value": true,
"min": "3",
"max": "255"
},
"up-duplicate-username": {},
"up-username-mutation": {}
},
"displayName": "${username}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"autocomplete": "username",
"readOnly": false,
"name": "username",
"value": "xxxx"
},
{
"validators": {
"up-email-exists-as-username": {},
"length": {
"max": "255",
"ignore.empty.value": true
},
"up-blank-attribute-value": {
"error-message": "missingEmailMessage",
"fail-on-null": false
},
"up-duplicate-email": {},
"email": {
"ignore.empty.value": true
},
"pattern": {
"ignore.empty.value": true,
"pattern": "gmail\\.com$"
}
},
"displayName": "${email}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"autocomplete": "email",
"readOnly": false,
"name": "email"
},
{
"validators": {
"length": {
"max": "255",
"ignore.empty.value": true
},
"person-name-prohibited-characters": {
"ignore.empty.value": true
},
"up-immutable-attribute": {},
"up-attribute-required-by-metadata-value": {}
},
"displayName": "${firstName}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"readOnly": false,
"name": "firstName"
},
{
"validators": {
"length": {
"max": "255",
"ignore.empty.value": true
},
"person-name-prohibited-characters": {
"ignore.empty.value": true
},
"up-immutable-attribute": {},
"up-attribute-required-by-metadata-value": {}
},
"displayName": "${lastName}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"readOnly": false,
"name": "lastName"
}
];
const attributesByName = Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any;
export const kcContextCommonMock: KcContextBase.Common = {
"url": {
"loginAction": "#",
@ -200,104 +294,8 @@ export const kcContextMocks: KcContextBase[] = [
...registerCommon,
"profile": {
"context": "REGISTRATION_PROFILE" as const,
...(() => {
const attributes: Attribute[] = [
{
"validators": {
"username-prohibited-characters": {
"ignore.empty.value": true
},
"up-username-has-value": {},
"length": {
"ignore.empty.value": true,
"min": "3",
"max": "255"
},
"up-duplicate-username": {},
"up-username-mutation": {}
},
"displayName": "${username}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"autocomplete": "username",
"readOnly": false,
"name": "username",
"value": "xxxx"
},
{
"validators": {
"up-email-exists-as-username": {},
"length": {
"max": "255",
"ignore.empty.value": true
},
"up-blank-attribute-value": {
"error-message": "missingEmailMessage",
"fail-on-null": false
},
"up-duplicate-email": {},
"email": {
"ignore.empty.value": true
},
"pattern": {
"ignore.empty.value": true,
"pattern": "gmail\\.com$"
}
},
"displayName": "${email}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"autocomplete": "email",
"readOnly": false,
"name": "email"
},
{
"validators": {
"length": {
"max": "255",
"ignore.empty.value": true
},
"person-name-prohibited-characters": {
"ignore.empty.value": true
},
"up-immutable-attribute": {},
"up-attribute-required-by-metadata-value": {}
},
"displayName": "${firstName}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"readOnly": false,
"name": "firstName"
},
{
"validators": {
"length": {
"max": "255",
"ignore.empty.value": true
},
"person-name-prohibited-characters": {
"ignore.empty.value": true
},
"up-immutable-attribute": {},
"up-attribute-required-by-metadata-value": {}
},
"displayName": "${lastName}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"readOnly": false,
"name": "lastName"
}
];
return {
attributes,
"attributesByName": Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any
} as any;
})()
attributes,
attributesByName
}
})
];
@ -423,5 +421,22 @@ export const kcContextMocks: KcContextBase[] = [
"baseUrl": "#"
},
"logoutConfirm": { "code": "123", skipLink: false }
}),
id<KcContextBase.UpdateUserProfile>({
...kcContextCommonMock,
"pageId": "update-user-profile.ftl",
"profile": {
"context": "REGISTRATION_PROFILE" as const,
attributes,
attributesByName
}
}),
id<KcContextBase.IdpReviewUserProfile>({
...kcContextCommonMock,
"pageId": "idp-review-user-profile.ftl",
"profile": {
attributes,
attributesByName
}
})
];

View File

@ -83,8 +83,6 @@ export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params:
return;
}
let isMounted = true;
refHasStartedFetching.current = true;
(async () => {
@ -144,10 +142,6 @@ export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params:
})()
]).then(modules => modules.map(module => module.default));
if (!isMounted) {
return;
}
setI18n({
...createI18nTranslationFunctions({
"fallbackMessages": {
@ -180,10 +174,6 @@ export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params:
)
});
})();
return () => {
isMounted = false;
};
}, []);
return i18n ?? null;

View File

@ -304,13 +304,17 @@ export function useGetErrors(params: {
return { getErrors };
}
/**
* NOTE: The attributesWithPassword returned is actually augmented with
* artificial password related attributes only if kcContext.passwordRequired === true
*/
export function useFormValidationSlice(params: {
kcContext: {
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
profile: {
attributes: Attribute[];
};
passwordRequired: boolean;
passwordRequired?: boolean;
realm: { registrationEmailAsUsername: boolean };
};
/** NOTE: Try to avoid passing a new ref every render for better performances. */

View File

@ -14,6 +14,7 @@ generateKeycloakThemeResources({
"extraPages": ["my-custom-page.ftl"],
"extraThemeProperties": ["env=test"],
"isStandalone": true,
"urlPathname": "/keycloakify-demo-app/"
"urlPathname": "/keycloakify-demo-app/",
"isSilent": false
}
});

View File

@ -103,13 +103,13 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
const fixedJsCodeExpected = `
function f() {
return ("kcContext" in window ? "https://demo-app.keycloakify.dev" : a.p) + "static/js/" + ({}[e] || e) + "." + {
return ("kcContext" in window ? "https://demo-app.keycloakify.dev/" : a.p) + "static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
}[e] + ".chunk.js"
}
function sameAsF() {
return ("kcContext" in window ? "https://demo-app.keycloakify.dev" : a.p) + "static/js/" + ({}[e] || e) + "." + {
return ("kcContext" in window ? "https://demo-app.keycloakify.dev/" : a.p) + "static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
}[e] + ".chunk.js"
}
@ -119,7 +119,7 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
if( pd === undefined || pd.configurable ){
var p= "";
Object.defineProperty(__webpack_require__, "p", {
get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev" : p; },
get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev/" : p; },
set: function (value){ p = value; }
});
}
@ -137,7 +137,7 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
if( pd === undefined || pd.configurable ){
var p= "";
Object.defineProperty(t, "p", {
get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev" : p; },
get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev/" : p; },
set: function (value){ p = value; }
});
}

View File

@ -8,6 +8,7 @@ export function setupSampleReactProject() {
downloadAndUnzip({
"url": "https://github.com/InseeFrLab/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
"destDirPath": sampleReactProjectDirPath,
"cacheDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak", ".cache")
"cacheDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak", ".cache"),
"isSilent": false
});
}

View File

@ -265,6 +265,11 @@
resolved "https://registry.yarnpkg.com/@types/memoizee/-/memoizee-0.4.8.tgz#04adc0c266a0f5d72db0556fdda2ba17dad9b519"
integrity sha512-qDpXKGgwKywnQt/64fH1O0LiPA++QGIYeykEUiZ51HymKVRLnUSGcRuF60IfpPeeXiuRwiR/W4y7S5VzbrgLCA==
"@types/minimist@^1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
"@types/node@^17.0.25":
version "17.0.45"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190"
@ -1188,6 +1193,11 @@ minimatch@^3.0.3, minimatch@^3.1.1:
dependencies:
brace-expansion "^1.1.7"
minimist@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mkdirp@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
@ -1733,10 +1743,10 @@ tslib@^2.1.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
tss-react@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-4.1.1.tgz#207220417e4b2f8eb26d8280ab9cdeb385063069"
integrity sha512-K1U2s/GGw+XycUjJGztJsLUhwm8KJWz5afL5WZU3SwMeQsA+gbETM6bSxVk2/DXBdw9uYLL9jkSYPAXh0tfYBw==
tss-react@^4.1.3:
version "4.1.3"
resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-4.1.3.tgz#170a2edd85e32ba3b752416eeb45438c35e90243"
integrity sha512-Ul99aVoVngHY2nwFC87jZlGP+K3LfG9ZRakimOzRwgJz77f/zXWuY2FVzwaSc+OW804twNaxZZRtLlpagex8sg==
dependencies:
"@emotion/cache" "*"
"@emotion/serialize" "*"