Skip to content

Commit

Permalink
feat: Support custom fields in user-profile-widget + Edit/Delete flows (
Browse files Browse the repository at this point in the history
#843)

## Related Issues

fixes descope/etc#8220
Part of descope/etc#6501

## Description

Widget Profile widget to support custom fields presentation - created a
new mixin to support custom fields userAttribute component.


![image](https://github.com/user-attachments/assets/6a6fed3b-6587-4160-aaaa-d466dabaa7c7)

---------

Signed-off-by: Nitzan Peretz <[email protected]>
Co-authored-by: nirgur <[email protected]>
  • Loading branch information
Nitzperetz and nirgur authored Dec 3, 2024
1 parent a49be2b commit d29e1f4
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 6 deletions.
4 changes: 2 additions & 2 deletions packages/widgets/user-profile-widget/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ module.exports = {
global: {
branches: 0,
functions: 12,
lines: 38,
statements: 37,
lines: 36,
statements: 36,
},
},
globals: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import '@descope/core-js-sdk';
import createWebSdk from '@descope/web-js-sdk';
import { createUserSdk } from './createUserSdk';
import '@descope/core-js-sdk';

declare const BUILD_VERSION: string;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,12 @@ export type CustomAttr = {
EditPermissions: string[];
editable: boolean;
};

export enum AttributeTypeName {
TEXT = 'text',
NUMBER = 'number',
BOOLEAN = 'boolean',
SINGLE_SELECT = 'singleSelect',
ARRAY = 'array',
DATE = 'date',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import {
FlowDriver,
ModalDriver,
UserAttributeDriver,
} from '@descope/sdk-component-drivers';
import {
compose,
createSingletonMixin,
withMemCache,
} from '@descope/sdk-helpers';
import { loggerMixin, modalMixin } from '@descope/sdk-mixins';
import { AttributeTypeName } from '../../../api/types';
import { getUserCustomAttrs } from '../../../state/selectors';
import { createFlowTemplate } from '../../helpers';
import { stateManagementMixin } from '../../stateManagementMixin';
import { initWidgetRootMixin } from './initWidgetRootMixin';

export const initUserCustomAttributesMixin = createSingletonMixin(
<T extends CustomElementConstructor>(superclass: T) =>
class UserCustomAttributesMixinClass extends compose(
stateManagementMixin,
loggerMixin,
initWidgetRootMixin,
modalMixin,
)(superclass) {
// flow Id is key in all maps
#editModals: Record<string, ModalDriver> = {};

#editFlows: Record<string, FlowDriver> = {};

#deleteModals: Record<string, ModalDriver> = {};

#deleteFlows: Record<string, FlowDriver> = {};

static getFormattedValue(type: string, val: any) {
if (type === AttributeTypeName.DATE && val) {
// to full date time
return new Date(val).toLocaleString();
}
if (type === AttributeTypeName.BOOLEAN && val !== undefined) {
return !val ? 'False' : 'True';
}
return (val || '').toString();
}

#initEditModalContent(flowId: string) {
this.#editModals[flowId]?.setContent(
createFlowTemplate({
projectId: this.projectId,
flowId,
baseUrl: this.baseUrl,
baseStaticUrl: this.baseStaticUrl,
}),
);
this.#editFlows[flowId]?.onSuccess(() => {
this.#editModals[flowId]?.close();
this.actions.getMe();
});
}

// have 2 init functions for edit and delete modals in order to keep the same standards as the email/phone/name mixin
#initDeleteModalContent(flowId: string) {
this.#deleteModals[flowId]?.setContent(
createFlowTemplate({
projectId: this.projectId,
flowId,
baseUrl: this.baseUrl,
baseStaticUrl: this.baseStaticUrl,
}),
);
this.#deleteFlows[flowId]?.onSuccess(() => {
this.#deleteModals[flowId]?.close();
this.actions.getMe();
});
}

#updateCustomValueUserAttrs = withMemCache(
(userCustomAttributes: ReturnType<typeof getUserCustomAttrs>) => {
const allCustomAttributesComponents =
this.shadowRoot?.querySelectorAll(
'descope-user-attribute[data-id^="customAttributes."]',
);

Array.from(allCustomAttributesComponents).forEach((nodeEle) => {
const attrName = nodeEle.getAttribute('data-id');
const customAttrName = attrName.replace('customAttributes.', '');
const type =
nodeEle.getAttribute('data-type') || AttributeTypeName.TEXT;
const val = userCustomAttributes[customAttrName];

const compInstance = new UserAttributeDriver(nodeEle, {
logger: this.logger,
});

compInstance.value =
UserCustomAttributesMixinClass.getFormattedValue(type, val);

this.#initEditFlow(nodeEle, customAttrName, compInstance);
this.#initDeleteFlow(nodeEle, customAttrName, compInstance);
});
},
);

