0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(elements): add logto-user-phone element (#6732)

This commit is contained in:
Xiao Yijun 2024-10-25 15:22:31 +08:00 committed by GitHub
parent a72e016b6d
commit 3d25448500
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 162 additions and 10 deletions

View file

@ -49,6 +49,7 @@
"@lit/react": "^1.0.5",
"@silverhand/essentials": "^2.9.1",
"ky": "^1.2.3",
"libphonenumber-js": "^1.11.12",
"lit": "^3.1.4"
},
"devDependencies": {

View file

@ -0,0 +1,65 @@
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 { LogtoUserPhone } from './logto-user-phone.js';
suite('logto-user-phone', () => {
test('is defined', () => {
const element = document.createElement(LogtoUserPhone.tagName);
assert.instanceOf(element, LogtoUserPhone);
});
test('should render error message when account context is not available', async () => {
const element = await fixture<LogtoUserPhone>(html`<logto-user-phone></logto-user-phone>`);
await element.updateComplete;
assert.equal(element.shadowRoot?.textContent, 'Unable to retrieve account context.');
});
test('should render phone number if the user has permission to view phone information', async () => {
const mockAccountApi = createMockAccountApi({
fetchUserProfile: async () => ({
primaryPhone: '12025550179',
}),
});
const provider = await fixture<LogtoAccountProvider>(
html`<logto-account-provider .accountApi=${mockAccountApi}>
<logto-user-phone></logto-user-phone>
</logto-account-provider>`
);
await provider.updateComplete;
const logtoUserPhone = provider.querySelector<LogtoUserPhone>(LogtoUserPhone.tagName);
await waitUntil(
() =>
logtoUserPhone?.shadowRoot?.querySelector('div[slot="content"]')?.textContent ===
'+1 202 555 0179',
'Unable to get phone number from account context'
);
});
test('should render nothing if the user lacks permission to view phone information', async () => {
const mockAccountApi = createMockAccountApi({
fetchUserProfile: async () => ({
primaryPhone: undefined,
}),
});
const provider = await fixture<LogtoAccountProvider>(
html`<logto-account-provider .accountApi=${mockAccountApi}>
<logto-user-phone></logto-user-phone>
</logto-account-provider>`
);
await provider.updateComplete;
const logtoUserPhone = provider.querySelector<LogtoUserPhone>(LogtoUserPhone.tagName);
await logtoUserPhone?.updateComplete;
assert.equal(logtoUserPhone?.shadowRoot?.children.length, 0);
});
});

View file

@ -0,0 +1,41 @@
import { html, type TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';
import { when } from 'lit/directives/when.js';
import phoneIcon from '../icons/phone.svg';
import { formatToInternationalPhoneNumber } from '../utils/format.js';
import { LogtoProfileItemElement } from './LogtoProfileItemElement.js';
const tagName = 'logto-user-phone';
@customElement(tagName)
export class LogtoUserPhone extends LogtoProfileItemElement {
static tagName = tagName;
protected isAccessible(): boolean {
return this.accountContext?.userProfile.primaryPhone !== undefined;
}
protected getItemLabelInfo() {
return {
icon: phoneIcon,
label: 'Phone number',
};
}
protected renderContent(): TemplateResult {
const { primaryPhone } = this.accountContext?.userProfile ?? {};
return when(
primaryPhone,
(phone) => html`<div slot="content">${formatToInternationalPhoneNumber(phone)}</div>`
);
}
}
declare global {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface HTMLElementTagNameMap {
[tagName]: LogtoUserPhone;
}
}

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5917 13.575L10.4667 13.475C10.4203 13.4435 10.3699 13.4183 10.3167 13.4L10.1667 13.3334C10.0316 13.3051 9.89155 13.3109 9.75918 13.3501C9.62682 13.3894 9.50628 13.461 9.40841 13.5584C9.33481 13.6394 9.27562 13.7324 9.23341 13.8334C9.17035 13.9852 9.15361 14.1523 9.1853 14.3136C9.21698 14.4749 9.29568 14.6233 9.41149 14.74C9.52731 14.8567 9.67505 14.9365 9.83613 14.9695C9.99721 15.0024 10.1644 14.9869 10.3167 14.925C10.4163 14.8767 10.5088 14.815 10.5917 14.7417C10.7074 14.6245 10.7857 14.4757 10.8169 14.314C10.848 14.1524 10.8306 13.9851 10.7667 13.8334C10.7252 13.737 10.6658 13.6493 10.5917 13.575ZM13.3334 1.66669H6.66675C6.00371 1.66669 5.36782 1.93008 4.89898 2.39892C4.43014 2.86776 4.16675 3.50365 4.16675 4.16669V15.8334C4.16675 16.4964 4.43014 17.1323 4.89898 17.6011C5.36782 18.07 6.00371 18.3334 6.66675 18.3334H13.3334C13.9965 18.3334 14.6323 18.07 15.1012 17.6011C15.57 17.1323 15.8334 16.4964 15.8334 15.8334V4.16669C15.8334 3.50365 15.57 2.86776 15.1012 2.39892C14.6323 1.93008 13.9965 1.66669 13.3334 1.66669ZM14.1667 15.8334C14.1667 16.0544 14.079 16.2663 13.9227 16.4226C13.7664 16.5789 13.5544 16.6667 13.3334 16.6667H6.66675C6.44573 16.6667 6.23377 16.5789 6.07749 16.4226C5.92121 16.2663 5.83341 16.0544 5.83341 15.8334V4.16669C5.83341 3.94567 5.92121 3.73371 6.07749 3.57743C6.23377 3.42115 6.44573 3.33335 6.66675 3.33335H13.3334C13.5544 3.33335 13.7664 3.42115 13.9227 3.57743C14.079 3.73371 14.1667 3.94567 14.1667 4.16669V15.8334Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -8,3 +8,4 @@ export * from './providers/logto-account-provider.js';
export * from './elements/logto-username.js';
export * from './elements/logto-user-email.js';
export * from './elements/logto-user-password.js';
export * from './elements/logto-user-phone.js';

View file

@ -1,7 +1,12 @@
import { createComponent } from '@lit/react';
import { LogtoUsername } from './elements/logto-username.js';
import { LogtoAccountProvider, LogtoUserEmail, LogtoUserPassword } from './index.js';
import {
LogtoAccountProvider,
LogtoUserEmail,
LogtoUserPassword,
LogtoUserPhone,
} from './index.js';
export * from './api/index.js';
@ -27,5 +32,10 @@ export const createReactComponents = (react: Parameters<typeof createComponent>[
elementClass: LogtoUserPassword,
react,
}),
LogtoUserPhone: createComponent({
tagName: LogtoUserPhone.tagName,
elementClass: LogtoUserPhone,
react,
}),
};
};

