Skip to content

Commit 6908440

Browse files
authored
feat: add totp to UPW (#1043)
1 parent d1eb29a commit 6908440

File tree

8 files changed

+155
-7
lines changed

8 files changed

+155
-7
lines changed

packages/libs/sdk-helpers/src/compose.ts

+31
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,37 @@ export function compose<
128128
fn12: (input: A11) => A12,
129129
): (input: Input) => A12;
130130

131+
export function compose<
132+
Input,
133+
A1,
134+
A2,
135+
A3,
136+
A4,
137+
A5,
138+
A6,
139+
A7,
140+
A8,
141+
A9,
142+
A10,
143+
A11,
144+
A12,
145+
A13,
146+
>(
147+
fn1: (input: Input) => A1,
148+
fn2: (input: A1) => A2,
149+
fn3: (input: A2) => A3,
150+
fn4: (input: A3) => A4,
151+
fn5: (input: A4) => A5,
152+
fn6: (input: A5) => A6,
153+
fn7: (input: A6) => A7,
154+
fn8: (input: A7) => A8,
155+
fn9: (input: A8) => A9,
156+
fn10: (input: A9) => A10,
157+
fn11: (input: A10) => A11,
158+
fn12: (input: A11) => A12,
159+
fn13: (input: A12) => A13,
160+
): (input: Input) => A13;
161+
131162
/**
132163
* Currently there is no way to create a compose function in Typescript without using overloading
133164
* This function currently support up to 10 wrappers

packages/widgets/user-profile-widget/e2e/user-profile-widget.spec.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ test.describe('widget', () => {
109109

110110
expect(isLoggedOut).toBe(true);
111111
});
112-
test.describe('user auth methods', () => {
112+
test.describe('user attributes', () => {
113113
// eslint-disable-next-line no-restricted-syntax
114114
for (const attr of [
115115
{ name: 'email', action: 'edit', newValue: '[email protected]' },
@@ -153,11 +153,12 @@ test.describe('widget', () => {
153153
}
154154
});
155155

156-
test.describe('user attributes', () => {
156+
test.describe('user auth methods', () => {
157157
// eslint-disable-next-line no-restricted-syntax
158158
for (const attr of [
159159
{ name: 'passkey', flagPath: 'webauthn', fulfilled: 'true' },
160160
{ name: 'password', flagPath: 'password', fulfilled: null },
161+
{ name: 'totp', flagPath: 'TOTP', fulfilled: 'true' },
161162
]) {
162163
test(`${attr.name}`, async ({ page }) => {
163164
await page.waitForTimeout(STATE_TIMEOUT);

packages/widgets/user-profile-widget/src/lib/widget/mixins/initMixin/initComponentsMixins/initPasskeyUserAuthMethodMixin.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export const initPasskeyUserAuthMethodMixin = createSingletonMixin(
6565
});
6666
}
6767

68-
#initPhoneUserAttr() {
68+
#initPasskeyAuthMethod() {
6969
this.passkeyUserAuthMethod = new UserAuthMethodDriver(
7070
() =>
7171
this.shadowRoot?.querySelector(
@@ -88,7 +88,7 @@ export const initPasskeyUserAuthMethodMixin = createSingletonMixin(
8888
async onWidgetRootReady() {
8989
await super.onWidgetRootReady?.();
9090

91-
this.#initPhoneUserAttr();
91+
this.#initPasskeyAuthMethod();
9292
this.#initModal();
9393

9494
this.#onFulfilledUpdate(getHasPasskey(this.state));

packages/widgets/user-profile-widget/src/lib/widget/mixins/initMixin/initComponentsMixins/initPasswordUserAuthMethodMixin.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const initPasswordUserAuthMethodMixin = createSingletonMixin(
5959
});
6060
}
6161

62-
#initPhoneUserAttr() {
62+
initPasswordAuthMethod() {
6363
this.passwordUserAuthMethod = new UserAuthMethodDriver(
6464
() =>
6565
this.shadowRoot?.querySelector(
@@ -76,7 +76,7 @@ export const initPasswordUserAuthMethodMixin = createSingletonMixin(
7676
async onWidgetRootReady() {
7777
await super.onWidgetRootReady?.();
7878

79-
this.#initPhoneUserAttr();
79+
this.initPasswordAuthMethod();
8080
this.#initModal();
8181
}
8282
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import {
2+
FlowDriver,
3+
ModalDriver,
4+
UserAuthMethodDriver,
5+
} from '@descope/sdk-component-drivers';
6+
import {
7+
compose,
8+
createSingletonMixin,
9+
withMemCache,
10+
} from '@descope/sdk-helpers';
11+
import {
12+
cookieConfigMixin,
13+
loggerMixin,
14+
modalMixin,
15+
} from '@descope/sdk-mixins';
16+
import { stateManagementMixin } from '../../stateManagementMixin';
17+
import { initWidgetRootMixin } from './initWidgetRootMixin';
18+
import { getHasTotp } from '../../../state/selectors';
19+
import { createFlowTemplate } from '../../helpers';
20+
import { flowSyncThemeMixin } from '../../flowSyncThemeMixin';
21+
22+
export const initTotpUserAuthMethodMixin = createSingletonMixin(
23+
<T extends CustomElementConstructor>(superclass: T) =>
24+
class TotpUserAuthMethodMixinClass extends compose(
25+
flowSyncThemeMixin,
26+
stateManagementMixin,
27+
loggerMixin,
28+
initWidgetRootMixin,
29+
cookieConfigMixin,
30+
modalMixin,
31+
)(superclass) {
32+
totpUserAuthMethod: UserAuthMethodDriver;
33+
34+
#modal: ModalDriver;
35+
36+
#flow: FlowDriver;
37+
38+
#initModal() {
39+
if (!this.totpUserAuthMethod.flowId) return;
40+
41+
this.#modal = this.createModal({ 'data-id': 'totp' });
42+
this.#flow = new FlowDriver(
43+
() => this.#modal.ele?.querySelector('descope-wc'),
44+
{ logger: this.logger },
45+
);
46+
this.#modal.afterClose = this.#initModalContent.bind(this);
47+
this.#initModalContent();
48+
this.syncFlowTheme(this.#flow);
49+
}
50+
51+
#initModalContent() {
52+
this.#modal.setContent(
53+
createFlowTemplate({
54+
projectId: this.projectId,
55+
flowId: this.totpUserAuthMethod.flowId,
56+
baseUrl: this.baseUrl,
57+
baseStaticUrl: this.baseStaticUrl,
58+
baseCdnUrl: this.baseCdnUrl,
59+
refreshCookieName: this.refreshCookieName,
60+
}),
61+
);
62+
this.#flow.onSuccess(() => {
63+
this.#modal.close();
64+
this.actions.getMe();
65+
});
66+
}
67+
68+
#initTotpAuthMethod() {
69+
this.totpUserAuthMethod = new UserAuthMethodDriver(
70+
() =>
71+
this.shadowRoot?.querySelector(
72+
'descope-user-auth-method[data-id="totp"]',
73+
),
74+
{ logger: this.logger },
75+
);
76+
77+
this.totpUserAuthMethod.onButtonClick(() => {
78+
this.#modal?.open();
79+
});
80+
}
81+
82+
#onFulfilledUpdate = withMemCache(
83+
(hasTotp: ReturnType<typeof getHasTotp>) => {
84+
this.totpUserAuthMethod.fulfilled = hasTotp;
85+
},
86+
);
87+
88+
async onWidgetRootReady() {
89+
await super.onWidgetRootReady?.();
90+
91+
this.#initTotpAuthMethod();
92+
this.#initModal();
93+
94+
this.#onFulfilledUpdate(getHasTotp(this.state));
95+
96+
this.subscribe(this.#onFulfilledUpdate.bind(this), getHasTotp);
97+
}
98+
},
99+
);

packages/widgets/user-profile-widget/src/lib/widget/mixins/initMixin/initMixin.ts

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { initPasskeyUserAuthMethodMixin } from './initComponentsMixins/initPassk
99
import { initPasswordUserAuthMethodMixin } from './initComponentsMixins/initPasswordUserAuthMethodMixin';
1010
import { initPhoneUserAttrMixin } from './initComponentsMixins/initPhoneUserAttrMixin';
1111
import { initUserCustomAttributesMixin } from './initComponentsMixins/initUserCustomAttributesMixin';
12+
import { initTotpUserAuthMethodMixin } from './initComponentsMixins/initTotpUserAuthMethodMixin';
1213

1314
export const initMixin = createSingletonMixin(
1415
<T extends CustomElementConstructor>(superclass: T) =>
@@ -24,6 +25,8 @@ export const initMixin = createSingletonMixin(
2425
initPhoneUserAttrMixin,
2526
initPasskeyUserAuthMethodMixin,
2627
initPasswordUserAuthMethodMixin,
28+
initPasswordUserAuthMethodMixin,
29+
initTotpUserAuthMethodMixin,
2730
initLogoutMixin,
2831
)(superclass) {
2932
async init() {

packages/widgets/user-profile-widget/src/lib/widget/state/selectors.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const getIsPhoneVerified = createSelector(
1717
);
1818
export const getHasPasskey = createSelector(getMe, (me) => me.webauthn);
1919
export const getHasPassword = createSelector(getMe, (me) => me.password);
20+
export const getHasTotp = createSelector(getMe, (me) => me.TOTP);
2021

2122
export const getUserCustomAttrs = createSelector(
2223
getMe,

packages/widgets/user-profile-widget/test/mocks/rootMock.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,20 @@ export default `
158158
<path
159159
d="M2.00065 8.83398C2.00065 7.45898 2.55898 6.20898 3.46732 5.30065L2.28398 4.11732C1.08398 5.32565 0.333984 6.99232 0.333984 8.83398C0.333984 12.234 2.87565 15.034 6.16732 15.4423V13.759C3.80898 13.359 2.00065 11.309 2.00065 8.83398ZM13.6673 8.83398C13.6673 5.15065 10.684 2.16732 7.00065 2.16732C6.95065 2.16732 6.90065 2.17565 6.85065 2.17565L7.75898 1.26732L6.58398 0.0839844L3.66732 3.00065L6.58398 5.91732L7.75898 4.74232L6.85898 3.84232C6.90898 3.84232 6.95898 3.83398 7.00065 3.83398C9.75898 3.83398 12.0007 6.07565 12.0007 8.83398C12.0007 11.309 10.1923 13.359 7.83398 13.759V15.4423C11.1257 15.034 13.6673 12.234 13.6673 8.83398Z"
160160
fill="currentColor"
161-
></path></svg></descope-user-auth-method></descope-container
161+
></path></svg></descope-user-auth-method>
162+
<descope-user-auth-method button-label="Edit TOTP" data-id="totp" flow-id="user-profile-reset-totp" full-width="true" id="cVd-kJLggr" label="Authenticator App" class="descope-user-auth-method">
163+
<descope-icon src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0LjI0IDUuNzQ5OTlDMTMuMDcgNC41Nzk5OSAxMS41NCAzLjk4OTk5IDEwIDMuOTg5OTlWOS45ODk5OUw1Ljc2IDE0LjIzQzguMSAxNi41NyAxMS45IDE2LjU3IDE0LjI1IDE0LjIzQzE2LjU5IDExLjg5IDE2LjU5IDguMDg5OTkgMTQuMjQgNS43NDk5OVpNMTAgLTAuMDEwMDA5OEM0LjQ4IC0wLjAxMDAwOTggMCA0LjQ2OTk5IDAgOS45ODk5OUMwIDE1LjUxIDQuNDggMTkuOTkgMTAgMTkuOTlDMTUuNTIgMTkuOTkgMjAgMTUuNTEgMjAgOS45ODk5OUMyMCA0LjQ2OTk5IDE1LjUyIC0wLjAxMDAwOTggMTAgLTAuMDEwMDA5OFpNMTAgMTcuOTlDNS41OCAxNy45OSAyIDE0LjQxIDIgOS45ODk5OUMyIDUuNTY5OTkgNS41OCAxLjk4OTk5IDEwIDEuOTg5OTlDMTQuNDIgMS45ODk5OSAxOCA1LjU2OTk5IDE4IDkuOTg5OTlDMTggMTQuNDEgMTQuNDIgMTcuOTkgMTAgMTcuOTlaIiBmaWxsPSIjNjM2Qzc0Ii8+Cjwvc3ZnPgo=" st-fill="currentColor" slot="method-icon" class="descope-icon">
164+
<svg xmlns="http://www.w3.org/2000/svg" fill="var(--descope-icon-fill, none)" viewBox="0 0 20 20" height="20" width="20" style="max-width: 100%; max-height: 100%;">
165+
<path fill="var(--descope-icon-fill, #636C74)" d="M14.24 5.74999C13.07 4.57999 11.54 3.98999 10 3.98999V9.98999L5.76 14.23C8.1 16.57 11.9 16.57 14.25 14.23C16.59 11.89 16.59 8.08999 14.24 5.74999ZM10 -0.0100098C4.48 -0.0100098 0 4.46999 0 9.98999C0 15.51 4.48 19.99 10 19.99C15.52 19.99 20 15.51 20 9.98999C20 4.46999 15.52 -0.0100098 10 -0.0100098ZM10 17.99C5.58 17.99 2 14.41 2 9.98999C2 5.56999 5.58 1.98999 10 1.98999C14.42 1.98999 18 5.56999 18 9.98999C18 14.41 14.42 17.99 10 17.99Z"></path>
166+
</svg>
167+
</descope-icon>
168+
<descope-icon src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCAxNSAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwLjAwMDIgMC45OTIwMjdDMTAuMDAwMiAxLjAxNjAzIDEwLjAwMDIgMS4wMTYwMyAxMC4wMDAyIDEuMDE2MDNMOC4yMjQxOSAzLjAwODAzSDIuOTkyMTlDMi40NjQxOSAzLjAwODAzIDIuMDA4MTkgMy40NDAwMyAyLjAwODE5IDMuOTkyMDNWMTIuMDA4QzIuMDA4MTkgMTIuNTM2IDIuNDQwMTkgMTIuOTkyIDIuOTkyMTkgMTIuOTkySDUuNTM2MTlDNS44NDgxOSAxMy4wNCA2LjE2MDE5IDEzLjA0IDYuNDcyMTkgMTIuOTkySDExLjAwODJDMTEuNTM2MiAxMi45OTIgMTEuOTkyMiAxMi41NiAxMS45OTIyIDEyLjAwOFY3Ljc4NDAzTDEzLjkzNjIgNS42MjQwM0wxNC4wMDgyIDUuNjcyMDNWMTEuOTg0QzE0LjAwODIgMTMuNjY0IDEyLjY2NDIgMTUuMDA4IDExLjAwODIgMTUuMDA4SDMuMDE2MTlDMS4zMzYxOSAxNS4wMDggLTAuMDA3ODEyNSAxMy42NjQgLTAuMDA3ODEyNSAxMS45ODRWMy45OTIwM0MtMC4wMDc4MTI1IDIuMzM2MDMgMS4zMzYxOSAwLjk5MjAyNyAzLjAxNjE5IDAuOTkyMDI3SDEwLjAwMDJaTTExLjI3MjIgMi42MjQwM0wxMi42MTYyIDQuMTEyMDNMNy43MjAxOSA5LjY4MDA0QzcuNTA0MTkgOS45MjAwNCA2LjgzMjE5IDEwLjIzMiA1LjY4MDE5IDEwLjYxNkM1LjY1NjE5IDEwLjY0IDUuNjA4MTkgMTAuNjQgNS41NjAxOSAxMC42MTZDNS40NjQxOSAxMC41OTIgNS4zOTIxOSAxMC40NzIgNS40NDAxOSAxMC4zNzZDNS43NTIxOSA5LjI0ODAzIDYuMDQwMTkgOC41NTIwMyA2LjI1NjE5IDguMzEyMDNMMTEuMjcyMiAyLjYyNDAzWk0xMS45MjAyIDEuODU2MDNMMTMuMjg4MiAwLjMyMDAyN0MxMy42NDgyIC0wLjA4Nzk3MzYgMTQuMjcyMiAtMC4xMTE5NzQgMTQuNjgwMiAwLjI3MjAyN0MxNS4wODgyIDAuNjMyMDI3IDE1LjExMjIgMS4yODAwMyAxNC43NTIyIDEuNjg4MDNMMTMuMjY0MiAzLjM2ODAzTDExLjkyMDIgMS44NTYwM1oiIGZpbGw9IiMwMDZBRjUiLz4KPC9zdmc+Cg==" st-fill="currentColor" slot="button-icon" width="1em" height="1em" class="descope-icon">
169+
<svg xmlns="http://www.w3.org/2000/svg" fill="var(--descope-icon-fill, none)" viewBox="0 0 15 15" height="15" width="15" style="max-width: 100%; max-height: 100%;">
170+
<path fill="var(--descope-icon-fill, #006AF5)" d="M10.0002 0.992027C10.0002 1.01603 10.0002 1.01603 10.0002 1.01603L8.22419 3.00803H2.99219C2.46419 3.00803 2.00819 3.44003 2.00819 3.99203V12.008C2.00819 12.536 2.44019 12.992 2.99219 12.992H5.53619C5.84819 13.04 6.16019 13.04 6.47219 12.992H11.0082C11.5362 12.992 11.9922 12.56 11.9922 12.008V7.78403L13.9362 5.62403L14.0082 5.67203V11.984C14.0082 13.664 12.6642 15.008 11.0082 15.008H3.01619C1.33619 15.008 -0.0078125 13.664 -0.0078125 11.984V3.99203C-0.0078125 2.33603 1.33619 0.992027 3.01619 0.992027H10.0002ZM11.2722 2.62403L12.6162 4.11203L7.72019 9.68004C7.50419 9.92004 6.83219 10.232 5.68019 10.616C5.65619 10.64 5.60819 10.64 5.56019 10.616C5.46419 10.592 5.39219 10.472 5.44019 10.376C5.75219 9.24803 6.04019 8.55203 6.25619 8.31203L11.2722 2.62403ZM11.9202 1.85603L13.2882 0.320027C13.6482 -0.0879736 14.2722 -0.111974 14.6802 0.272027C15.0882 0.632027 15.1122 1.28003 14.7522 1.68803L13.2642 3.36803L11.9202 1.85603Z"></path>
171+
</svg>
172+
</descope-icon>
173+
</descope-user-auth-method>
174+
</descope-container
162175
><descope-divider
163176
id="divider2"
164177
italic="false"

0 commit comments

Comments
 (0)