#initEditFlow(
nodeEle: Element,
customAttrName: string,
compInstance: UserAttributeDriver,
) {
const editFlowId = nodeEle.getAttribute('edit-flow-id');
if (editFlowId) {
this.#editModals[editFlowId] = this.createModal({
'data-id': `edit-${customAttrName}`,
});

this.#editFlows[editFlowId] = new FlowDriver(
() =>
this.#editModals[editFlowId]?.ele?.querySelector('descope-wc'),
{ logger: this.logger },
);
this.#editModals[editFlowId].afterClose =
this.#initEditModalContent.bind(this, editFlowId);

compInstance.onEditClick(() => {
this.#editModals?.[editFlowId]?.open();
});

this.#initEditModalContent(editFlowId);
}
}

#initDeleteFlow(
nodeEle: Element,
customAttrName: string,
compInstance: UserAttributeDriver,
) {
const deleteFlowId = nodeEle.getAttribute('delete-flow-id');
if (deleteFlowId) {
this.#deleteModals[deleteFlowId] = this.createModal({
'data-id': `delete-${customAttrName}`,
});

this.#deleteFlows[deleteFlowId] = new FlowDriver(
() =>
this.#deleteModals[deleteFlowId]?.ele?.querySelector(
'descope-wc',
),
{ logger: this.logger },
);
this.#deleteModals[deleteFlowId].afterClose =
this.#initDeleteModalContent.bind(this, deleteFlowId);

compInstance.onDeleteClick(() => {
this.#deleteModals?.[deleteFlowId]?.open();
});

this.#initDeleteModalContent(deleteFlowId);
}
}

async onWidgetRootReady() {
await super.onWidgetRootReady?.();

this.#updateCustomValueUserAttrs(getUserCustomAttrs(this.state));

this.subscribe(
this.#updateCustomValueUserAttrs.bind(this),
getUserCustomAttrs,
);
}
},
);
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { compose, createSingletonMixin } from '@descope/sdk-helpers';
import { debuggerMixin, themeMixin } from '@descope/sdk-mixins';
import { flowRedirectUrlMixin } from '../flowRedirectUrlMixin';
import { initAvatarMixin } from './initComponentsMixins/initAvatarMixin';
import { initEmailUserAttrMixin } from './initComponentsMixins/initEmailUserAttrMixin';
import { initLogoutMixin } from './initComponentsMixins/initLogoutMixin';
import { initNameUserAttrMixin } from './initComponentsMixins/initNameUserAttrMixin';
import { initPhoneUserAttrMixin } from './initComponentsMixins/initPhoneUserAttrMixin';
import { initPasskeyUserAuthMethodMixin } from './initComponentsMixins/initPasskeyUserAuthMethodMixin';
import { initPasswordUserAuthMethodMixin } from './initComponentsMixins/initPasswordUserAuthMethodMixin';
import { initLogoutMixin } from './initComponentsMixins/initLogoutMixin';
import { flowRedirectUrlMixin } from '../flowRedirectUrlMixin';
import { initPhoneUserAttrMixin } from './initComponentsMixins/initPhoneUserAttrMixin';
import { initUserCustomAttributesMixin } from './initComponentsMixins/initUserCustomAttributesMixin';

export const initMixin = createSingletonMixin(
<T extends CustomElementConstructor>(superclass: T) =>
Expand All @@ -16,6 +17,7 @@ export const initMixin = createSingletonMixin(
debuggerMixin,
themeMixin,
flowRedirectUrlMixin, // This mixin must be before all other mixins that loads flows
initUserCustomAttributesMixin,
initEmailUserAttrMixin,
initAvatarMixin,
initNameUserAttrMixin,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ export const getIsPhoneVerified = createSelector(
);
export const getHasPasskey = createSelector(getMe, (me) => me.webauthn);
export const getHasPassword = createSelector(getMe, (me) => me.password);

export const getUserCustomAttrs = createSelector(
getMe,
(me) => me.customAttributes as Record<string, any>,
);

0 comments on commit d29e1f4

Please sign in to comment.