From b2bc43b45577743e72f537fe6ef08c84bde2d552 Mon Sep 17 00:00:00 2001 From: lenhanphung Date: Thu, 13 Mar 2025 10:10:05 +0700 Subject: [PATCH] feat(Profile): Add phone number field to Profile View --- src/components/Profile/PhoneNumberSection.jsx | 71 +++++++++++++++++ src/components/Profile/ProfileView.jsx | 2 + src/lib/phoneHelper.js | 26 +++++++ src/lib/phoneHelper.spec.js | 78 +++++++++++++++++++ src/locales/de.json | 5 ++ src/locales/de_DE.json | 5 ++ src/locales/en.json | 7 +- src/locales/es.json | 5 ++ src/locales/fr.json | 7 +- src/locales/ja.json | 5 ++ src/locales/nl_NL.json | 5 ++ 11 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 src/components/Profile/PhoneNumberSection.jsx create mode 100644 src/lib/phoneHelper.js create mode 100644 src/lib/phoneHelper.spec.js diff --git a/src/components/Profile/PhoneNumberSection.jsx b/src/components/Profile/PhoneNumberSection.jsx new file mode 100644 index 00000000..8465dc58 --- /dev/null +++ b/src/components/Profile/PhoneNumberSection.jsx @@ -0,0 +1,71 @@ +import React, { useMemo, useState } from 'react' + +import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' +import { useQuery, useMutation } from 'cozy-client' + +import Input from 'components/Input' +import { buildSettingsInstanceQuery } from 'lib/queries' +import { validatePhoneNumber } from 'lib/phoneHelper' + +const PhoneNumberSection = () => { + const { t } = useI18n() + + const { mutate, mutationStatus } = useMutation() + const [formError, setFormError] = useState() + + const instanceQuery = buildSettingsInstanceQuery() + const { data: instance } = useQuery( + instanceQuery.definition, + instanceQuery.options + ) + + const handleBlur = (_, value) => { + if (value !== '') { + mutate({ + _rev: instance.meta.rev, + ...instance, + attributes: { + ...instance.attributes, + phone_number: value + } + }) + } + } + + const errors = useMemo(() => { + if (mutationStatus === 'failed') { + return ['ProfileView.infos.server_error'] + } + if (formError) { + return [formError] + } + return undefined + }, [mutationStatus, formError]) + + const handleChange = (_, value) => { + if (value === '') { + setFormError(undefined) + } else if (!validatePhoneNumber(value)) { + setFormError('ProfileView.phone_number.invalid') + } else { + setFormError(undefined) + } + } + + return ( + + ) +} + +export { PhoneNumberSection } diff --git a/src/components/Profile/ProfileView.jsx b/src/components/Profile/ProfileView.jsx index 9e0a82a0..f11f662e 100644 --- a/src/components/Profile/ProfileView.jsx +++ b/src/components/Profile/ProfileView.jsx @@ -19,6 +19,7 @@ import TrackingSection from 'components/Profile/TrackingSection' import PasswordSection from 'components/Profile/PasswordSection' import EmailSection from 'components/Email/EmailSection' import { PublicNameSection } from 'components/Profile/PublicNameSection' +import { PhoneNumberSection } from 'components/Profile/PhoneNumberSection' import { hasQueryBeenLoaded, useQuery } from 'cozy-client' import { buildSettingsInstanceQuery } from 'lib/queries' @@ -50,6 +51,7 @@ const ProfileView = ({ + diff --git a/src/lib/phoneHelper.js b/src/lib/phoneHelper.js new file mode 100644 index 00000000..f3bbc160 --- /dev/null +++ b/src/lib/phoneHelper.js @@ -0,0 +1,26 @@ +/** + * Regular expression for phone number validation + * Supports formats: + * - May start with a + sign (optional) + * - May have parentheses for country/area code (optional) + * - Number groups may be separated by hyphens, spaces, or dots + * - Supports complex formats like "+1 (123) 456-7890" + */ +const PHONE_REGEX = + /^[+]?[0-9]{0,3}[ ]?[(]?[0-9]{1,4}[)]?[-\s.]?[0-9]{1,3}[-\s.]?[0-9]{1,4}[-\s.]?[0-9]{1,4}$/ + +/** + * Validates a phone number + * @param {string} phoneNumber - The phone number to validate + * @returns {boolean} - true if the phone number is valid, false otherwise + */ +export const validatePhoneNumber = phoneNumber => { + if (!phoneNumber || typeof phoneNumber !== 'string') { + return false + } + return PHONE_REGEX.test(phoneNumber) +} + +export default { + validatePhoneNumber +} diff --git a/src/lib/phoneHelper.spec.js b/src/lib/phoneHelper.spec.js new file mode 100644 index 00000000..86a7e72c --- /dev/null +++ b/src/lib/phoneHelper.spec.js @@ -0,0 +1,78 @@ +import { validatePhoneNumber } from './phoneHelper' + +describe('phoneHelper', () => { + describe('validatePhoneNumber', () => { + it('should return false for null or undefined values', () => { + expect(validatePhoneNumber(null)).toBe(false) + expect(validatePhoneNumber(undefined)).toBe(false) + }) + + it('should return false for non-string values', () => { + expect(validatePhoneNumber(123456789)).toBe(false) + expect(validatePhoneNumber({})).toBe(false) + expect(validatePhoneNumber([])).toBe(false) + }) + + it('should return false for empty string', () => { + expect(validatePhoneNumber('')).toBe(false) + }) + + describe('Valid phone number formats', () => { + const validPhoneNumbers = [ + // Basic formats + '0912345678', + '123-456-7890', + '123.456.7890', + '123 456 7890', + '(123) 456-7890', + + // International formats + '+1-555-123-4567', // US + '+44 20 1234 5678', // UK + '+33123456789', // France + '+49 30 1234 5678', // Germany + '+81 3 1234 5678', // Japan + '+86 10 1234 5678', // China + '+61 2 1234 5678', // Australia + + // Mixed formats + '+1 (123) 456-7890', + '(123)456-7890' + ] + + validPhoneNumbers.forEach(phoneNumber => { + it(`should return true for valid phone number: ${phoneNumber}`, () => { + expect(validatePhoneNumber(phoneNumber)).toBe(true) + }) + }) + }) + + describe('Invalid phone number formats', () => { + const invalidPhoneNumbers = [ + // Too short + '123', + '+1', + + // Contains invalid characters + 'abc1234567', + '123-abc-7890', + '123@456@7890', + + // Incorrect format + '++1234567890', // Double plus sign + '(123))456-7890', // Unbalanced parentheses + '(123', // Incomplete parentheses + + // Too long + '123456789012345678901', + '+123456789012345678901' + ] + + invalidPhoneNumbers.forEach(phoneNumber => { + it(`should return false for invalid phone number: ${phoneNumber}`, () => { + expect(validatePhoneNumber(phoneNumber)).toBe(false) + }) + }) + }) + }) +}) diff --git a/src/locales/de.json b/src/locales/de.json index 522c8b22..53de39f9 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -146,6 +146,11 @@ "title": "Benutzername", "label": "Dein Benutzername wird angezeigt, wenn du Dateien mit Cozy-Benutzern teilst." }, + "phone_number": { + "title": "Telefonnummer", + "label": "Deine Telefonnummer wird verwendet, um dich zu benachrichtigen, wenn du dein Passwort vergisst.", + "invalid": "Die Telefonnummer fühlt sich nicht richtig an." + }, "tracking": { "title": "Hilf uns, unser Produkt zu verbessern", "label": "Autorisiere Cozy Cloud, anonyme Nutzungsdaten zu sammeln, um unser Produkt zu verbessern." diff --git a/src/locales/de_DE.json b/src/locales/de_DE.json index e71f4efc..506f6430 100644 --- a/src/locales/de_DE.json +++ b/src/locales/de_DE.json @@ -146,6 +146,11 @@ "title": "Benutzername", "label": "Dein Benutzername wird angezeigt, wenn du Dateien mit Cozy-Benutzern teilst." }, + "phone_number": { + "title": "Telefonnummer", + "label": "Deine Telefonnummer wird verwendet, um dich über wichtige Ereignisse zu informieren.", + "invalid": "Die Telefonnummer scheint nicht korrekt zu sein. Bitte überprüfe sie und versuche es erneut." + }, "tracking": { "title": "Hilf uns, unser Produkt zu verbessern", "label": "Erlaube es Cozy Cloud, anonyme Nutzungsdaten zur Verbesserung des Dienstes zu sammeln." diff --git a/src/locales/en.json b/src/locales/en.json index 20310475..6cf5904b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -180,6 +180,11 @@ "title": "Username", "label": "Your username will be displayed when you share files with Cozy users." }, + "phone_number": { + "title": "Phone number", + "label": "Your phone number will be used to send you notifications and to enable you to recover your password.", + "invalid": "The phone number is invalid" + }, "default_redirection": { "title": "Home screen", "label": "This is the first screen you will see when you open your Cozy.", @@ -579,4 +584,4 @@ "manage": "Manage my plan", "resume": "Continue" } -} +} \ No newline at end of file diff --git a/src/locales/es.json b/src/locales/es.json index ddeceded..12c2d763 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -146,6 +146,11 @@ "title": "Usuario", "label": "Su nombre de usuario será visualizado cuando usted comparta archivos con usuarios Cozy." }, + "phone_number": { + "title": "Número de teléfono", + "label": "Su número de teléfono será utilizado para enviarle notificaciones por SMS.", + "invalid": "Su número de teléfono no parece correcto." + }, "tracking": { "title": "Ayúdenos a mejorar nuestro producto", "label": "Autorizar Cozy Cloud a adquirir anónimamente sus datos de uso para mejorar nuestro producto." diff --git a/src/locales/fr.json b/src/locales/fr.json index 2a9ed74b..c76ca82d 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -180,6 +180,11 @@ "title": "Nom d'usage", "label": "Votre nom d'usage est utilisé lorsque vous partagez des fichiers avec d'autres utilisateurs de Cozy." }, + "phone_number": { + "title": "Numéro de téléphone", + "label": "Votre numéro de téléphone est utilisé pour vous envoyer des notifications par SMS.", + "invalid": "Le numéro de téléphone que vous avez entré ne semble pas correct." + }, "default_redirection": { "title": "Écran d'accueil", "label": "C’est le premier écran que vous verrez en ouvrant votre Cozy.", @@ -579,4 +584,4 @@ "manage": "Gérer mon offre", "resume": "Continuer" } -} +} \ No newline at end of file diff --git a/src/locales/ja.json b/src/locales/ja.json index 74caff00..76256c77 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -146,6 +146,11 @@ "title": "ユーザー名", "label": "Cozy ユーザーとファイルを共有するときに、ユーザー名が表示されます。" }, + "phone_number": { + "title": "電話番号", + "label": "Cozy は、アカウントのセキュリティを向上させるために、電話番号を使用することができます。", + "invalid": "電話番号が正しくないようです。" + }, "tracking": { "title": "製品の改善にご協力ください", "label": "製品を改善するために、Cozy クラウドが使用状況のデータを匿名で取得する権限を与えます." diff --git a/src/locales/nl_NL.json b/src/locales/nl_NL.json index df9f7b6e..ef28b61f 100644 --- a/src/locales/nl_NL.json +++ b/src/locales/nl_NL.json @@ -177,6 +177,11 @@ "title": "Gebruikersnaam", "label": "Je gebruikersnaam wordt getoond als je bestanden deelt met Cozy-gebruikers." }, + "phone_number": { + "title": "Telefoonnummer", + "label": "Je telefoonnummer wordt gebruikt voor tweestapsverificatie en om je te informeren over belangrijke gebeurtenissen.", + "invalid": "Dit telefoonnummer is ongeldig." + }, "default_redirection": { "title": "Voorpagina", "label": "Dit is de pagina die je ziet zodra je Cozy opent.",