Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/javascript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export type {
EmbeddedFlowExecuteRequestConfig,
FlowExecutionError,
ConsentAttributeElement,
PromptElement,
ConsentPurposeDecision,
ConsentDecisions,
ConsentPurposeData,
Expand Down
135 changes: 125 additions & 10 deletions packages/react/src/components/adapters/Consent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,19 @@
* under the License.
*/

import {type ConsentPurposeData} from '@thunderid/browser';
import {FC, ReactNode} from 'react';
import ConsentCheckboxList from './ConsentCheckboxList';
import {
type ConsentPurposeData,
FlowMetadataResponse,
PromptElement,
resolveFlowTemplateLiterals,
} from '@thunderid/browser';
import {type ChangeEvent, FC, ReactNode} from 'react';
import ConsentCheckboxList, {getConsentOptionalKey} from './ConsentCheckboxList';
import Typography from '../primitives/Typography/Typography';
import Toggle from '../primitives/Toggle/Toggle';
import {Info} from '../primitives/Icons';
import Tooltip from '../primitives/Tooltip/Tooltip';
import {UseTranslation} from '../../hooks/useTranslation';

/**
* Backward-compatible consent purpose type exported by @thunderid/react.
Expand All @@ -40,6 +49,16 @@ export interface ConsentRenderProps {
purposes: ConsentPurposeData[];
}

/**
* Interface for consent configuration
*/
export interface ConsentConfig {
essential?: string;
optional?: string;
essentialInfo?: string;
optionalInfo?: string;
}

/**
* Props for the Consent component.
*/
Expand Down Expand Up @@ -73,14 +92,80 @@ export interface ConsentProps {
* Callback invoked when a user toggles an optional attribute.
*/
onInputChange: (name: string, value: string) => void;
/**
* Config to modified detail in consent page
*/
config?: Record<string, unknown>;

/**
* Config of meta response
*/
meta?: FlowMetadataResponse | null;

/**
* translation data
*/
t?: UseTranslation['t'];
}

const defaultConfig: Required<Pick<ConsentConfig, 'essential' | 'optional'>> = {
essential: 'Essential Attributtes',
optional: 'Optional Attributes',
};

/**
* Consent component renders the list of purposes and their associated attributes (essential and optional)
* based on the data provided by the backend. It allows users to toggle optional attributes while essential
* attributes are displayed as read-only.
*/
const Consent: FC<ConsentProps> = ({consentData, formValues, onInputChange, children}: ConsentProps) => {
const Consent: FC<ConsentProps> = ({
consentData,
formValues,
config: suppliedConfig = {},
onInputChange,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
children,
meta,
t,
}: ConsentProps) => {
/** Resolve any remaining {{t()}} or {{meta()}} template expressions in a string at render time. */
const resolve = (text: string | undefined): string => {
if (!text || (!t && !meta)) {
return text || '';
}
return resolveFlowTemplateLiterals(text, {meta, t: t || ((k: string): string => k)});
};

const config: ConsentConfig = {...defaultConfig, ...suppliedConfig};
const essentialInfo = typeof config.essentialInfo === 'string' ? resolve(config.essentialInfo.trim()) : '';
const optionalInfo = typeof config.optionalInfo === 'string' ? resolve(config.optionalInfo.trim()) : '';
const essentialLabel = resolve(config['essential']);
const optionalLabel = resolve(config['optional']);

/**
* Method to check whether master toggle button is checked or not
* @param purpose Purpose object
* @param checked boolean variable to check, whether toggle is checked or unchecked
*/
const handleChange = (purpose: ConsentPurposeData, checked: boolean): void => {
const checkValue = checked ? 'true' : 'false';
purpose.optional.map((opt: PromptElement) => {
const key: string = getConsentOptionalKey(purpose.purposeId, opt.name);
onInputChange(key, checkValue);
});
};

/**
* Check all optional claims are selected or not
* @param purpose Purpose object
* @returns boolean value to denote all optional claims are selected
*/
const checkOptValue = (purpose: ConsentPurposeData): boolean => {
return purpose.optional.every((opt: PromptElement) => {
const key: string = getConsentOptionalKey(purpose.purposeId, opt.name);
return formValues[key] === 'true';
});
};

if (!consentData) return null;

let purposes: ConsentPurposeData[] = [];
Expand Down Expand Up @@ -115,9 +200,16 @@ const Consent: FC<ConsentProps> = ({consentData, formValues, onInputChange, chil

{purpose.essential && purpose.essential.length > 0 && (
<div style={{marginTop: '0.5rem'}}>
<Typography variant="subtitle2" fontWeight="bold">
Essential Attributes
</Typography>
<div style={{display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '10px'}}>
<Typography variant="subtitle2" fontWeight="bold">
{essentialLabel}
</Typography>
{essentialInfo !== '' && (
<Tooltip helperText={essentialInfo}>
<Info width="1rem" height="1rem" />
</Tooltip>
)}
</div>
<ConsentCheckboxList
variant="ESSENTIAL"
purpose={purpose}
Expand All @@ -129,9 +221,32 @@ const Consent: FC<ConsentProps> = ({consentData, formValues, onInputChange, chil

{purpose.optional && purpose.optional.length > 0 && (
<div style={{marginTop: '0.5rem'}}>
<Typography variant="subtitle2" fontWeight="bold">
{purpose.type === 'permissions' ? 'Permissions' : 'Optional Attributes'}
</Typography>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
paddingRight: '4px',
marginBottom: '10px',
}}
>
<div style={{display: 'flex', alignItems: 'center', gap: '4px'}}>
<Typography variant="subtitle2" fontWeight="bold">
{purpose.type === 'permissions' ? 'Permissions' : optionalLabel}
</Typography>
{optionalInfo !== '' && (
<Tooltip helperText={optionalInfo}>
<Info width="1rem" height="1rem" />
</Tooltip>
)}
</div>
<Toggle
id={`consent_opt_${purpose.purposeId}_all`}
checked={checkOptValue(purpose)}
aria-label="Toggle all optional attributes"
onChange={(e: ChangeEvent<HTMLInputElement>): void => handleChange(purpose, e.target.checked)}
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
<ConsentCheckboxList
variant="OPTIONAL"
purpose={purpose}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const useStyles = (theme: Theme, colorScheme: string): Record<string, string> =>
`,
listItem: css`
padding: 0 0.25rem;
margin-bottom: 4px;
`,
listRow: css`
display: flex;
Expand Down
32 changes: 16 additions & 16 deletions packages/react/src/components/adapters/ConsentCheckboxList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {type ConsentPurposeData, withVendorCSSClassPrefix, bem} from '@thunderid
import {type ChangeEvent, FC, ReactNode} from 'react';
import useStyles from './ConsentCheckboxList.styles';
import useTheme from '../../contexts/Theme/useTheme';
import Divider from '../primitives/Divider/Divider';
import Toggle from '../primitives/Toggle/Toggle';
import Typography from '../primitives/Typography/Typography';

Expand Down Expand Up @@ -128,8 +127,8 @@ const ConsentCheckboxList: FC<ConsentCheckboxListProps> = ({
return true;
}
const key: string = getConsentOptionalKey(purpose.purposeId, attrName);
// Default to opted-in (true) when there's no explicit form value
return formValues[key] !== 'false';
// Default to be false when there's no explicit form value
return formValues[key] === 'true';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

const handleChange = (attrName: string, checked: boolean): void => {
Expand Down Expand Up @@ -172,20 +171,21 @@ const ConsentCheckboxList: FC<ConsentCheckboxListProps> = ({
{attr}
</Typography>
</div>
<Toggle
id={inputId}
checked={checked}
disabled={isEssential}
onChange={
isEssential
? undefined
: (e: ChangeEvent<HTMLInputElement>): void => handleChange(attr, e.target.checked)
}
/>
{isEssential ? (
<Typography variant="body2">Required</Typography>
) : (
<Toggle
id={inputId}
checked={checked}
disabled={isEssential}
onChange={
isEssential
? undefined
: (e: ChangeEvent<HTMLInputElement>): void => handleChange(attr, e.target.checked)
}
/>
)}
</div>
<Divider
className={cx(withVendorCSSClassPrefix(bem('consent-checkbox-list', 'divider')), styles['divider'])}
/>
</div>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ const createAuthComponentFromFlow = (
...p.essential.map((e): ConsentAttributeElement => ({approved: !isDeny, name: e.name})),
...p.optional.map(
(e): ConsentAttributeElement => ({
approved: isDeny ? false : formValues[getConsentOptionalKey(p.purposeId, e.name)] !== 'false',
approved: !isDeny && formValues[getConsentOptionalKey(p.purposeId, e.name)] === 'true',
name: e.name,
}),
),
Expand Down Expand Up @@ -772,6 +772,9 @@ const createAuthComponentFromFlow = (
consentData={consentPromptRawData as any}
formValues={formValues}
onInputChange={onInputChange}
config={component.config}
meta={options.meta}
t={options.t}
/>
);
}
Expand Down
Loading
Loading