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.",