feat(console): phone number validation with flags (#5139)
Formats the phonenumber according to the preselected country
31
console/package-lock.json
generated
@@ -32,10 +32,12 @@
|
||||
"codemirror": "^5.65.8",
|
||||
"cors": "^2.8.5",
|
||||
"file-saver": "^2.0.5",
|
||||
"flag-icons": "^6.6.6",
|
||||
"google-proto-files": "^3.0.2",
|
||||
"google-protobuf": "^3.21.2",
|
||||
"grpc-web": "^1.4.1",
|
||||
"libphonenumber-js": "^1.10.15",
|
||||
"i18n-iso-countries": "^7.5.0",
|
||||
"libphonenumber-js": "^1.10.19",
|
||||
"material-design-icons-iconfont": "^6.1.1",
|
||||
"moment": "^2.29.4",
|
||||
"ngx-color": "^8.0.3",
|
||||
@@ -6728,6 +6730,11 @@
|
||||
"integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/diacritics": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
|
||||
"integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA=="
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
@@ -7929,6 +7936,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/flag-icons": {
|
||||
"version": "6.6.6",
|
||||
"resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-6.6.6.tgz",
|
||||
"integrity": "sha512-4lHDKxldnQ7q617pf9Dx9nAetT+9zcMpUexbRrc9kjLw9KJgZ83zA5Dky3Vv7ZDzUjAiZ46x/cy5P0HnEnqA2A=="
|
||||
},
|
||||
"node_modules/flat-cache": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
|
||||
@@ -8688,6 +8700,17 @@
|
||||
"ms": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18n-iso-countries": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.5.0.tgz",
|
||||
"integrity": "sha512-PtfKJNWLVhhU0KBX/8asmywjAcuyQk07mmmMwxFJcddTNBJJ1yvpY2qxVmyxbtVF+9+6eg9phgpv83XPUKU5CA==",
|
||||
"dependencies": {
|
||||
"diacritics": "1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
@@ -10194,9 +10217,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/libphonenumber-js": {
|
||||
"version": "1.10.15",
|
||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.15.tgz",
|
||||
"integrity": "sha512-sLeVLmWX17VCKKulc+aDIRHS95TxoTsKMRJi5s5gJdwlqNzMWcBCtSHHruVyXjqfi67daXM2SnLf2juSrdx5Sg=="
|
||||
"version": "1.10.19",
|
||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.19.tgz",
|
||||
"integrity": "sha512-MDZ1zLIkfSDZV5xBta3nuvbEOlsnKCPe4z5r3hyup/AXveevkl9A1eSWmLhd2FX4k7pJDe4MrLeQsux0HI/VWg=="
|
||||
},
|
||||
"node_modules/license-webpack-plugin": {
|
||||
"version": "4.0.2",
|
||||
|
@@ -36,10 +36,12 @@
|
||||
"codemirror": "^5.65.8",
|
||||
"cors": "^2.8.5",
|
||||
"file-saver": "^2.0.5",
|
||||
"flag-icons": "^6.6.6",
|
||||
"google-proto-files": "^3.0.2",
|
||||
"google-protobuf": "^3.21.2",
|
||||
"grpc-web": "^1.4.1",
|
||||
"libphonenumber-js": "^1.10.15",
|
||||
"i18n-iso-countries": "^7.5.0",
|
||||
"libphonenumber-js": "^1.10.19",
|
||||
"material-design-icons-iconfont": "^6.1.1",
|
||||
"moment": "^2.29.4",
|
||||
"ngx-color": "^8.0.3",
|
||||
|
@@ -18,6 +18,7 @@ import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
|
||||
import { from, Observable } from 'rxjs';
|
||||
import * as i18nIsoCountries from 'i18n-iso-countries';
|
||||
import { AuthGuard } from 'src/app/guards/auth.guard';
|
||||
import { RoleGuard } from 'src/app/guards/role.guard';
|
||||
import { UserGuard } from 'src/app/guards/user.guard';
|
||||
@@ -58,10 +59,15 @@ import { ThemeService } from './services/theme.service';
|
||||
import { ToastService } from './services/toast.service';
|
||||
|
||||
registerLocaleData(localeDe);
|
||||
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/de.json'));
|
||||
registerLocaleData(localeZh);
|
||||
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/zh.json'));
|
||||
registerLocaleData(localeFr);
|
||||
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/fr.json'));
|
||||
registerLocaleData(localeIt);
|
||||
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/it.json'));
|
||||
registerLocaleData(localeEn);
|
||||
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/en.json'));
|
||||
|
||||
export class WebpackTranslateLoader implements TranslateLoader {
|
||||
getTranslation(lang: string): Observable<any> {
|
||||
|
@@ -139,13 +139,26 @@
|
||||
|
||||
<p class="user-create-section">{{ 'USER.CREATE.ADDRESSANDPHONESECTION' | translate }}</p>
|
||||
|
||||
<cnsl-form-field>
|
||||
<cnsl-label>{{ 'USER.PROFILE.PHONE' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="phone" />
|
||||
<span cnslError *ngIf="phone?.invalid && phone?.errors?.required">
|
||||
{{ 'USER.VALIDATION.REQUIRED' | translate }}
|
||||
</span>
|
||||
</cnsl-form-field>
|
||||
<div class="phone-grid">
|
||||
<cnsl-form-field>
|
||||
<cnsl-label>{{ 'USER.PROFILE.COUNTRY' | translate }}</cnsl-label>
|
||||
<mat-select [(value)]="selected" (selectionChange)="setCountryCallingCode()" data-cy="country-calling-code">
|
||||
<mat-select-trigger> <span class="fi fi-{{ selected?.countryCode | lowercase }}"></span></mat-select-trigger>
|
||||
<mat-option *ngFor="let country of countryPhoneCodes" [value]="country">
|
||||
<span class="fi fi-{{ country.countryCode | lowercase }}"></span>
|
||||
<span class="phone-country-name">{{ country.countryName }}</span>
|
||||
<span class="phone-country-code">+{{ country.countryCallingCode }}</span>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field>
|
||||
<cnsl-label>{{ 'USER.PROFILE.PHONE' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="phone" matTooltip="{{ 'USER.PROFILE.PHONE_HINT' | translate }}" />
|
||||
<span cnslError *ngIf="phone?.invalid && phone?.errors?.required">
|
||||
{{ 'USER.VALIDATION.REQUIRED' | translate }}
|
||||
</span>
|
||||
</cnsl-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-create-btn-container">
|
||||
<button
|
||||
|
@@ -53,3 +53,22 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.phone-grid {
|
||||
max-width: 22rem;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
column-gap: 1rem;
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.phone-country-name {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
.phone-country-code {
|
||||
color: darkgrey;
|
||||
}
|
||||
|
@@ -1,10 +1,9 @@
|
||||
import { Location } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import parsePhoneNumber from 'libphonenumber-js';
|
||||
import { parsePhoneNumber, CountryCode } from 'libphonenumber-js';
|
||||
import { Subject } from 'rxjs';
|
||||
import { debounceTime, takeUntil } from 'rxjs/operators';
|
||||
import { AddHumanUserRequest } from 'src/app/proto/generated/zitadel/management_pb';
|
||||
import { Domain } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
||||
@@ -14,6 +13,8 @@ import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from '../../validators';
|
||||
import { CountryCallingCodesService, CountryPhoneCode } from 'src/app/services/country-calling-codes.service';
|
||||
import { formatPhone } from 'src/app/utils/formatPhone';
|
||||
|
||||
function passwordConfirmValidator(c: AbstractControl): any {
|
||||
if (!c.parent || !c) {
|
||||
@@ -40,10 +41,12 @@ function passwordConfirmValidator(c: AbstractControl): any {
|
||||
templateUrl: './user-create.component.html',
|
||||
styleUrls: ['./user-create.component.scss'],
|
||||
})
|
||||
export class UserCreateComponent implements OnDestroy {
|
||||
export class UserCreateComponent implements OnInit, OnDestroy {
|
||||
public user: AddHumanUserRequest.AsObject = new AddHumanUserRequest().toObject();
|
||||
public genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED];
|
||||
public languages: string[] = ['de', 'en', 'it', 'fr'];
|
||||
public selected: CountryPhoneCode | undefined;
|
||||
public countryPhoneCodes: CountryPhoneCode[] = [];
|
||||
public userForm!: UntypedFormGroup;
|
||||
public pwdForm!: UntypedFormGroup;
|
||||
private destroyed$: Subject<void> = new Subject();
|
||||
@@ -63,6 +66,7 @@ export class UserCreateComponent implements OnDestroy {
|
||||
private mgmtService: ManagementService,
|
||||
private changeDetRef: ChangeDetectorRef,
|
||||
private _location: Location,
|
||||
private countryCallingCodesService: CountryCallingCodesService,
|
||||
breadcrumbService: BreadcrumbService,
|
||||
) {
|
||||
breadcrumbService.setBreadcrumb([
|
||||
@@ -151,17 +155,6 @@ export class UserCreateComponent implements OnDestroy {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.userForm.controls['phone'].valueChanges.pipe(takeUntil(this.destroyed$), debounceTime(300)).subscribe((value) => {
|
||||
const phoneNumber = parsePhoneNumber(value ?? '', 'CH');
|
||||
if (phoneNumber) {
|
||||
const formmatted = phoneNumber.formatInternational();
|
||||
const country = phoneNumber.country;
|
||||
if (this.phone && country && this.phone.value && this.phone.value !== formmatted) {
|
||||
this.phone.setValue(formmatted);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public createUser(): void {
|
||||
@@ -190,7 +183,10 @@ export class UserCreateComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
if (this.phone && this.phone.value) {
|
||||
humanReq.setPhone(new AddHumanUserRequest.Phone().setPhone(this.phone.value));
|
||||
// Try to parse number and format it according to country
|
||||
const phoneNumber = formatPhone(this.phone.value);
|
||||
this.selected = this.countryPhoneCodes.find((code) => code.countryCode === phoneNumber.country);
|
||||
humanReq.setPhone(new AddHumanUserRequest.Phone().setPhone(phoneNumber.phone));
|
||||
}
|
||||
|
||||
this.mgmtService
|
||||
@@ -206,6 +202,18 @@ export class UserCreateComponent implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
public setCountryCallingCode(): void {
|
||||
let value = (this.phone?.value as string) || '';
|
||||
this.phone?.setValue('+' + this.selected?.countryCallingCode + ' ' + value.replace(/\+[0-9]*\s/, ''));
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Set default selected country for phone numbers
|
||||
const defaultCountryCallingCode = 'CH';
|
||||
this.countryPhoneCodes = this.countryCallingCodesService.getCountryCallingCodes();
|
||||
this.selected = this.countryPhoneCodes.find((code) => code.countryCode === defaultCountryCallingCode);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
|
@@ -18,9 +18,11 @@ import { PasswordComplexityViewModule } from 'src/app/modules/password-complexit
|
||||
|
||||
import { UserCreateRoutingModule } from './user-create-routing.module';
|
||||
import { UserCreateComponent } from './user-create.component';
|
||||
import { CountryCallingCodesService } from 'src/app/services/country-calling-codes.service';
|
||||
|
||||
@NgModule({
|
||||
declarations: [UserCreateComponent],
|
||||
providers: [CountryCallingCodesService],
|
||||
imports: [
|
||||
UserCreateRoutingModule,
|
||||
CommonModule,
|
||||
|
@@ -21,6 +21,7 @@ import { Buffer } from 'buffer';
|
||||
import { EditDialogComponent, EditDialogType } from './edit-dialog/edit-dialog.component';
|
||||
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
|
||||
import { LoginPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
||||
import { formatPhone } from 'src/app/utils/formatPhone';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-auth-user-detail',
|
||||
@@ -271,6 +272,9 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
|
||||
public savePhone(phone: string): void {
|
||||
if (this.user?.human) {
|
||||
// Format phone before save (add +)
|
||||
phone = formatPhone(phone).phone;
|
||||
|
||||
this.userService
|
||||
.setMyPhone(phone)
|
||||
.then(() => {
|
||||
|
@@ -3,12 +3,31 @@
|
||||
</h1>
|
||||
<p class="desc cnsl-secondary-text">{{ data.descriptionKey | translate }}</p>
|
||||
<div mat-dialog-content>
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label
|
||||
>{{ data.labelKey | translate }} <span *ngIf="isPhone && phoneCountry">({{ phoneCountry }})</span>
|
||||
</cnsl-label>
|
||||
<input [formControl]="valueControl" cnslInput (keydown.enter)="valueControl.valid ? closeDialogWithValue() : null" />
|
||||
</cnsl-form-field>
|
||||
<div class="phone-grid">
|
||||
<cnsl-form-field *ngIf="isPhone">
|
||||
<cnsl-label>{{ 'USER.PROFILE.COUNTRY' | translate }}</cnsl-label>
|
||||
<mat-select [(value)]="selected" (selectionChange)="setCountryCallingCode()">
|
||||
<mat-select-trigger> <span class="fi fi-{{ selected?.countryCode | lowercase }}"></span></mat-select-trigger>
|
||||
<mat-option *ngFor="let country of countryPhoneCodes" [value]="country">
|
||||
<span class="fi fi-{{ country.countryCode | lowercase }}"></span>
|
||||
<span class="phone-country-name">{{ country.countryName }}</span>
|
||||
<span class="phone-country-code">+{{ country.countryCallingCode }}</span>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field>
|
||||
<cnsl-label>{{ data.labelKey | translate }}</cnsl-label>
|
||||
<input
|
||||
cnslInput
|
||||
[formControl]="valueControl"
|
||||
matTooltip="{{ 'USER.PROFILE.PHONE_HINT' | translate }}"
|
||||
[matTooltipDisabled]="!isPhone"
|
||||
/>
|
||||
<span cnslError *ngIf="valueControl?.invalid && valueControl?.errors?.required">
|
||||
{{ 'USER.VALIDATION.REQUIRED' | translate }}
|
||||
</span>
|
||||
</cnsl-form-field>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="data.type === EditDialogType.EMAIL && data.isVerifiedTextKey">
|
||||
<mat-checkbox class="verified-checkbox" [(ngModel)]="isVerified">
|
||||
|
@@ -22,3 +22,22 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.phone-grid {
|
||||
max-width: 22rem;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
column-gap: 1rem;
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.phone-country-name {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
.phone-country-code {
|
||||
color: darkgrey;
|
||||
}
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { UntypedFormControl, Validators } from '@angular/forms';
|
||||
import {
|
||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||
MatLegacyDialogRef as MatDialogRef,
|
||||
} from '@angular/material/legacy-dialog';
|
||||
import { parsePhoneNumber } from 'libphonenumber-js';
|
||||
import { CountryCode, parsePhoneNumber } from 'libphonenumber-js';
|
||||
import { CountryCallingCodesService, CountryPhoneCode } from 'src/app/services/country-calling-codes.service';
|
||||
import { formatPhone } from 'src/app/utils/formatPhone';
|
||||
|
||||
export enum EditDialogType {
|
||||
PHONE = 1,
|
||||
@@ -16,39 +18,37 @@ export enum EditDialogType {
|
||||
templateUrl: './edit-dialog.component.html',
|
||||
styleUrls: ['./edit-dialog.component.scss'],
|
||||
})
|
||||
export class EditDialogComponent {
|
||||
export class EditDialogComponent implements OnInit {
|
||||
public isPhone: boolean = false;
|
||||
public isVerified: boolean = false;
|
||||
public phoneCountry: string = 'CH';
|
||||
public valueControl: UntypedFormControl = new UntypedFormControl(['', [Validators.required]]);
|
||||
public EditDialogType: any = EditDialogType;
|
||||
constructor(public dialogRef: MatDialogRef<EditDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: any) {
|
||||
public selected: CountryPhoneCode | undefined;
|
||||
public countryPhoneCodes: CountryPhoneCode[] = [];
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<EditDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private countryCallingCodesService: CountryCallingCodesService,
|
||||
) {
|
||||
this.valueControl.setValue(data.value);
|
||||
if (data.type === EditDialogType.PHONE) {
|
||||
this.isPhone = true;
|
||||
}
|
||||
|
||||
this.valueControl.valueChanges.subscribe((value) => {
|
||||
if (value && value.length > 1) {
|
||||
this.changeValue(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private changeValue(changedValue: string): void {
|
||||
if (this.isPhone && changedValue) {
|
||||
try {
|
||||
const phoneNumber = parsePhoneNumber(changedValue ?? '', 'CH');
|
||||
if (phoneNumber) {
|
||||
const formmatted = phoneNumber.formatInternational();
|
||||
this.phoneCountry = phoneNumber.country || '';
|
||||
if (formmatted !== this.valueControl.value) {
|
||||
this.valueControl.setValue(formmatted);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
public setCountryCallingCode(): void {
|
||||
let value = (this.valueControl?.value as string) || '';
|
||||
this.valueControl?.setValue('+' + this.selected?.countryCallingCode + ' ' + value.replace(/\+[0-9]*\s/, ''));
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.isPhone) {
|
||||
// Get country phone codes and set selected flag to guessed country or default country
|
||||
this.countryPhoneCodes = this.countryCallingCodesService.getCountryCallingCodes();
|
||||
const phoneNumber = formatPhone(this.valueControl?.value);
|
||||
this.selected = this.countryPhoneCodes.find((code) => code.countryCode === phoneNumber.country);
|
||||
this.valueControl.setValue(phoneNumber.phone);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -36,7 +36,7 @@
|
||||
<div class="contact-method-row">
|
||||
<div class="left">
|
||||
<span class="label cnsl-secondary-text">{{ 'USER.PHONE' | translate }}</span>
|
||||
<span class="name">{{ human.phone?.phone ? human.phone?.phone : ('USER.PHONEEMPTY' | translate) }}</span>
|
||||
<cnsl-phone-detail [phone]="human.phone?.phone"></cnsl-phone-detail>
|
||||
<span *ngIf="human.phone?.isPhoneVerified" class="contact-state verified">{{ 'USER.PHONEVERIFIED' | translate }}</span>
|
||||
<div *ngIf="human.phone?.phone && !human.phone?.isPhoneVerified" class="block">
|
||||
<span class="contact-state notverified">{{ 'USER.NOTVERIFIED' | translate }}</span>
|
||||
|
@@ -0,0 +1,4 @@
|
||||
<span class="name">
|
||||
<span *ngIf="country" class="fi fi-{{ country | lowercase }} margin-right"></span>
|
||||
{{ phone ? phone : ('USER.PHONEEMPTY' | translate) }}
|
||||
</span>
|
@@ -0,0 +1,3 @@
|
||||
.margin-right {
|
||||
margin-right: 0.5em;
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { PhoneDetailComponent } from './phone-detail.component';
|
||||
|
||||
describe('PhoneDetailComponent', () => {
|
||||
let component: PhoneDetailComponent;
|
||||
let fixture: ComponentFixture<PhoneDetailComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PhoneDetailComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PhoneDetailComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,24 @@
|
||||
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { formatPhone } from 'src/app/utils/formatPhone';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-phone-detail',
|
||||
templateUrl: './phone-detail.component.html',
|
||||
styleUrls: ['./phone-detail.component.scss'],
|
||||
})
|
||||
export class PhoneDetailComponent implements OnChanges {
|
||||
@Input() phone: string | undefined;
|
||||
public country: string | undefined;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.phone.currentValue) {
|
||||
const phoneNumber = formatPhone(changes.phone.currentValue);
|
||||
if (this.phone !== phoneNumber.phone) {
|
||||
this.phone = phoneNumber.phone;
|
||||
this.country = phoneNumber.country;
|
||||
}
|
||||
} else {
|
||||
this.country = undefined;
|
||||
}
|
||||
}
|
||||
}
|
@@ -58,6 +58,9 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
|
||||
import { MachineSecretDialogComponent } from './user-detail/machine-secret-dialog/machine-secret-dialog.component';
|
||||
import { MetadataModule } from 'src/app/modules/metadata/metadata.module';
|
||||
import { QRCodeModule } from 'angularx-qrcode';
|
||||
import { PhoneDetailComponent } from './phone-detail/phone-detail.component';
|
||||
import { CountryCallingCodesService } from 'src/app/services/country-calling-codes.service';
|
||||
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -76,8 +79,10 @@ import { QRCodeModule } from 'angularx-qrcode';
|
||||
DialogU2FComponent,
|
||||
DialogPasswordlessComponent,
|
||||
AuthFactorDialogComponent,
|
||||
PhoneDetailComponent,
|
||||
MachineSecretDialogComponent,
|
||||
],
|
||||
providers: [CountryCallingCodesService],
|
||||
imports: [
|
||||
ChangesModule,
|
||||
CommonModule,
|
||||
@@ -122,6 +127,7 @@ import { QRCodeModule } from 'angularx-qrcode';
|
||||
InputModule,
|
||||
MachineKeysModule,
|
||||
InfoSectionModule,
|
||||
MatSelectModule,
|
||||
],
|
||||
})
|
||||
export class UserDetailModule {}
|
||||
|
@@ -21,6 +21,7 @@ import { Buffer } from 'buffer';
|
||||
import { EditDialogComponent, EditDialogType } from '../auth-user-detail/edit-dialog/edit-dialog.component';
|
||||
import { ResendEmailDialogComponent } from '../auth-user-detail/resend-email-dialog/resend-email-dialog.component';
|
||||
import { LoginPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
||||
import { formatPhone } from 'src/app/utils/formatPhone';
|
||||
import { MachineSecretDialogComponent } from './machine-secret-dialog/machine-secret-dialog.component';
|
||||
|
||||
const GENERAL: SidenavSetting = { id: 'general', i18nKey: 'USER.SETTINGS.GENERAL' };
|
||||
@@ -355,6 +356,9 @@ export class UserDetailComponent implements OnInit {
|
||||
|
||||
public savePhone(phone: string): void {
|
||||
if (this.user.id && phone) {
|
||||
// Format phone before save (add +)
|
||||
phone = formatPhone(phone).phone;
|
||||
|
||||
this.mgmtUserService
|
||||
.updateHumanPhone(this.user.id, phone)
|
||||
.then(() => {
|
||||
|
30
console/src/app/services/country-calling-codes.service.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CountryCode, getCountries, getCountryCallingCode } from 'libphonenumber-js';
|
||||
import * as i18nIsoCountries from 'i18n-iso-countries';
|
||||
|
||||
export interface CountryPhoneCode {
|
||||
countryCode: string;
|
||||
countryName: string;
|
||||
countryCallingCode: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CountryCallingCodesService {
|
||||
constructor(private translateService: TranslateService) {}
|
||||
|
||||
public getCountryCallingCodes(): CountryPhoneCode[] {
|
||||
const currentLang = this.translateService.currentLang ?? 'en';
|
||||
const countryPhoneCodes = getCountries()
|
||||
.filter((code: CountryCode) => i18nIsoCountries.getName(code.toString(), currentLang))
|
||||
.map((code: CountryCode) => {
|
||||
return <CountryPhoneCode>{
|
||||
countryCode: code,
|
||||
countryName: i18nIsoCountries.getName(code.toString(), currentLang),
|
||||
countryCallingCode: getCountryCallingCode(code),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.countryName.localeCompare(b.countryName));
|
||||
return countryPhoneCodes;
|
||||
}
|
||||
}
|
19
console/src/app/utils/formatPhone.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { CountryCode, parsePhoneNumber } from 'libphonenumber-js';
|
||||
|
||||
export function formatPhone(phone: string): { phone: string; country: CountryCode } {
|
||||
const defaultCountry = 'CH';
|
||||
|
||||
if (phone) {
|
||||
try {
|
||||
const phoneNumber = parsePhoneNumber(phone, defaultCountry);
|
||||
const country = phoneNumber.country ?? defaultCountry;
|
||||
if (phoneNumber) {
|
||||
return { phone: phoneNumber.formatInternational(), country };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return { phone, country: defaultCountry };
|
||||
}
|
Before Width: | Height: | Size: 652 B |
Before Width: | Height: | Size: 170 B |
Before Width: | Height: | Size: 620 B |
Before Width: | Height: | Size: 735 B |
Before Width: | Height: | Size: 646 B |
Before Width: | Height: | Size: 688 B |
Before Width: | Height: | Size: 122 B |
Before Width: | Height: | Size: 309 B |
Before Width: | Height: | Size: 549 B |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 327 B |
Before Width: | Height: | Size: 759 B |
Before Width: | Height: | Size: 133 B |
Before Width: | Height: | Size: 630 B |
Before Width: | Height: | Size: 281 B |
Before Width: | Height: | Size: 279 B |
Before Width: | Height: | Size: 243 B |
Before Width: | Height: | Size: 461 B |
Before Width: | Height: | Size: 381 B |
Before Width: | Height: | Size: 258 B |
Before Width: | Height: | Size: 172 B |
Before Width: | Height: | Size: 283 B |
Before Width: | Height: | Size: 106 B |
Before Width: | Height: | Size: 188 B |
Before Width: | Height: | Size: 691 B |
Before Width: | Height: | Size: 169 B |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 918 B |
Before Width: | Height: | Size: 955 B |
Before Width: | Height: | Size: 547 B |
Before Width: | Height: | Size: 159 B |
Before Width: | Height: | Size: 750 B |
Before Width: | Height: | Size: 289 B |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 260 B |
Before Width: | Height: | Size: 172 B |
Before Width: | Height: | Size: 452 B |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 593 B |
Before Width: | Height: | Size: 449 B |
Before Width: | Height: | Size: 327 B |
Before Width: | Height: | Size: 296 B |
Before Width: | Height: | Size: 172 B |
Before Width: | Height: | Size: 165 B |
Before Width: | Height: | Size: 722 B |
Before Width: | Height: | Size: 285 B |
Before Width: | Height: | Size: 245 B |
Before Width: | Height: | Size: 315 B |
Before Width: | Height: | Size: 158 B |
Before Width: | Height: | Size: 109 B |
Before Width: | Height: | Size: 356 B |
Before Width: | Height: | Size: 407 B |
Before Width: | Height: | Size: 296 B |
Before Width: | Height: | Size: 720 B |
Before Width: | Height: | Size: 572 B |
Before Width: | Height: | Size: 341 B |
Before Width: | Height: | Size: 106 B |
Before Width: | Height: | Size: 531 B |
Before Width: | Height: | Size: 203 B |
Before Width: | Height: | Size: 515 B |
Before Width: | Height: | Size: 422 B |
Before Width: | Height: | Size: 405 B |
Before Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 158 B |
Before Width: | Height: | Size: 366 B |
Before Width: | Height: | Size: 374 B |
Before Width: | Height: | Size: 99 B |
Before Width: | Height: | Size: 584 B |
Before Width: | Height: | Size: 682 B |
Before Width: | Height: | Size: 596 B |
Before Width: | Height: | Size: 546 B |
Before Width: | Height: | Size: 186 B |
Before Width: | Height: | Size: 876 B |
Before Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 269 B |
Before Width: | Height: | Size: 260 B |
Before Width: | Height: | Size: 165 B |
Before Width: | Height: | Size: 109 B |
Before Width: | Height: | Size: 99 B |