View file

@ -0,0 +1,23 @@
import { parsePhoneNumberWithError } from 'libphonenumber-js';
/**
* Parse phone number to readable international format.
* E.g. 16502530000 -> +1 650 253 0000
*
* Note:
* This function is identical to the one in `shared/src/utils/phone.ts`, but is duplicated here
* because the `shared` package's universal module includes Node.js-specific methods, making it
* unusable in the `elements` project. An issue has been created to address this in the `shared`
* module in the future.
*
* TODO: @xiaoyijun LOG-10299
*/
export const formatToInternationalPhoneNumber = (phone: string) => {
try {
const phoneNumber = phone.startsWith('+') ? phone : `+${phone}`;
return parsePhoneNumberWithError(phoneNumber).formatInternational();
} catch {
// If parsing fails, return the original phone number
return phone;
}
};

View file

@ -3395,6 +3395,9 @@ importers:
ky:
specifier: ^1.2.3
version: 1.2.3
libphonenumber-js:
specifier: ^1.11.12
version: 1.11.12
lit:
specifier: ^3.1.4
version: 3.1.4
@ -10489,6 +10492,9 @@ packages:
libphonenumber-js@1.10.51:
resolution: {integrity: sha512-vY2I+rQwrDQzoPds0JeTEpeWzbUJgqoV0O4v31PauHBb/e+1KCXKylHcDnBMgJZ9fH9mErsEbROJY3Z3JtqEmg==}
libphonenumber-js@1.11.12:
resolution: {integrity: sha512-QkJn9/D7zZ1ucvT++TQSvZuSA2xAWeUytU+DiEQwbPKLyrDpvbul2AFs1CGbRAPpSCCk47aRAb5DX5mmcayp4g==}
lighthouse-logger@1.4.2:
resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==}
@ -16362,10 +16368,10 @@ snapshots:
eslint-config-prettier: 9.1.0(eslint@8.57.0)
eslint-config-xo: 0.44.0(eslint@8.57.0)
eslint-config-xo-typescript: 4.0.0(@typescript-eslint/eslint-plugin@7.7.0(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3))(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3)
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0)
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-consistent-default-export-name: 0.0.15
eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-n: 17.2.1(eslint@8.57.0)
eslint-plugin-no-use-extend-native: 0.5.0
eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.0.0)
@ -19536,13 +19542,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0):
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0):
dependencies:
debug: 4.3.5
enhanced-resolve: 5.16.0
eslint: 8.57.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.3
is-core-module: 2.13.1
@ -19553,14 +19559,14 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 7.7.0(eslint@8.57.0)(typescript@5.5.3)
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0)
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0)
transitivePeerDependencies:
- supports-color
@ -19582,7 +19588,7 @@ snapshots:
eslint: 8.57.0
ignore: 5.3.1
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
dependencies:
array-includes: 3.1.8
array.prototype.findlastindex: 1.2.5
@ -19592,7 +19598,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.13.1
is-glob: 4.0.3
@ -21889,6 +21895,8 @@ snapshots:
libphonenumber-js@1.10.51: {}
libphonenumber-js@1.11.12: {}
lighthouse-logger@1.4.2:
dependencies:
debug: 2.6.9