Skip to content

Commit

Permalink
feat: support components attributes (#1030)
Browse files Browse the repository at this point in the history
## Related Issues

Related to descope/etc#8207

## Description

Adds support for components' attributes via SDK

## Must

- [X] Tests
- [ ] Documentation (if applicable)

---------

Co-authored-by: Bar Saar <[email protected]>
  • Loading branch information
nmacianx and Bars92 authored Feb 26, 2025
1 parent 607ff8a commit a2944a1
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 4 deletions.
9 changes: 9 additions & 0 deletions packages/libs/sdk-mixins/src/mixins/configMixin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ export type ClientScript = {
resultKey?: string;
};

export type ComponentsDynamicAttrs = {
attributes: Record<string, any>;
};

export type ComponentsConfig = Record<string, any> & {
componentsDynamicAttrs?: Record<string, ComponentsDynamicAttrs>;
};

export type ClientCondition = {
operator: Operator;
key: string;
Expand All @@ -48,6 +56,7 @@ export type ClientConditionResult = {
screenName: string;
clientScripts?: ClientScript[];
interactionId: string;
componentsConfig?: ComponentsConfig;
};

export type FlowConfig = {
Expand Down
8 changes: 8 additions & 0 deletions packages/sdks/web-component/src/lib/descope-wc/DescopeWc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
} from '../helpers/templates';
import {
ClientScript,
ComponentsConfig,
CustomScreenState,
Direction,
FlowState,
Expand Down Expand Up @@ -454,6 +455,7 @@ class DescopeWc extends BaseDescopeWc {
ssoRedirect: this.nativeOptions.ssoRedirect,
}
: undefined;
let conditionComponentsConfig: ComponentsConfig = {};

// if there is no execution id we should start a new flow
if (!executionId) {
Expand All @@ -469,6 +471,7 @@ class DescopeWc extends BaseDescopeWc {
conditionInteractionId,
startScreenName,
clientScripts: conditionScripts,
componentsConfig: conditionComponentsConfig,
} = calculateConditions(
{ loginId, code, token, abTestingKey },
flowConfig.conditions,
Expand Down Expand Up @@ -717,6 +720,11 @@ class DescopeWc extends BaseDescopeWc {
loginId,
name: this.sdk.getLastUserDisplayName() || loginId,
},
componentsConfig: {
...flowConfig.componentsConfig,
...conditionComponentsConfig,
...screenState?.componentsConfig,
},
},
htmlFilename: `${readyScreenId}.html`,
htmlLocaleFilename: filenameWithLocale,
Expand Down
1 change: 1 addition & 0 deletions packages/sdks/web-component/src/lib/helpers/conditions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,6 @@ export const calculateConditions = (
startScreenName: conditionResult.met.screenName,
conditionInteractionId: conditionResult.met.interactionId,
clientScripts: conditionResult.met.clientScripts,
componentsConfig: conditionResult.met.componentsConfig,
};
};
25 changes: 22 additions & 3 deletions packages/sdks/web-component/src/lib/helpers/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,29 @@ const setElementConfig = (
if (!componentsConfig) {
return;
}
const { componentsDynamicAttrs, ...rest } = componentsConfig;

const configMap = Object.keys(rest).reduce((acc, componentName) => {
acc[`[name=${componentName}]`] = rest[componentName];
return acc;
}, {});

if (componentsDynamicAttrs) {
Object.keys(componentsDynamicAttrs).forEach((componentSelector) => {
const componentDynamicAttrs = componentsDynamicAttrs[componentSelector];
if (componentDynamicAttrs) {
const { attributes } = componentDynamicAttrs;
if (attributes && Object.keys(attributes).length) {
configMap[componentSelector] = attributes;
}
}
});
}

// collect components that needs configuration from DOM
Object.keys(componentsConfig).forEach((componentName) => {
baseEle.querySelectorAll(`[name=${componentName}]`).forEach((comp) => {
const config = componentsConfig[componentName];
Object.keys(configMap).forEach((componentsSelector) => {
baseEle.querySelectorAll(componentsSelector).forEach((comp) => {
const config = configMap[componentsSelector];

Object.keys(config).forEach((attr) => {
let value = config[attr];
Expand Down
10 changes: 9 additions & 1 deletion packages/sdks/web-component/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ export type Sdk = ReturnType<typeof createSdk>;

export type SdkFlowNext = Sdk['flow']['next'];

export type ComponentsConfig = Record<string, any>;
export type ComponentsDynamicAttrs = {
attributes: Record<string, any>;
};

export type ComponentsConfig = Record<string, any> & {
componentsDynamicAttrs?: Record<string, ComponentsDynamicAttrs>;
};
export type CssVars = Record<string, any>;

type KeepArgsByIndex<F, Indices extends readonly number[]> = F extends (
Expand Down Expand Up @@ -182,6 +188,7 @@ export interface ClientConditionResult {
screenId: string;
screenName: string;
clientScripts?: ClientScript[];
componentsConfig?: ComponentsConfig;
interactionId: string;
}

Expand Down Expand Up @@ -252,6 +259,7 @@ export type FlowConfig = {
},
];
clientScripts?: ClientScript[];
componentsConfig?: ComponentsConfig;
};

export interface ProjectConfiguration {
Expand Down
186 changes: 186 additions & 0 deletions packages/sdks/web-component/test/descope-wc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4748,6 +4748,192 @@ describe('web-component', () => {
timeout: WAIT_TIMEOUT,
});
});

it('should parse componentsAttrs values to screen components after next', async () => {
startMock.mockReturnValueOnce(generateSdkResponse());
nextMock.mockReturnValue(
generateSdkResponse({
screenState: {
componentsConfig: {
componentsDynamicAttrs: {
"[data-connector-id='id123']": {
attributes: {
'test-attr': 'test-value',
'test-attr2': 2,
},
},
"[id='id456']": {
attributes: {
'test-attr': 'test-value3',
},
},
},
},
},
}),
);

pageContent = `<descope-button>click</descope-button><div>Loaded</div><input data-connector-id="id123" class="descope-input" placeholder="input1"></input><input id="id456" class="descope-input" placeholder="input2"></input>`;

document.body.innerHTML = `<h1>Custom element test</h1> <descope-wc flow-id="sign-in" project-id="1"></descope-wc>`;

await waitFor(() => screen.getByShadowText('Loaded'), {
timeout: WAIT_TIMEOUT,
});

fireEvent.click(screen.getByShadowText('click'));

await waitFor(
() =>
expect(screen.getByShadowPlaceholderText('input1')).toHaveAttribute(
'test-attr',
'test-value',
),
{ timeout: WAIT_TIMEOUT },
);
expect(screen.getByShadowPlaceholderText('input1')).toHaveAttribute(
'test-attr2',
'2',
);
expect(screen.getByShadowPlaceholderText('input2')).toHaveAttribute(
'test-attr',
'test-value3',
);
expect(screen.getByShadowPlaceholderText('input2')).not.toHaveAttribute(
'test-attr2',
);
});

it('should parse componentsAttrs values to screen components after start', async () => {
startMock.mockReturnValueOnce(
generateSdkResponse({
screenState: {
componentsConfig: {
componentsDynamicAttrs: {
"[placeholder='input1']": {
attributes: {
'test-attr': 'test-value',
},
},
},
},
},
}),
);

pageContent = `<descope-button>click</descope-button><div>Loaded</div><input class="descope-input" placeholder="input1"></input>`;

document.body.innerHTML = `<h1>Custom element test</h1> <descope-wc flow-id="sign-in" project-id="1"></descope-wc>`;

await waitFor(() => screen.getByShadowText('Loaded'), {
timeout: WAIT_TIMEOUT,
});

await waitFor(
() =>
expect(screen.getByShadowPlaceholderText('input1')).toHaveAttribute(
'test-attr',
'test-value',
),
{ timeout: WAIT_TIMEOUT },
);
});

it('should parse componentsAttrs values to screen components from config', async () => {
configContent = {
...configContent,
flows: {
'sign-in': {
startScreenId: 'screen-0',
componentsConfig: {
componentsDynamicAttrs: {
"[id='id123']": {
attributes: {
'test-attr': 'test-value',
},
},
},
},
},
},
};
pageContent = `<descope-button>click</descope-button><div>Loaded</div><input id="id123" class="descope-input" placeholder="input1"></input>`;

document.body.innerHTML = `<h1>Custom element test</h1> <descope-wc flow-id="sign-in" project-id="1"></descope-wc>`;

await waitFor(() => screen.getByShadowText('Loaded'), {
timeout: WAIT_TIMEOUT,
});

await waitFor(
() =>
expect(screen.getByShadowPlaceholderText('input1')).toHaveAttribute(
'test-attr',
'test-value',
),
{ timeout: WAIT_TIMEOUT },
);

expect(startMock).not.toHaveBeenCalled();
expect(nextMock).not.toHaveBeenCalled();
});

it('should parse componentsAttrs values to screen components from config with condition', async () => {
configContent = {
...configContent,
flows: {
'sign-in': {
conditions: [
{
key: 'idpInitiated',
met: {
interactionId: 'vhz8zebfaw',
screenId: 'met',
},
operator: 'is-true',
predicate: '',
},
{
key: 'ELSE',
met: {
componentsConfig: {
componentsDynamicAttrs: {
"[id='id123']": {
attributes: {
'test-attr': 'test-value',
},
},
},
},
interactionId: 'ELSE',
screenId: 'unmet',
},
unmet: {},
},
],
},
},
};

pageContent = `<descope-button>click</descope-button><div>Loaded</div><input id="id123" class="descope-input" placeholder="input1"></input>`;

document.body.innerHTML = `<h1>Custom element test</h1> <descope-wc flow-id="sign-in" project-id="1"></descope-wc>`;

await waitFor(() => screen.getByShadowText('Loaded'), {
timeout: WAIT_TIMEOUT,
});

await waitFor(
() =>
expect(screen.getByShadowPlaceholderText('input1')).toHaveAttribute(
'test-attr',
'test-value',
),
{ timeout: WAIT_TIMEOUT },
);

expect(startMock).not.toHaveBeenCalled();
});
});

describe('cssVars', () => {
Expand Down

0 comments on commit a2944a1

Please sign in to comment.