@thunderid/react 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/auth/flow/Consent.cjs +93 -73
- package/dist/cjs/components/auth/flow/Consent.cjs.map +1 -1
- package/dist/components/auth/flow/Consent.d.ts.map +1 -1
- package/dist/components/auth/flow/Consent.js +94 -75
- package/dist/components/auth/flow/Consent.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,10 +5,18 @@ const require_Typography = require('../../primitives/Typography/Typography.cjs')
|
|
|
5
5
|
const require_Toggle = require('../../primitives/Toggle/Toggle.cjs');
|
|
6
6
|
const require_ConsentCheckboxList = require('./ConsentCheckboxList.cjs');
|
|
7
7
|
const require_Tooltip = require('../../primitives/Tooltip/Tooltip.cjs');
|
|
8
|
+
let react = require("react");
|
|
9
|
+
react = require_rolldown_runtime.__toESM(react);
|
|
8
10
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
11
|
react_jsx_runtime = require_rolldown_runtime.__toESM(react_jsx_runtime);
|
|
10
12
|
|
|
11
13
|
//#region src/components/auth/flow/Consent.tsx
|
|
14
|
+
const isObject = (value) => typeof value === "object" && value !== null;
|
|
15
|
+
/**
|
|
16
|
+
* Keep only well-formed prompt elements (non-null objects with a string `name`).
|
|
17
|
+
* @param value Raw `essential`/`optional` value from the consent payload
|
|
18
|
+
*/
|
|
19
|
+
const toPromptElements = (value) => Array.isArray(value) ? value.filter((element) => isObject(element) && typeof element["name"] === "string") : [];
|
|
12
20
|
/**
|
|
13
21
|
* Consent component renders the list of purposes and their associated attributes (essential and optional)
|
|
14
22
|
* based on the data provided by the backend. It allows users to toggle optional attributes while essential
|
|
@@ -41,21 +49,26 @@ const Consent = ({ consentData, formValues, onInputChange, children, t }) => {
|
|
|
41
49
|
display: "flex",
|
|
42
50
|
gap: "4px"
|
|
43
51
|
});
|
|
44
|
-
/** Resolve i18n keys */
|
|
45
|
-
const resolve = (text) => {
|
|
46
|
-
if (!text || !t) return text || "";
|
|
47
|
-
return t(text);
|
|
48
|
-
};
|
|
49
|
-
const essentialInfo = resolve(`consent.essential_claims.info`);
|
|
50
|
-
const optionalInfo = resolve(`consent.optional_claims.info`);
|
|
51
|
-
const permissionInfo = resolve(`consent.authorize_scope.info`);
|
|
52
52
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
53
|
+
* Resolve an i18n key, falling back to the given value when no translation is found.
|
|
54
|
+
* `t` returns the key itself for missing translations, so that case is treated as "not found".
|
|
55
|
+
* @param key Translation key
|
|
56
|
+
* @param fallback Value to use when the key cannot be resolved
|
|
55
57
|
*/
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
const resolve = (key, fallback = "") => {
|
|
59
|
+
const translated = t?.(key);
|
|
60
|
+
return translated && translated !== key ? translated : fallback;
|
|
61
|
+
};
|
|
62
|
+
const essentialInfo = resolve("consent.essential_claims.info");
|
|
63
|
+
const optionalInfo = resolve("consent.optional_claims.info");
|
|
64
|
+
const essentialPermissionInfo = resolve("consent.essential_scope.info");
|
|
65
|
+
const optionalPermissionInfo = resolve("consent.optional_scope.info");
|
|
66
|
+
const essentialLabel = resolve("consent.essential_claims", "Essential Attributes");
|
|
67
|
+
const optionalLabel = resolve("consent.optional_claims", "Optional Attributes");
|
|
68
|
+
const essentialPermissionLabel = resolve("consent.essential_scope", "Essential Permissions");
|
|
69
|
+
const optionalPermissionLabel = resolve("consent.optional_scope", "Optional Permissions");
|
|
70
|
+
const toggleAllAttributesLabel = resolve("consent.optional_claims.toggle_all", "Toggle all optional attributes");
|
|
71
|
+
const toggleAllPermissionsLabel = resolve("consent.optional_scope.toggle_all", "Toggle all optional permissions");
|
|
59
72
|
/**
|
|
60
73
|
* Method to check whether master toggle button is checked or not
|
|
61
74
|
* @param purpose Purpose object
|
|
@@ -63,7 +76,7 @@ const Consent = ({ consentData, formValues, onInputChange, children, t }) => {
|
|
|
63
76
|
*/
|
|
64
77
|
const handleChange = (purpose, checked) => {
|
|
65
78
|
const checkValue = checked ? "true" : "false";
|
|
66
|
-
purpose.optional.
|
|
79
|
+
purpose.optional.forEach((opt) => {
|
|
67
80
|
onInputChange(require_ConsentCheckboxList.getConsentOptionalKey(purpose.purposeId, opt.name), checkValue);
|
|
68
81
|
});
|
|
69
82
|
};
|
|
@@ -77,14 +90,38 @@ const Consent = ({ consentData, formValues, onInputChange, children, t }) => {
|
|
|
77
90
|
return formValues[require_ConsentCheckboxList.getConsentOptionalKey(purpose.purposeId, opt.name)] === "true";
|
|
78
91
|
});
|
|
79
92
|
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Renders a section title with an optional info tooltip.
|
|
95
|
+
* @param label Section title
|
|
96
|
+
* @param info Tooltip text; the tooltip is omitted when empty
|
|
97
|
+
*/
|
|
98
|
+
const renderSectionTitle = (label, info) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Typography.default, {
|
|
99
|
+
variant: "subtitle2",
|
|
100
|
+
fontWeight: "bold",
|
|
101
|
+
children: label
|
|
102
|
+
}), info && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Tooltip.default, {
|
|
103
|
+
helperText: info,
|
|
104
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Info.default, {
|
|
105
|
+
width: "1rem",
|
|
106
|
+
height: "1rem"
|
|
107
|
+
})
|
|
108
|
+
})] });
|
|
109
|
+
const purposes = (0, react.useMemo)(() => {
|
|
110
|
+
if (!consentData) return [];
|
|
111
|
+
let parsed;
|
|
112
|
+
try {
|
|
113
|
+
parsed = typeof consentData === "string" ? JSON.parse(consentData) : consentData;
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
const rawPurposes = Array.isArray(parsed) ? parsed : parsed?.purposes;
|
|
118
|
+
if (!Array.isArray(rawPurposes)) return [];
|
|
119
|
+
return rawPurposes.filter((purpose) => isObject(purpose) && typeof purpose["purposeId"] === "string").map((purpose) => ({
|
|
120
|
+
...purpose,
|
|
121
|
+
essential: toPromptElements(purpose["essential"]),
|
|
122
|
+
optional: toPromptElements(purpose["optional"])
|
|
123
|
+
})).sort((a, b) => Number(b.type === "permissions") - Number(a.type === "permissions"));
|
|
124
|
+
}, [consentData]);
|
|
88
125
|
if (purposes.length === 0) return null;
|
|
89
126
|
if (children) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: children({
|
|
90
127
|
formValues,
|
|
@@ -93,62 +130,45 @@ const Consent = ({ consentData, formValues, onInputChange, children, t }) => {
|
|
|
93
130
|
}) });
|
|
94
131
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
95
132
|
className: purposesContainerClass,
|
|
96
|
-
children: purposes.map((purpose, purposeIndex) =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
className:
|
|
100
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
101
|
-
className:
|
|
102
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
})
|
|
133
|
+
children: purposes.map((purpose, purposeIndex) => {
|
|
134
|
+
const isPermission = purpose.type === "permissions";
|
|
135
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
136
|
+
className: purposeItemClass,
|
|
137
|
+
children: [purpose.essential?.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
138
|
+
className: sectionClass,
|
|
139
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
140
|
+
className: sectionHeaderClass,
|
|
141
|
+
children: renderSectionTitle(isPermission ? essentialPermissionLabel : essentialLabel, isPermission ? essentialPermissionInfo : essentialInfo)
|
|
142
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_ConsentCheckboxList.default, {
|
|
143
|
+
variant: "ESSENTIAL",
|
|
144
|
+
purpose,
|
|
145
|
+
formValues,
|
|
146
|
+
onInputChange,
|
|
147
|
+
t
|
|
112
148
|
})]
|
|
113
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
114
|
-
|
|
115
|
-
purpose,
|
|
116
|
-
formValues,
|
|
117
|
-
onInputChange,
|
|
118
|
-
t
|
|
119
|
-
})]
|
|
120
|
-
}), purpose.optional && purpose.optional.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
121
|
-
className: sectionClass,
|
|
122
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
123
|
-
className: optionalSectionHeaderClass,
|
|
149
|
+
}), purpose.optional?.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
150
|
+
className: sectionClass,
|
|
124
151
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
125
|
-
className:
|
|
126
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
height: "1rem"
|
|
135
|
-
})
|
|
152
|
+
className: optionalSectionHeaderClass,
|
|
153
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
154
|
+
className: optionalSectionLabelClass,
|
|
155
|
+
children: renderSectionTitle(isPermission ? optionalPermissionLabel : optionalLabel, isPermission ? optionalPermissionInfo : optionalInfo)
|
|
156
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Toggle.default, {
|
|
157
|
+
id: `consent_opt_${purpose.purposeId}_all`,
|
|
158
|
+
checked: checkOptValue(purpose),
|
|
159
|
+
"aria-label": isPermission ? toggleAllPermissionsLabel : toggleAllAttributesLabel,
|
|
160
|
+
onChange: (e) => handleChange(purpose, e.target.checked)
|
|
136
161
|
})]
|
|
137
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
162
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_ConsentCheckboxList.default, {
|
|
163
|
+
variant: "OPTIONAL",
|
|
164
|
+
purpose,
|
|
165
|
+
formValues,
|
|
166
|
+
onInputChange,
|
|
167
|
+
t
|
|
142
168
|
})]
|
|
143
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_ConsentCheckboxList.default, {
|
|
144
|
-
variant: "OPTIONAL",
|
|
145
|
-
purpose,
|
|
146
|
-
formValues,
|
|
147
|
-
onInputChange,
|
|
148
|
-
t
|
|
149
169
|
})]
|
|
150
|
-
})
|
|
151
|
-
}
|
|
170
|
+
}, purpose.purposeId || purposeIndex);
|
|
171
|
+
})
|
|
152
172
|
});
|
|
153
173
|
};
|
|
154
174
|
var Consent_default = Consent;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Consent.cjs","names":["Consent: FC<ConsentProps>","purposesContainerClass: string","css","purposeItemClass: string","sectionClass: string","sectionHeaderClass: string","optionalSectionHeaderClass: string","optionalSectionLabelClass: string","getConsentOptionalKey","purposes: ConsentPurposeData[]","parsed: ConsentPurposeData[] | {purposes: ConsentPurposeData[]}","Typography","Tooltip","Info","ConsentCheckboxList","Toggle"],"sources":["../../../../../src/components/auth/flow/Consent.tsx"],"sourcesContent":["// Copyright 2026 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {type ConsentPurposeData, PromptElement} from '@thunderid/browser';\nimport {type ChangeEvent, FC, ReactNode} from 'react';\nimport ConsentCheckboxList, {getConsentOptionalKey} from './ConsentCheckboxList';\nimport Typography from '../../primitives/Typography/Typography';\nimport Toggle from '../../primitives/Toggle/Toggle';\nimport {Info} from '../../primitives/Icons';\nimport Tooltip from '../../primitives/Tooltip/Tooltip';\nimport {UseTranslation} from '../../../hooks/useTranslation';\nimport {css} from '../../../styles/emotion';\n\n/**\n * Backward-compatible consent purpose type exported by @thunderid/react.\n *\n * Some consumers import this name directly; keep it as an alias to the v2 model.\n */\nexport type ConsentPurpose = ConsentPurposeData;\n\n/**\n * Render props exposed by Consent when using the render-prop pattern.\n */\nexport interface ConsentRenderProps {\n /** Current form values - used to read optional checkbox state. */\n formValues: Record<string, string>;\n /** Callback invoked when a user toggles an optional attribute. */\n onInputChange: (name: string, value: string) => void;\n /** The resolved list of consent purposes parsed from `consentData`. */\n purposes: ConsentPurposeData[];\n}\n\n/**\n * Props for the Consent component.\n */\nexport interface ConsentProps {\n /**\n * Render-props callback. When provided, the default consent UI is replaced with\n * whatever JSX the callback returns. The parsed `purposes` list is injected so\n * consumers do not need to re-parse `consentData` themselves.\n *\n * @example\n * ```tsx\n * <Consent consentData={raw} formValues={formInputs} onInputChange={onChange} t={t}>\n * {({ purposes, formValues, onInputChange, t }) => (\n * <div>\n * {purposes.map(p => <MyConsentSection key={p.purposeId} purpose={p} />)}\n * </div>\n * )}\n * </Consent>\n * ```\n */\n children?: (props: ConsentRenderProps) => ReactNode;\n /**\n * The raw JSON string returned by the backend in `additionalData.consentPrompt`.\n */\n consentData?: string | ConsentPurposeData[] | {purposes: ConsentPurposeData[]};\n /**\n * Current form values - used to read optional checkbox state.\n */\n formValues: Record<string, string>;\n /**\n * Callback invoked when a user toggles an optional attribute.\n */\n onInputChange: (name: string, value: string) => void;\n\n /**\n * translation data\n */\n t?: UseTranslation['t'];\n}\n\n/**\n * Consent component renders the list of purposes and their associated attributes (essential and optional)\n * based on the data provided by the backend. It allows users to toggle optional attributes while essential\n * attributes are displayed as read-only.\n */\nconst Consent: FC<ConsentProps> = ({consentData, formValues, onInputChange, children, t}: ConsentProps) => {\n // Computed per render (not at module scope): a CSP nonce configured on <ThunderIDProvider>\n // isn't known until the provider renders, and Emotion needs it applied before any style\n // insertion happens. These are static, so Emotion's own cache dedupes the repeat calls to a\n // no-op after the first render — this isn't a real per-render cost.\n const purposesContainerClass: string = css({\n display: 'flex',\n flexDirection: 'column',\n gap: '1rem',\n marginTop: '0.25rem',\n });\n const purposeItemClass: string = css({paddingBottom: '1rem'});\n const sectionClass: string = css({marginTop: '0.5rem'});\n const sectionHeaderClass: string = css({alignItems: 'center', display: 'flex', gap: '4px', marginBottom: '10px'});\n const optionalSectionHeaderClass: string = css({\n alignItems: 'center',\n display: 'flex',\n justifyContent: 'space-between',\n marginBottom: '10px',\n paddingRight: '4px',\n });\n const optionalSectionLabelClass: string = css({alignItems: 'center', display: 'flex', gap: '4px'});\n\n /** Resolve i18n keys */\n const resolve = (text: string | undefined): string => {\n if (!text || !t) {\n return text || '';\n }\n return t(text);\n };\n\n const essentialInfo = resolve(`consent.essential_claims.info`);\n const optionalInfo = resolve(`consent.optional_claims.info`);\n const permissionInfo = resolve(`consent.authorize_scope.info`);\n /**\n * Falls back to default config values if essential/optional\n * keys cannot be resolved via translation files .\n */\n const essentialLabel = resolve(`consent.essential_claims`) || 'Essential Attributes';\n const optionalLabel = resolve(`consent.optional_claims`) || 'Optional Attributes';\n const permissionLabel = resolve(`consent.authorize_scope`) || 'Permissions';\n\n /**\n * Method to check whether master toggle button is checked or not\n * @param purpose Purpose object\n * @param checked boolean variable to check, whether toggle is checked or unchecked\n */\n const handleChange = (purpose: ConsentPurposeData, checked: boolean): void => {\n const checkValue = checked ? 'true' : 'false';\n purpose.optional.map((opt: PromptElement) => {\n const key: string = getConsentOptionalKey(purpose.purposeId, opt.name);\n onInputChange(key, checkValue);\n });\n };\n\n /**\n * Check all optional claims are selected or not\n * @param purpose Purpose object\n * @returns boolean value to denote all optional claims are selected\n */\n const checkOptValue = (purpose: ConsentPurposeData): boolean => {\n return purpose.optional.every((opt: PromptElement) => {\n const key: string = getConsentOptionalKey(purpose.purposeId, opt.name);\n return formValues[key] === 'true';\n });\n };\n\n if (!consentData) return null;\n\n let purposes: ConsentPurposeData[] = [];\n\n try {\n const parsed: ConsentPurposeData[] | {purposes: ConsentPurposeData[]} =\n typeof consentData === 'string' ? JSON.parse(consentData) : consentData;\n\n purposes = Array.isArray(parsed) ? parsed : parsed.purposes || [];\n } catch (e) {\n // Failed to parse consent prompt data\n return null;\n }\n\n if (purposes.length === 0) return null;\n\n if (children) {\n return <>{children({formValues, onInputChange, purposes: purposes})}</>;\n }\n\n return (\n <div className={purposesContainerClass}>\n {purposes.map((purpose: ConsentPurposeData, purposeIndex: number) => (\n <div key={purpose.purposeId || purposeIndex} className={purposeItemClass}>\n {/* TODO: Uncomment when the backend supports multiple purposes for a application */}\n {/* <Typography variant=\"h6\" fontWeight={600} gutterBottom color=\"inherit\">\n {purpose.purposeName}\n </Typography>\n <Typography variant=\"body2\" color=\"inherit\" style={{marginBottom: '1rem', opacity: 0.85}}>\n {purpose.description}\n </Typography> */}\n\n {purpose.essential && purpose.essential.length > 0 && (\n <div className={sectionClass}>\n <div className={sectionHeaderClass}>\n <Typography variant=\"subtitle2\" fontWeight=\"bold\">\n {essentialLabel}\n </Typography>\n {essentialInfo !== '' && (\n <Tooltip helperText={essentialInfo}>\n <Info width=\"1rem\" height=\"1rem\" />\n </Tooltip>\n )}\n </div>\n <ConsentCheckboxList\n variant=\"ESSENTIAL\"\n purpose={purpose}\n formValues={formValues}\n onInputChange={onInputChange}\n t={t}\n />\n </div>\n )}\n\n {purpose.optional && purpose.optional.length > 0 && (\n <div className={sectionClass}>\n <div className={optionalSectionHeaderClass}>\n <div className={optionalSectionLabelClass}>\n <Typography variant=\"subtitle2\" fontWeight=\"bold\">\n {purpose.type === 'permissions' ? permissionLabel : optionalLabel}\n </Typography>\n {/* Show tooltip for optional claims/permissions according to their type */}\n {Boolean(purpose.type === 'permissions' ? permissionInfo : optionalInfo) && (\n <Tooltip helperText={purpose.type === 'permissions' ? permissionInfo : optionalInfo}>\n <Info width=\"1rem\" height=\"1rem\" />\n </Tooltip>\n )}\n </div>\n <Toggle\n id={`consent_opt_${purpose.purposeId}_all`}\n checked={checkOptValue(purpose)}\n aria-label=\"Toggle all optional attributes\"\n onChange={(e: ChangeEvent<HTMLInputElement>): void => handleChange(purpose, e.target.checked)}\n />\n </div>\n <ConsentCheckboxList\n variant=\"OPTIONAL\"\n purpose={purpose}\n formValues={formValues}\n onInputChange={onInputChange}\n t={t}\n />\n </div>\n )}\n </div>\n ))}\n </div>\n );\n};\n\nexport default Consent;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Consent.cjs","names":["Consent: FC<ConsentProps>","purposesContainerClass: string","css","purposeItemClass: string","sectionClass: string","sectionHeaderClass: string","optionalSectionHeaderClass: string","optionalSectionLabelClass: string","translated: string | undefined","getConsentOptionalKey","Typography","Tooltip","Info","purposes: ConsentPurposeData[]","parsed: unknown","rawPurposes: unknown","isPermission: boolean","ConsentCheckboxList","Toggle"],"sources":["../../../../../src/components/auth/flow/Consent.tsx"],"sourcesContent":["// Copyright 2026 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {type ConsentPurposeData, PromptElement} from '@thunderid/browser';\nimport {type ChangeEvent, FC, ReactNode, useMemo} from 'react';\nimport ConsentCheckboxList, {getConsentOptionalKey} from './ConsentCheckboxList';\nimport Typography from '../../primitives/Typography/Typography';\nimport Toggle from '../../primitives/Toggle/Toggle';\nimport {Info} from '../../primitives/Icons';\nimport Tooltip from '../../primitives/Tooltip/Tooltip';\nimport {UseTranslation} from '../../../hooks/useTranslation';\nimport {css} from '../../../styles/emotion';\n\n/**\n * Backward-compatible consent purpose type exported by @thunderid/react.\n *\n * Some consumers import this name directly; keep it as an alias to the v2 model.\n */\nexport type ConsentPurpose = ConsentPurposeData;\n\n/**\n * Render props exposed by Consent when using the render-prop pattern.\n */\nexport interface ConsentRenderProps {\n /** Current form values - used to read optional checkbox state. */\n formValues: Record<string, string>;\n /** Callback invoked when a user toggles an optional attribute. */\n onInputChange: (name: string, value: string) => void;\n /** The resolved list of consent purposes parsed from `consentData`. */\n purposes: ConsentPurposeData[];\n}\n\n/**\n * Props for the Consent component.\n */\nexport interface ConsentProps {\n /**\n * Render-props callback. When provided, the default consent UI is replaced with\n * whatever JSX the callback returns. The parsed `purposes` list is injected so\n * consumers do not need to re-parse `consentData` themselves.\n *\n * @example\n * ```tsx\n * <Consent consentData={raw} formValues={formInputs} onInputChange={onChange} t={t}>\n * {({ purposes, formValues, onInputChange, t }) => (\n * <div>\n * {purposes.map(p => <MyConsentSection key={p.purposeId} purpose={p} />)}\n * </div>\n * )}\n * </Consent>\n * ```\n */\n children?: (props: ConsentRenderProps) => ReactNode;\n /**\n * The raw JSON string returned by the backend in `additionalData.consentPrompt`.\n */\n consentData?: string | ConsentPurposeData[] | {purposes: ConsentPurposeData[]};\n /**\n * Current form values - used to read optional checkbox state.\n */\n formValues: Record<string, string>;\n /**\n * Callback invoked when a user toggles an optional attribute.\n */\n onInputChange: (name: string, value: string) => void;\n\n /**\n * translation data\n */\n t?: UseTranslation['t'];\n}\n\nconst isObject = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;\n\n/**\n * Keep only well-formed prompt elements (non-null objects with a string `name`).\n * @param value Raw `essential`/`optional` value from the consent payload\n */\nconst toPromptElements = (value: unknown): PromptElement[] =>\n Array.isArray(value)\n ? (value.filter(\n (element: unknown) => isObject(element) && typeof element['name'] === 'string',\n ) as unknown as PromptElement[])\n : [];\n\n/**\n * Consent component renders the list of purposes and their associated attributes (essential and optional)\n * based on the data provided by the backend. It allows users to toggle optional attributes while essential\n * attributes are displayed as read-only.\n */\nconst Consent: FC<ConsentProps> = ({consentData, formValues, onInputChange, children, t}: ConsentProps) => {\n // Computed per render (not at module scope): a CSP nonce configured on <ThunderIDProvider>\n // isn't known until the provider renders, and Emotion needs it applied before any style\n // insertion happens. These are static, so Emotion's own cache dedupes the repeat calls to a\n // no-op after the first render — this isn't a real per-render cost.\n const purposesContainerClass: string = css({\n display: 'flex',\n flexDirection: 'column',\n gap: '1rem',\n marginTop: '0.25rem',\n });\n const purposeItemClass: string = css({paddingBottom: '1rem'});\n const sectionClass: string = css({marginTop: '0.5rem'});\n const sectionHeaderClass: string = css({alignItems: 'center', display: 'flex', gap: '4px', marginBottom: '10px'});\n const optionalSectionHeaderClass: string = css({\n alignItems: 'center',\n display: 'flex',\n justifyContent: 'space-between',\n marginBottom: '10px',\n paddingRight: '4px',\n });\n const optionalSectionLabelClass: string = css({alignItems: 'center', display: 'flex', gap: '4px'});\n\n /**\n * Resolve an i18n key, falling back to the given value when no translation is found.\n * `t` returns the key itself for missing translations, so that case is treated as \"not found\".\n * @param key Translation key\n * @param fallback Value to use when the key cannot be resolved\n */\n const resolve = (key: string, fallback = ''): string => {\n const translated: string | undefined = t?.(key);\n return translated && translated !== key ? translated : fallback;\n };\n\n const essentialInfo = resolve('consent.essential_claims.info');\n const optionalInfo = resolve('consent.optional_claims.info');\n const essentialPermissionInfo = resolve('consent.essential_scope.info');\n const optionalPermissionInfo = resolve('consent.optional_scope.info');\n const essentialLabel = resolve('consent.essential_claims', 'Essential Attributes');\n const optionalLabel = resolve('consent.optional_claims', 'Optional Attributes');\n const essentialPermissionLabel = resolve('consent.essential_scope', 'Essential Permissions');\n const optionalPermissionLabel = resolve('consent.optional_scope', 'Optional Permissions');\n const toggleAllAttributesLabel = resolve('consent.optional_claims.toggle_all', 'Toggle all optional attributes');\n const toggleAllPermissionsLabel = resolve('consent.optional_scope.toggle_all', 'Toggle all optional permissions');\n\n /**\n * Method to check whether master toggle button is checked or not\n * @param purpose Purpose object\n * @param checked boolean variable to check, whether toggle is checked or unchecked\n */\n const handleChange = (purpose: ConsentPurposeData, checked: boolean): void => {\n const checkValue = checked ? 'true' : 'false';\n purpose.optional.forEach((opt: PromptElement) => {\n const key: string = getConsentOptionalKey(purpose.purposeId, opt.name);\n onInputChange(key, checkValue);\n });\n };\n\n /**\n * Check all optional claims are selected or not\n * @param purpose Purpose object\n * @returns boolean value to denote all optional claims are selected\n */\n const checkOptValue = (purpose: ConsentPurposeData): boolean => {\n return purpose.optional.every((opt: PromptElement) => {\n const key: string = getConsentOptionalKey(purpose.purposeId, opt.name);\n return formValues[key] === 'true';\n });\n };\n\n /**\n * Renders a section title with an optional info tooltip.\n * @param label Section title\n * @param info Tooltip text; the tooltip is omitted when empty\n */\n const renderSectionTitle = (label: string, info: string): ReactNode => (\n <>\n <Typography variant=\"subtitle2\" fontWeight=\"bold\">\n {label}\n </Typography>\n {info && (\n <Tooltip helperText={info}>\n <Info width=\"1rem\" height=\"1rem\" />\n </Tooltip>\n )}\n </>\n );\n\n // Parse, validate and order the purposes once per `consentData` change rather than on every render\n // (e.g. each `formValues` update). Must run before any early return to respect the rules of hooks.\n const purposes: ConsentPurposeData[] = useMemo((): ConsentPurposeData[] => {\n if (!consentData) return [];\n\n let parsed: unknown;\n\n try {\n parsed = typeof consentData === 'string' ? JSON.parse(consentData) : consentData;\n } catch (e) {\n // Failed to parse consent prompt data\n return [];\n }\n\n const rawPurposes: unknown = Array.isArray(parsed) ? parsed : (parsed as {purposes?: unknown})?.purposes;\n\n if (!Array.isArray(rawPurposes)) return [];\n\n // `JSON.parse` doesn't validate the shape, so drop malformed purposes and elements here\n // instead of letting the sort or the render read properties off `null`. Purposes without a\n // `purposeId` are omitted too, since it keys the optional-element form values and they would collide.\n const normalized: ConsentPurposeData[] = rawPurposes\n .filter(\n (purpose: unknown): purpose is Record<string, unknown> =>\n isObject(purpose) && typeof purpose['purposeId'] === 'string',\n )\n .map(\n (purpose: Record<string, unknown>): ConsentPurposeData => ({\n ...(purpose as unknown as ConsentPurposeData),\n essential: toPromptElements(purpose['essential']),\n optional: toPromptElements(purpose['optional']),\n }),\n );\n\n // Order purposes so that permissions come first and attributes come last.\n // `normalized` is already a copy, so sorting doesn't mutate the caller's data; Array.prototype.sort\n // is stable, so purposes of the same type keep their original order.\n return normalized.sort(\n (a: ConsentPurposeData, b: ConsentPurposeData) =>\n Number(b.type === 'permissions') - Number(a.type === 'permissions'),\n );\n }, [consentData]);\n\n if (purposes.length === 0) return null;\n\n if (children) {\n return <>{children({formValues, onInputChange, purposes: purposes})}</>;\n }\n\n return (\n <div className={purposesContainerClass}>\n {purposes.map((purpose: ConsentPurposeData, purposeIndex: number) => {\n // Pick section labels/tooltips according to the purpose type (permissions vs attributes)\n const isPermission: boolean = purpose.type === 'permissions';\n\n return (\n <div key={purpose.purposeId || purposeIndex} className={purposeItemClass}>\n {/* TODO: Uncomment when the backend supports multiple purposes for a application */}\n {/* <Typography variant=\"h6\" fontWeight={600} gutterBottom color=\"inherit\">\n {purpose.purposeName}\n </Typography>\n <Typography variant=\"body2\" color=\"inherit\" style={{marginBottom: '1rem', opacity: 0.85}}>\n {purpose.description}\n </Typography> */}\n\n {purpose.essential?.length > 0 && (\n <div className={sectionClass}>\n <div className={sectionHeaderClass}>\n {renderSectionTitle(\n isPermission ? essentialPermissionLabel : essentialLabel,\n isPermission ? essentialPermissionInfo : essentialInfo,\n )}\n </div>\n <ConsentCheckboxList\n variant=\"ESSENTIAL\"\n purpose={purpose}\n formValues={formValues}\n onInputChange={onInputChange}\n t={t}\n />\n </div>\n )}\n\n {purpose.optional?.length > 0 && (\n <div className={sectionClass}>\n <div className={optionalSectionHeaderClass}>\n <div className={optionalSectionLabelClass}>\n {renderSectionTitle(\n isPermission ? optionalPermissionLabel : optionalLabel,\n isPermission ? optionalPermissionInfo : optionalInfo,\n )}\n </div>\n <Toggle\n id={`consent_opt_${purpose.purposeId}_all`}\n checked={checkOptValue(purpose)}\n aria-label={isPermission ? toggleAllPermissionsLabel : toggleAllAttributesLabel}\n onChange={(e: ChangeEvent<HTMLInputElement>): void => handleChange(purpose, e.target.checked)}\n />\n </div>\n <ConsentCheckboxList\n variant=\"OPTIONAL\"\n purpose={purpose}\n formValues={formValues}\n onInputChange={onInputChange}\n t={t}\n />\n </div>\n )}\n </div>\n );\n })}\n </div>\n );\n};\n\nexport default Consent;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Consent.d.ts","sourceRoot":"","sources":["../../../../src/components/auth/flow/Consent.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAC,KAAK,kBAAkB,EAAgB,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAmB,EAAE,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"Consent.d.ts","sourceRoot":"","sources":["../../../../src/components/auth/flow/Consent.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAC,KAAK,kBAAkB,EAAgB,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAmB,EAAE,EAAE,SAAS,EAAU,MAAM,OAAO,CAAC;AAM/D,OAAO,EAAC,cAAc,EAAC,MAAM,+BAA+B,CAAC;AAG7D;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,kBAAkB,CAAC;AAEhD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,kEAAkE;IAClE,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD,uEAAuE;IACvE,QAAQ,EAAE,kBAAkB,EAAE,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,SAAS,CAAC;IACpD;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,kBAAkB,EAAE,GAAG;QAAC,QAAQ,EAAE,kBAAkB,EAAE,CAAA;KAAC,CAAC;IAC/E;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC;;OAEG;IACH,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAErD;;OAEG;IACH,CAAC,CAAC,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC;CACzB;AAeD;;;;GAIG;AACH,QAAA,MAAM,OAAO,EAAE,EAAE,CAAC,YAAY,CAyM7B,CAAC;AAEF,eAAe,OAAO,CAAC"}
|
|
@@ -4,9 +4,16 @@ import Typography_default from "../../primitives/Typography/Typography.js";
|
|
|
4
4
|
import Toggle_default from "../../primitives/Toggle/Toggle.js";
|
|
5
5
|
import ConsentCheckboxList_default, { getConsentOptionalKey } from "./ConsentCheckboxList.js";
|
|
6
6
|
import Tooltip_default from "../../primitives/Tooltip/Tooltip.js";
|
|
7
|
-
import {
|
|
7
|
+
import { useMemo } from "react";
|
|
8
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
9
|
|
|
9
10
|
//#region src/components/auth/flow/Consent.tsx
|
|
11
|
+
const isObject = (value) => typeof value === "object" && value !== null;
|
|
12
|
+
/**
|
|
13
|
+
* Keep only well-formed prompt elements (non-null objects with a string `name`).
|
|
14
|
+
* @param value Raw `essential`/`optional` value from the consent payload
|
|
15
|
+
*/
|
|
16
|
+
const toPromptElements = (value) => Array.isArray(value) ? value.filter((element) => isObject(element) && typeof element["name"] === "string") : [];
|
|
10
17
|
/**
|
|
11
18
|
* Consent component renders the list of purposes and their associated attributes (essential and optional)
|
|
12
19
|
* based on the data provided by the backend. It allows users to toggle optional attributes while essential
|
|
@@ -39,21 +46,26 @@ const Consent = ({ consentData, formValues, onInputChange, children, t }) => {
|
|
|
39
46
|
display: "flex",
|
|
40
47
|
gap: "4px"
|
|
41
48
|
});
|
|
42
|
-
/** Resolve i18n keys */
|
|
43
|
-
const resolve = (text) => {
|
|
44
|
-
if (!text || !t) return text || "";
|
|
45
|
-
return t(text);
|
|
46
|
-
};
|
|
47
|
-
const essentialInfo = resolve(`consent.essential_claims.info`);
|
|
48
|
-
const optionalInfo = resolve(`consent.optional_claims.info`);
|
|
49
|
-
const permissionInfo = resolve(`consent.authorize_scope.info`);
|
|
50
49
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* Resolve an i18n key, falling back to the given value when no translation is found.
|
|
51
|
+
* `t` returns the key itself for missing translations, so that case is treated as "not found".
|
|
52
|
+
* @param key Translation key
|
|
53
|
+
* @param fallback Value to use when the key cannot be resolved
|
|
53
54
|
*/
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
const resolve = (key, fallback = "") => {
|
|
56
|
+
const translated = t?.(key);
|
|
57
|
+
return translated && translated !== key ? translated : fallback;
|
|
58
|
+
};
|
|
59
|
+
const essentialInfo = resolve("consent.essential_claims.info");
|
|
60
|
+
const optionalInfo = resolve("consent.optional_claims.info");
|
|
61
|
+
const essentialPermissionInfo = resolve("consent.essential_scope.info");
|
|
62
|
+
const optionalPermissionInfo = resolve("consent.optional_scope.info");
|
|
63
|
+
const essentialLabel = resolve("consent.essential_claims", "Essential Attributes");
|
|
64
|
+
const optionalLabel = resolve("consent.optional_claims", "Optional Attributes");
|
|
65
|
+
const essentialPermissionLabel = resolve("consent.essential_scope", "Essential Permissions");
|
|
66
|
+
const optionalPermissionLabel = resolve("consent.optional_scope", "Optional Permissions");
|
|
67
|
+
const toggleAllAttributesLabel = resolve("consent.optional_claims.toggle_all", "Toggle all optional attributes");
|
|
68
|
+
const toggleAllPermissionsLabel = resolve("consent.optional_scope.toggle_all", "Toggle all optional permissions");
|
|
57
69
|
/**
|
|
58
70
|
* Method to check whether master toggle button is checked or not
|
|
59
71
|
* @param purpose Purpose object
|
|
@@ -61,7 +73,7 @@ const Consent = ({ consentData, formValues, onInputChange, children, t }) => {
|
|
|
61
73
|
*/
|
|
62
74
|
const handleChange = (purpose, checked) => {
|
|
63
75
|
const checkValue = checked ? "true" : "false";
|
|
64
|
-
purpose.optional.
|
|
76
|
+
purpose.optional.forEach((opt) => {
|
|
65
77
|
onInputChange(getConsentOptionalKey(purpose.purposeId, opt.name), checkValue);
|
|
66
78
|
});
|
|
67
79
|
};
|
|
@@ -75,78 +87,85 @@ const Consent = ({ consentData, formValues, onInputChange, children, t }) => {
|
|
|
75
87
|
return formValues[getConsentOptionalKey(purpose.purposeId, opt.name)] === "true";
|
|
76
88
|
});
|
|
77
89
|
};
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Renders a section title with an optional info tooltip.
|
|
92
|
+
* @param label Section title
|
|
93
|
+
* @param info Tooltip text; the tooltip is omitted when empty
|
|
94
|
+
*/
|
|
95
|
+
const renderSectionTitle = (label, info) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Typography_default, {
|
|
96
|
+
variant: "subtitle2",
|
|
97
|
+
fontWeight: "bold",
|
|
98
|
+
children: label
|
|
99
|
+
}), info && /* @__PURE__ */ jsx(Tooltip_default, {
|
|
100
|
+
helperText: info,
|
|
101
|
+
children: /* @__PURE__ */ jsx(Info_default, {
|
|
102
|
+
width: "1rem",
|
|
103
|
+
height: "1rem"
|
|
104
|
+
})
|
|
105
|
+
})] });
|
|
106
|
+
const purposes = useMemo(() => {
|
|
107
|
+
if (!consentData) return [];
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = typeof consentData === "string" ? JSON.parse(consentData) : consentData;
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
const rawPurposes = Array.isArray(parsed) ? parsed : parsed?.purposes;
|
|
115
|
+
if (!Array.isArray(rawPurposes)) return [];
|
|
116
|
+
return rawPurposes.filter((purpose) => isObject(purpose) && typeof purpose["purposeId"] === "string").map((purpose) => ({
|
|
117
|
+
...purpose,
|
|
118
|
+
essential: toPromptElements(purpose["essential"]),
|
|
119
|
+
optional: toPromptElements(purpose["optional"])
|
|
120
|
+
})).sort((a, b) => Number(b.type === "permissions") - Number(a.type === "permissions"));
|
|
121
|
+
}, [consentData]);
|
|
86
122
|
if (purposes.length === 0) return null;
|
|
87
|
-
if (children) return /* @__PURE__ */ jsx(Fragment, { children: children({
|
|
123
|
+
if (children) return /* @__PURE__ */ jsx(Fragment$1, { children: children({
|
|
88
124
|
formValues,
|
|
89
125
|
onInputChange,
|
|
90
126
|
purposes
|
|
91
127
|
}) });
|
|
92
128
|
return /* @__PURE__ */ jsx("div", {
|
|
93
129
|
className: purposesContainerClass,
|
|
94
|
-
children: purposes.map((purpose, purposeIndex) =>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
className:
|
|
98
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
99
|
-
className:
|
|
100
|
-
children: [/* @__PURE__ */ jsx(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
})
|
|
130
|
+
children: purposes.map((purpose, purposeIndex) => {
|
|
131
|
+
const isPermission = purpose.type === "permissions";
|
|
132
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
133
|
+
className: purposeItemClass,
|
|
134
|
+
children: [purpose.essential?.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
135
|
+
className: sectionClass,
|
|
136
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
137
|
+
className: sectionHeaderClass,
|
|
138
|
+
children: renderSectionTitle(isPermission ? essentialPermissionLabel : essentialLabel, isPermission ? essentialPermissionInfo : essentialInfo)
|
|
139
|
+
}), /* @__PURE__ */ jsx(ConsentCheckboxList_default, {
|
|
140
|
+
variant: "ESSENTIAL",
|
|
141
|
+
purpose,
|
|
142
|
+
formValues,
|
|
143
|
+
onInputChange,
|
|
144
|
+
t
|
|
110
145
|
})]
|
|
111
|
-
}), /* @__PURE__ */
|
|
112
|
-
|
|
113
|
-
purpose,
|
|
114
|
-
formValues,
|
|
115
|
-
onInputChange,
|
|
116
|
-
t
|
|
117
|
-
})]
|
|
118
|
-
}), purpose.optional && purpose.optional.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
119
|
-
className: sectionClass,
|
|
120
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
121
|
-
className: optionalSectionHeaderClass,
|
|
146
|
+
}), purpose.optional?.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
147
|
+
className: sectionClass,
|
|
122
148
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
123
|
-
className:
|
|
124
|
-
children: [/* @__PURE__ */ jsx(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
height: "1rem"
|
|
133
|
-
})
|
|
149
|
+
className: optionalSectionHeaderClass,
|
|
150
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
151
|
+
className: optionalSectionLabelClass,
|
|
152
|
+
children: renderSectionTitle(isPermission ? optionalPermissionLabel : optionalLabel, isPermission ? optionalPermissionInfo : optionalInfo)
|
|
153
|
+
}), /* @__PURE__ */ jsx(Toggle_default, {
|
|
154
|
+
id: `consent_opt_${purpose.purposeId}_all`,
|
|
155
|
+
checked: checkOptValue(purpose),
|
|
156
|
+
"aria-label": isPermission ? toggleAllPermissionsLabel : toggleAllAttributesLabel,
|
|
157
|
+
onChange: (e) => handleChange(purpose, e.target.checked)
|
|
134
158
|
})]
|
|
135
|
-
}), /* @__PURE__ */ jsx(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
159
|
+
}), /* @__PURE__ */ jsx(ConsentCheckboxList_default, {
|
|
160
|
+
variant: "OPTIONAL",
|
|
161
|
+
purpose,
|
|
162
|
+
formValues,
|
|
163
|
+
onInputChange,
|
|
164
|
+
t
|
|
140
165
|
})]
|
|
141
|
-
}), /* @__PURE__ */ jsx(ConsentCheckboxList_default, {
|
|
142
|
-
variant: "OPTIONAL",
|
|
143
|
-
purpose,
|
|
144
|
-
formValues,
|
|
145
|
-
onInputChange,
|
|
146
|
-
t
|
|
147
166
|
})]
|
|
148
|
-
})
|
|
149
|
-
}
|
|
167
|
+
}, purpose.purposeId || purposeIndex);
|
|
168
|
+
})
|
|
150
169
|
});
|
|
151
170
|
};
|
|
152
171
|
var Consent_default = Consent;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Consent.js","names":["Consent: FC<ConsentProps>","purposesContainerClass: string","purposeItemClass: string","sectionClass: string","sectionHeaderClass: string","optionalSectionHeaderClass: string","optionalSectionLabelClass: string","purposes: ConsentPurposeData[]","parsed: ConsentPurposeData[] | {purposes: ConsentPurposeData[]}","Typography","Tooltip","Info","ConsentCheckboxList","Toggle"],"sources":["../../../../src/components/auth/flow/Consent.tsx"],"sourcesContent":["// Copyright 2026 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {type ConsentPurposeData, PromptElement} from '@thunderid/browser';\nimport {type ChangeEvent, FC, ReactNode} from 'react';\nimport ConsentCheckboxList, {getConsentOptionalKey} from './ConsentCheckboxList';\nimport Typography from '../../primitives/Typography/Typography';\nimport Toggle from '../../primitives/Toggle/Toggle';\nimport {Info} from '../../primitives/Icons';\nimport Tooltip from '../../primitives/Tooltip/Tooltip';\nimport {UseTranslation} from '../../../hooks/useTranslation';\nimport {css} from '../../../styles/emotion';\n\n/**\n * Backward-compatible consent purpose type exported by @thunderid/react.\n *\n * Some consumers import this name directly; keep it as an alias to the v2 model.\n */\nexport type ConsentPurpose = ConsentPurposeData;\n\n/**\n * Render props exposed by Consent when using the render-prop pattern.\n */\nexport interface ConsentRenderProps {\n /** Current form values - used to read optional checkbox state. */\n formValues: Record<string, string>;\n /** Callback invoked when a user toggles an optional attribute. */\n onInputChange: (name: string, value: string) => void;\n /** The resolved list of consent purposes parsed from `consentData`. */\n purposes: ConsentPurposeData[];\n}\n\n/**\n * Props for the Consent component.\n */\nexport interface ConsentProps {\n /**\n * Render-props callback. When provided, the default consent UI is replaced with\n * whatever JSX the callback returns. The parsed `purposes` list is injected so\n * consumers do not need to re-parse `consentData` themselves.\n *\n * @example\n * ```tsx\n * <Consent consentData={raw} formValues={formInputs} onInputChange={onChange} t={t}>\n * {({ purposes, formValues, onInputChange, t }) => (\n * <div>\n * {purposes.map(p => <MyConsentSection key={p.purposeId} purpose={p} />)}\n * </div>\n * )}\n * </Consent>\n * ```\n */\n children?: (props: ConsentRenderProps) => ReactNode;\n /**\n * The raw JSON string returned by the backend in `additionalData.consentPrompt`.\n */\n consentData?: string | ConsentPurposeData[] | {purposes: ConsentPurposeData[]};\n /**\n * Current form values - used to read optional checkbox state.\n */\n formValues: Record<string, string>;\n /**\n * Callback invoked when a user toggles an optional attribute.\n */\n onInputChange: (name: string, value: string) => void;\n\n /**\n * translation data\n */\n t?: UseTranslation['t'];\n}\n\n/**\n * Consent component renders the list of purposes and their associated attributes (essential and optional)\n * based on the data provided by the backend. It allows users to toggle optional attributes while essential\n * attributes are displayed as read-only.\n */\nconst Consent: FC<ConsentProps> = ({consentData, formValues, onInputChange, children, t}: ConsentProps) => {\n // Computed per render (not at module scope): a CSP nonce configured on <ThunderIDProvider>\n // isn't known until the provider renders, and Emotion needs it applied before any style\n // insertion happens. These are static, so Emotion's own cache dedupes the repeat calls to a\n // no-op after the first render — this isn't a real per-render cost.\n const purposesContainerClass: string = css({\n display: 'flex',\n flexDirection: 'column',\n gap: '1rem',\n marginTop: '0.25rem',\n });\n const purposeItemClass: string = css({paddingBottom: '1rem'});\n const sectionClass: string = css({marginTop: '0.5rem'});\n const sectionHeaderClass: string = css({alignItems: 'center', display: 'flex', gap: '4px', marginBottom: '10px'});\n const optionalSectionHeaderClass: string = css({\n alignItems: 'center',\n display: 'flex',\n justifyContent: 'space-between',\n marginBottom: '10px',\n paddingRight: '4px',\n });\n const optionalSectionLabelClass: string = css({alignItems: 'center', display: 'flex', gap: '4px'});\n\n /** Resolve i18n keys */\n const resolve = (text: string | undefined): string => {\n if (!text || !t) {\n return text || '';\n }\n return t(text);\n };\n\n const essentialInfo = resolve(`consent.essential_claims.info`);\n const optionalInfo = resolve(`consent.optional_claims.info`);\n const permissionInfo = resolve(`consent.authorize_scope.info`);\n /**\n * Falls back to default config values if essential/optional\n * keys cannot be resolved via translation files .\n */\n const essentialLabel = resolve(`consent.essential_claims`) || 'Essential Attributes';\n const optionalLabel = resolve(`consent.optional_claims`) || 'Optional Attributes';\n const permissionLabel = resolve(`consent.authorize_scope`) || 'Permissions';\n\n /**\n * Method to check whether master toggle button is checked or not\n * @param purpose Purpose object\n * @param checked boolean variable to check, whether toggle is checked or unchecked\n */\n const handleChange = (purpose: ConsentPurposeData, checked: boolean): void => {\n const checkValue = checked ? 'true' : 'false';\n purpose.optional.map((opt: PromptElement) => {\n const key: string = getConsentOptionalKey(purpose.purposeId, opt.name);\n onInputChange(key, checkValue);\n });\n };\n\n /**\n * Check all optional claims are selected or not\n * @param purpose Purpose object\n * @returns boolean value to denote all optional claims are selected\n */\n const checkOptValue = (purpose: ConsentPurposeData): boolean => {\n return purpose.optional.every((opt: PromptElement) => {\n const key: string = getConsentOptionalKey(purpose.purposeId, opt.name);\n return formValues[key] === 'true';\n });\n };\n\n if (!consentData) return null;\n\n let purposes: ConsentPurposeData[] = [];\n\n try {\n const parsed: ConsentPurposeData[] | {purposes: ConsentPurposeData[]} =\n typeof consentData === 'string' ? JSON.parse(consentData) : consentData;\n\n purposes = Array.isArray(parsed) ? parsed : parsed.purposes || [];\n } catch (e) {\n // Failed to parse consent prompt data\n return null;\n }\n\n if (purposes.length === 0) return null;\n\n if (children) {\n return <>{children({formValues, onInputChange, purposes: purposes})}</>;\n }\n\n return (\n <div className={purposesContainerClass}>\n {purposes.map((purpose: ConsentPurposeData, purposeIndex: number) => (\n <div key={purpose.purposeId || purposeIndex} className={purposeItemClass}>\n {/* TODO: Uncomment when the backend supports multiple purposes for a application */}\n {/* <Typography variant=\"h6\" fontWeight={600} gutterBottom color=\"inherit\">\n {purpose.purposeName}\n </Typography>\n <Typography variant=\"body2\" color=\"inherit\" style={{marginBottom: '1rem', opacity: 0.85}}>\n {purpose.description}\n </Typography> */}\n\n {purpose.essential && purpose.essential.length > 0 && (\n <div className={sectionClass}>\n <div className={sectionHeaderClass}>\n <Typography variant=\"subtitle2\" fontWeight=\"bold\">\n {essentialLabel}\n </Typography>\n {essentialInfo !== '' && (\n <Tooltip helperText={essentialInfo}>\n <Info width=\"1rem\" height=\"1rem\" />\n </Tooltip>\n )}\n </div>\n <ConsentCheckboxList\n variant=\"ESSENTIAL\"\n purpose={purpose}\n formValues={formValues}\n onInputChange={onInputChange}\n t={t}\n />\n </div>\n )}\n\n {purpose.optional && purpose.optional.length > 0 && (\n <div className={sectionClass}>\n <div className={optionalSectionHeaderClass}>\n <div className={optionalSectionLabelClass}>\n <Typography variant=\"subtitle2\" fontWeight=\"bold\">\n {purpose.type === 'permissions' ? permissionLabel : optionalLabel}\n </Typography>\n {/* Show tooltip for optional claims/permissions according to their type */}\n {Boolean(purpose.type === 'permissions' ? permissionInfo : optionalInfo) && (\n <Tooltip helperText={purpose.type === 'permissions' ? permissionInfo : optionalInfo}>\n <Info width=\"1rem\" height=\"1rem\" />\n </Tooltip>\n )}\n </div>\n <Toggle\n id={`consent_opt_${purpose.purposeId}_all`}\n checked={checkOptValue(purpose)}\n aria-label=\"Toggle all optional attributes\"\n onChange={(e: ChangeEvent<HTMLInputElement>): void => handleChange(purpose, e.target.checked)}\n />\n </div>\n <ConsentCheckboxList\n variant=\"OPTIONAL\"\n purpose={purpose}\n formValues={formValues}\n onInputChange={onInputChange}\n t={t}\n />\n </div>\n )}\n </div>\n ))}\n </div>\n );\n};\n\nexport default Consent;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Consent.js","names":["Consent: FC<ConsentProps>","purposesContainerClass: string","purposeItemClass: string","sectionClass: string","sectionHeaderClass: string","optionalSectionHeaderClass: string","optionalSectionLabelClass: string","translated: string | undefined","Typography","Tooltip","Info","purposes: ConsentPurposeData[]","parsed: unknown","rawPurposes: unknown","isPermission: boolean","ConsentCheckboxList","Toggle"],"sources":["../../../../src/components/auth/flow/Consent.tsx"],"sourcesContent":["// Copyright 2026 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {type ConsentPurposeData, PromptElement} from '@thunderid/browser';\nimport {type ChangeEvent, FC, ReactNode, useMemo} from 'react';\nimport ConsentCheckboxList, {getConsentOptionalKey} from './ConsentCheckboxList';\nimport Typography from '../../primitives/Typography/Typography';\nimport Toggle from '../../primitives/Toggle/Toggle';\nimport {Info} from '../../primitives/Icons';\nimport Tooltip from '../../primitives/Tooltip/Tooltip';\nimport {UseTranslation} from '../../../hooks/useTranslation';\nimport {css} from '../../../styles/emotion';\n\n/**\n * Backward-compatible consent purpose type exported by @thunderid/react.\n *\n * Some consumers import this name directly; keep it as an alias to the v2 model.\n */\nexport type ConsentPurpose = ConsentPurposeData;\n\n/**\n * Render props exposed by Consent when using the render-prop pattern.\n */\nexport interface ConsentRenderProps {\n /** Current form values - used to read optional checkbox state. */\n formValues: Record<string, string>;\n /** Callback invoked when a user toggles an optional attribute. */\n onInputChange: (name: string, value: string) => void;\n /** The resolved list of consent purposes parsed from `consentData`. */\n purposes: ConsentPurposeData[];\n}\n\n/**\n * Props for the Consent component.\n */\nexport interface ConsentProps {\n /**\n * Render-props callback. When provided, the default consent UI is replaced with\n * whatever JSX the callback returns. The parsed `purposes` list is injected so\n * consumers do not need to re-parse `consentData` themselves.\n *\n * @example\n * ```tsx\n * <Consent consentData={raw} formValues={formInputs} onInputChange={onChange} t={t}>\n * {({ purposes, formValues, onInputChange, t }) => (\n * <div>\n * {purposes.map(p => <MyConsentSection key={p.purposeId} purpose={p} />)}\n * </div>\n * )}\n * </Consent>\n * ```\n */\n children?: (props: ConsentRenderProps) => ReactNode;\n /**\n * The raw JSON string returned by the backend in `additionalData.consentPrompt`.\n */\n consentData?: string | ConsentPurposeData[] | {purposes: ConsentPurposeData[]};\n /**\n * Current form values - used to read optional checkbox state.\n */\n formValues: Record<string, string>;\n /**\n * Callback invoked when a user toggles an optional attribute.\n */\n onInputChange: (name: string, value: string) => void;\n\n /**\n * translation data\n */\n t?: UseTranslation['t'];\n}\n\nconst isObject = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;\n\n/**\n * Keep only well-formed prompt elements (non-null objects with a string `name`).\n * @param value Raw `essential`/`optional` value from the consent payload\n */\nconst toPromptElements = (value: unknown): PromptElement[] =>\n Array.isArray(value)\n ? (value.filter(\n (element: unknown) => isObject(element) && typeof element['name'] === 'string',\n ) as unknown as PromptElement[])\n : [];\n\n/**\n * Consent component renders the list of purposes and their associated attributes (essential and optional)\n * based on the data provided by the backend. It allows users to toggle optional attributes while essential\n * attributes are displayed as read-only.\n */\nconst Consent: FC<ConsentProps> = ({consentData, formValues, onInputChange, children, t}: ConsentProps) => {\n // Computed per render (not at module scope): a CSP nonce configured on <ThunderIDProvider>\n // isn't known until the provider renders, and Emotion needs it applied before any style\n // insertion happens. These are static, so Emotion's own cache dedupes the repeat calls to a\n // no-op after the first render — this isn't a real per-render cost.\n const purposesContainerClass: string = css({\n display: 'flex',\n flexDirection: 'column',\n gap: '1rem',\n marginTop: '0.25rem',\n });\n const purposeItemClass: string = css({paddingBottom: '1rem'});\n const sectionClass: string = css({marginTop: '0.5rem'});\n const sectionHeaderClass: string = css({alignItems: 'center', display: 'flex', gap: '4px', marginBottom: '10px'});\n const optionalSectionHeaderClass: string = css({\n alignItems: 'center',\n display: 'flex',\n justifyContent: 'space-between',\n marginBottom: '10px',\n paddingRight: '4px',\n });\n const optionalSectionLabelClass: string = css({alignItems: 'center', display: 'flex', gap: '4px'});\n\n /**\n * Resolve an i18n key, falling back to the given value when no translation is found.\n * `t` returns the key itself for missing translations, so that case is treated as \"not found\".\n * @param key Translation key\n * @param fallback Value to use when the key cannot be resolved\n */\n const resolve = (key: string, fallback = ''): string => {\n const translated: string | undefined = t?.(key);\n return translated && translated !== key ? translated : fallback;\n };\n\n const essentialInfo = resolve('consent.essential_claims.info');\n const optionalInfo = resolve('consent.optional_claims.info');\n const essentialPermissionInfo = resolve('consent.essential_scope.info');\n const optionalPermissionInfo = resolve('consent.optional_scope.info');\n const essentialLabel = resolve('consent.essential_claims', 'Essential Attributes');\n const optionalLabel = resolve('consent.optional_claims', 'Optional Attributes');\n const essentialPermissionLabel = resolve('consent.essential_scope', 'Essential Permissions');\n const optionalPermissionLabel = resolve('consent.optional_scope', 'Optional Permissions');\n const toggleAllAttributesLabel = resolve('consent.optional_claims.toggle_all', 'Toggle all optional attributes');\n const toggleAllPermissionsLabel = resolve('consent.optional_scope.toggle_all', 'Toggle all optional permissions');\n\n /**\n * Method to check whether master toggle button is checked or not\n * @param purpose Purpose object\n * @param checked boolean variable to check, whether toggle is checked or unchecked\n */\n const handleChange = (purpose: ConsentPurposeData, checked: boolean): void => {\n const checkValue = checked ? 'true' : 'false';\n purpose.optional.forEach((opt: PromptElement) => {\n const key: string = getConsentOptionalKey(purpose.purposeId, opt.name);\n onInputChange(key, checkValue);\n });\n };\n\n /**\n * Check all optional claims are selected or not\n * @param purpose Purpose object\n * @returns boolean value to denote all optional claims are selected\n */\n const checkOptValue = (purpose: ConsentPurposeData): boolean => {\n return purpose.optional.every((opt: PromptElement) => {\n const key: string = getConsentOptionalKey(purpose.purposeId, opt.name);\n return formValues[key] === 'true';\n });\n };\n\n /**\n * Renders a section title with an optional info tooltip.\n * @param label Section title\n * @param info Tooltip text; the tooltip is omitted when empty\n */\n const renderSectionTitle = (label: string, info: string): ReactNode => (\n <>\n <Typography variant=\"subtitle2\" fontWeight=\"bold\">\n {label}\n </Typography>\n {info && (\n <Tooltip helperText={info}>\n <Info width=\"1rem\" height=\"1rem\" />\n </Tooltip>\n )}\n </>\n );\n\n // Parse, validate and order the purposes once per `consentData` change rather than on every render\n // (e.g. each `formValues` update). Must run before any early return to respect the rules of hooks.\n const purposes: ConsentPurposeData[] = useMemo((): ConsentPurposeData[] => {\n if (!consentData) return [];\n\n let parsed: unknown;\n\n try {\n parsed = typeof consentData === 'string' ? JSON.parse(consentData) : consentData;\n } catch (e) {\n // Failed to parse consent prompt data\n return [];\n }\n\n const rawPurposes: unknown = Array.isArray(parsed) ? parsed : (parsed as {purposes?: unknown})?.purposes;\n\n if (!Array.isArray(rawPurposes)) return [];\n\n // `JSON.parse` doesn't validate the shape, so drop malformed purposes and elements here\n // instead of letting the sort or the render read properties off `null`. Purposes without a\n // `purposeId` are omitted too, since it keys the optional-element form values and they would collide.\n const normalized: ConsentPurposeData[] = rawPurposes\n .filter(\n (purpose: unknown): purpose is Record<string, unknown> =>\n isObject(purpose) && typeof purpose['purposeId'] === 'string',\n )\n .map(\n (purpose: Record<string, unknown>): ConsentPurposeData => ({\n ...(purpose as unknown as ConsentPurposeData),\n essential: toPromptElements(purpose['essential']),\n optional: toPromptElements(purpose['optional']),\n }),\n );\n\n // Order purposes so that permissions come first and attributes come last.\n // `normalized` is already a copy, so sorting doesn't mutate the caller's data; Array.prototype.sort\n // is stable, so purposes of the same type keep their original order.\n return normalized.sort(\n (a: ConsentPurposeData, b: ConsentPurposeData) =>\n Number(b.type === 'permissions') - Number(a.type === 'permissions'),\n );\n }, [consentData]);\n\n if (purposes.length === 0) return null;\n\n if (children) {\n return <>{children({formValues, onInputChange, purposes: purposes})}</>;\n }\n\n return (\n <div className={purposesContainerClass}>\n {purposes.map((purpose: ConsentPurposeData, purposeIndex: number) => {\n // Pick section labels/tooltips according to the purpose type (permissions vs attributes)\n const isPermission: boolean = purpose.type === 'permissions';\n\n return (\n <div key={purpose.purposeId || purposeIndex} className={purposeItemClass}>\n {/* TODO: Uncomment when the backend supports multiple purposes for a application */}\n {/* <Typography variant=\"h6\" fontWeight={600} gutterBottom color=\"inherit\">\n {purpose.purposeName}\n </Typography>\n <Typography variant=\"body2\" color=\"inherit\" style={{marginBottom: '1rem', opacity: 0.85}}>\n {purpose.description}\n </Typography> */}\n\n {purpose.essential?.length > 0 && (\n <div className={sectionClass}>\n <div className={sectionHeaderClass}>\n {renderSectionTitle(\n isPermission ? essentialPermissionLabel : essentialLabel,\n isPermission ? essentialPermissionInfo : essentialInfo,\n )}\n </div>\n <ConsentCheckboxList\n variant=\"ESSENTIAL\"\n purpose={purpose}\n formValues={formValues}\n onInputChange={onInputChange}\n t={t}\n />\n </div>\n )}\n\n {purpose.optional?.length > 0 && (\n <div className={sectionClass}>\n <div className={optionalSectionHeaderClass}>\n <div className={optionalSectionLabelClass}>\n {renderSectionTitle(\n isPermission ? optionalPermissionLabel : optionalLabel,\n isPermission ? optionalPermissionInfo : optionalInfo,\n )}\n </div>\n <Toggle\n id={`consent_opt_${purpose.purposeId}_all`}\n checked={checkOptValue(purpose)}\n aria-label={isPermission ? toggleAllPermissionsLabel : toggleAllAttributesLabel}\n onChange={(e: ChangeEvent<HTMLInputElement>): void => handleChange(purpose, e.target.checked)}\n />\n </div>\n <ConsentCheckboxList\n variant=\"OPTIONAL\"\n purpose={purpose}\n formValues={formValues}\n onInputChange={onInputChange}\n t={t}\n />\n </div>\n )}\n </div>\n );\n })}\n </div>\n );\n};\n\nexport default Consent;\n"],"mappings":"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"}
|