diff --git a/packages/elements/index.html b/packages/elements/index.html index 026da204aca..cff35cc4e23 100644 --- a/packages/elements/index.html +++ b/packages/elements/index.html @@ -10,7 +10,7 @@ - + diff --git a/packages/elements/src/account/elements/logto-account-center.test.ts b/packages/elements/src/account/elements/logto-account-center.test.ts new file mode 100644 index 00000000000..25983ed71ef --- /dev/null +++ b/packages/elements/src/account/elements/logto-account-center.test.ts @@ -0,0 +1,95 @@ +import { assert, fixture, html, waitUntil } from '@open-wc/testing'; + +import { createMockAccountApi } from '../__mocks__/account-api.js'; +import { type LogtoAccountProvider } from '../providers/logto-account-provider.js'; + +import { LogtoAccountCenter } from './logto-account-center.js'; + +suite('logto-account-center', () => { + test('is defined', () => { + const element = document.createElement(LogtoAccountCenter.tagName); + assert.instanceOf(element, LogtoAccountCenter); + }); + + test('should render error message when account context is not available', async () => { + const element = await fixture( + html`` + ); + await element.updateComplete; + + assert.equal(element.shadowRoot?.textContent?.trim(), 'Unable to retrieve account context.'); + }); + + test('should render components correctly based on user info', async () => { + const mockAccountApi = createMockAccountApi({ + fetchUserProfile: async () => ({ + username: 'testuser', + primaryEmail: 'test@example.com', + primaryPhone: '1234567890', + hasPassword: true, + identities: { + google: { + userId: 'google-123', + details: { name: 'John Doe', email: 'john@example.com' }, + }, + facebook: { + userId: 'facebook-123', + details: { name: 'Jane Doe', email: 'jane@example.com' }, + }, + }, + }), + }); + + const provider = await fixture( + html` + + ` + ); + + await provider.updateComplete; + + const accountCenter = provider.querySelector(LogtoAccountCenter.tagName); + await accountCenter?.updateComplete; + + await waitUntil(() => { + const shadowRoot = accountCenter?.shadowRoot; + return ( + shadowRoot?.querySelector('logto-username') && + shadowRoot.querySelector('logto-user-email') && + shadowRoot.querySelector('logto-user-phone') && + shadowRoot.querySelector('logto-user-password') && + shadowRoot.querySelectorAll('logto-social-identity').length === 2 + ); + }, 'Unable to render all expected components'); + }); + + test('should only render components for existing user info', async () => { + const mockAccountApi = createMockAccountApi({ + fetchUserProfile: async () => ({ + username: 'testuser', + primaryEmail: undefined, + primaryPhone: undefined, + hasPassword: undefined, + identities: {}, + }), + }); + + const provider = await fixture( + html` + + ` + ); + + await provider.updateComplete; + + const accountCenter = provider.querySelector(LogtoAccountCenter.tagName); + await accountCenter?.updateComplete; + + const shadowRoot = accountCenter?.shadowRoot; + assert.exists(shadowRoot?.querySelector('logto-username')); + assert.notExists(shadowRoot.querySelector('logto-user-email')); + assert.notExists(shadowRoot.querySelector('logto-user-phone')); + assert.notExists(shadowRoot.querySelector('logto-user-password')); + assert.notExists(shadowRoot.querySelector('logto-social-identity')); + }); +}); diff --git a/packages/elements/src/account/elements/logto-account-center.ts b/packages/elements/src/account/elements/logto-account-center.ts new file mode 100644 index 00000000000..1636e64b8a7 --- /dev/null +++ b/packages/elements/src/account/elements/logto-account-center.ts @@ -0,0 +1,54 @@ +import { consume } from '@lit/context'; +import { css, html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +import { + logtoAccountContext, + type LogtoAccountContextType, +} from '../providers/logto-account-provider.js'; + +const tagName = 'logto-account-center'; + +@customElement(tagName) +export class LogtoAccountCenter extends LitElement { + static tagName = tagName; + + static styles = css` + :host { + display: flex; + flex-direction: column; + gap: var(--logto-account-center-item-spacing, var(--logto-spacing-md)); + } + `; + + @consume({ context: logtoAccountContext, subscribe: true }) + private readonly accountContext?: LogtoAccountContextType; + + render() { + if (!this.accountContext) { + return html`Unable to retrieve account context.`; + } + + const { + userProfile: { username, primaryEmail, primaryPhone, hasPassword, identities }, + } = this.accountContext; + + return html` + ${username !== undefined && html``} + ${primaryEmail !== undefined && html``} + ${primaryPhone !== undefined && html``} + ${hasPassword !== undefined && html``} + ${identities !== undefined && + Object.entries(identities).map( + ([target]) => html`` + )} + `; + } +} + +declare global { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface HTMLElementTagNameMap { + [tagName]: LogtoAccountCenter; + } +} diff --git a/packages/elements/src/account/index.ts b/packages/elements/src/account/index.ts index fbee0fa067a..de88aabfc3a 100644 --- a/packages/elements/src/account/index.ts +++ b/packages/elements/src/account/index.ts @@ -11,3 +11,4 @@ export * from './elements/logto-user-email.js'; export * from './elements/logto-user-password.js'; export * from './elements/logto-user-phone.js'; export * from './elements/logto-social-identity.js'; +export * from './elements/logto-account-center.js'; diff --git a/packages/elements/src/account/react.ts b/packages/elements/src/account/react.ts index 14e63ed0fc3..851daf811ab 100644 --- a/packages/elements/src/account/react.ts +++ b/packages/elements/src/account/react.ts @@ -2,6 +2,7 @@ import { createComponent } from '@lit/react'; import { LogtoUsername } from './elements/logto-username.js'; import { + LogtoAccountCenter, LogtoAccountProvider, LogtoSocialIdentity, LogtoUserEmail, @@ -43,5 +44,10 @@ export const createReactComponents = (react: Parameters[ elementClass: LogtoSocialIdentity, react, }), + LogtoAccountCenter: createComponent({ + tagName: LogtoAccountCenter.tagName, + elementClass: LogtoAccountCenter, + react, + }), }; };