Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions src/components/Profile/PhoneNumberSection.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Input
name="phone_number"
type="tel"
title={t('ProfileView.phone_number.title')}
label={t(`ProfileView.phone_number.label`)}
value={instance?.phone_number || ''}
onBlur={handleBlur}
onChange={handleChange}
submitting={mutationStatus === 'loading'}
saved={mutationStatus === 'loaded'}
errors={errors}
/>
)
}

export { PhoneNumberSection }
2 changes: 2 additions & 0 deletions src/components/Profile/ProfileView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -50,6 +51,7 @@ const ProfileView = ({
<Stack spacing="l">
<EmailSection />
<PublicNameSection />
<PhoneNumberSection />
<PasswordSection />
<TwoFA />
<LanguageSection />
Expand Down
26 changes: 26 additions & 0 deletions src/lib/phoneHelper.js
Original file line number Diff line number Diff line change
@@ -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
}
78 changes: 78 additions & 0 deletions src/lib/phoneHelper.spec.js
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
})
})
5 changes: 5 additions & 0 deletions src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
5 changes: 5 additions & 0 deletions src/locales/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
7 changes: 6 additions & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -579,4 +584,4 @@
"manage": "Manage my plan",
"resume": "Continue"
}
}
}
5 changes: 5 additions & 0 deletions src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
7 changes: 6 additions & 1 deletion src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -579,4 +584,4 @@
"manage": "Gérer mon offre",
"resume": "Continuer"
}
}
}
5 changes: 5 additions & 0 deletions src/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@
"title": "ユーザー名",
"label": "Cozy ユーザーとファイルを共有するときに、ユーザー名が表示されます。"
},
"phone_number": {
"title": "電話番号",
"label": "Cozy は、アカウントのセキュリティを向上させるために、電話番号を使用することができます。",
"invalid": "電話番号が正しくないようです。"
},
"tracking": {
"title": "製品の改善にご協力ください",
"label": "製品を改善するために、Cozy クラウドが使用状況のデータを匿名で取得する権限を与えます."
Expand Down
5 changes: 5 additions & 0 deletions src/locales/nl_NL.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Loading