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,
+ }),
};
};