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",
|
"codemirror": "^5.65.8",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"flag-icons": "^6.6.6",
|
||||||
"google-proto-files": "^3.0.2",
|
"google-proto-files": "^3.0.2",
|
||||||
"google-protobuf": "^3.21.2",
|
"google-protobuf": "^3.21.2",
|
||||||
"grpc-web": "^1.4.1",
|
"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",
|
"material-design-icons-iconfont": "^6.1.1",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"ngx-color": "^8.0.3",
|
"ngx-color": "^8.0.3",
|
||||||
@@ -6728,6 +6730,11 @@
|
|||||||
"integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==",
|
"integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/diff": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
@@ -7929,6 +7936,11 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/flat-cache": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
|
||||||
@@ -8688,6 +8700,17 @@
|
|||||||
"ms": "^2.0.0"
|
"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": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
@@ -10194,9 +10217,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/libphonenumber-js": {
|
"node_modules/libphonenumber-js": {
|
||||||
"version": "1.10.15",
|
"version": "1.10.19",
|
||||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.15.tgz",
|
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.19.tgz",
|
||||||
"integrity": "sha512-sLeVLmWX17VCKKulc+aDIRHS95TxoTsKMRJi5s5gJdwlqNzMWcBCtSHHruVyXjqfi67daXM2SnLf2juSrdx5Sg=="
|
"integrity": "sha512-MDZ1zLIkfSDZV5xBta3nuvbEOlsnKCPe4z5r3hyup/AXveevkl9A1eSWmLhd2FX4k7pJDe4MrLeQsux0HI/VWg=="
|
||||||
},
|
},
|
||||||
"node_modules/license-webpack-plugin": {
|
"node_modules/license-webpack-plugin": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
|
@@ -36,10 +36,12 @@
|
|||||||
"codemirror": "^5.65.8",
|
"codemirror": "^5.65.8",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"flag-icons": "^6.6.6",
|
||||||
"google-proto-files": "^3.0.2",
|
"google-proto-files": "^3.0.2",
|
||||||
"google-protobuf": "^3.21.2",
|
"google-protobuf": "^3.21.2",
|
||||||
"grpc-web": "^1.4.1",
|
"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",
|
"material-design-icons-iconfont": "^6.1.1",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"ngx-color": "^8.0.3",
|
"ngx-color": "^8.0.3",
|
||||||
|
@@ -18,6 +18,7 @@ import { ServiceWorkerModule } from '@angular/service-worker';
|
|||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
|
import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
|
||||||
import { from, Observable } from 'rxjs';
|
import { from, Observable } from 'rxjs';
|
||||||
|
import * as i18nIsoCountries from 'i18n-iso-countries';
|
||||||
import { AuthGuard } from 'src/app/guards/auth.guard';
|
import { AuthGuard } from 'src/app/guards/auth.guard';
|
||||||
import { RoleGuard } from 'src/app/guards/role.guard';
|
import { RoleGuard } from 'src/app/guards/role.guard';
|
||||||
import { UserGuard } from 'src/app/guards/user.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';
|
import { ToastService } from './services/toast.service';
|
||||||
|
|
||||||
registerLocaleData(localeDe);
|
registerLocaleData(localeDe);
|
||||||
|
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/de.json'));
|
||||||
registerLocaleData(localeZh);
|
registerLocaleData(localeZh);
|
||||||
|
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/zh.json'));
|
||||||
registerLocaleData(localeFr);
|
registerLocaleData(localeFr);
|
||||||
|
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/fr.json'));
|
||||||
registerLocaleData(localeIt);
|
registerLocaleData(localeIt);
|
||||||
|
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/it.json'));
|
||||||
registerLocaleData(localeEn);
|
registerLocaleData(localeEn);
|
||||||
|
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/en.json'));
|
||||||
|
|
||||||
export class WebpackTranslateLoader implements TranslateLoader {
|
export class WebpackTranslateLoader implements TranslateLoader {
|
||||||
getTranslation(lang: string): Observable<any> {
|
getTranslation(lang: string): Observable<any> {
|
||||||
|
@@ -139,14 +139,27 @@
|
|||||||
|
|
||||||
<p class="user-create-section">{{ 'USER.CREATE.ADDRESSANDPHONESECTION' | translate }}</p>
|
<p class="user-create-section">{{ 'USER.CREATE.ADDRESSANDPHONESECTION' | translate }}</p>
|
||||||
|
|
||||||
|
<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-form-field>
|
||||||
<cnsl-label>{{ 'USER.PROFILE.PHONE' | translate }}</cnsl-label>
|
<cnsl-label>{{ 'USER.PROFILE.PHONE' | translate }}</cnsl-label>
|
||||||
<input cnslInput formControlName="phone" />
|
<input cnslInput formControlName="phone" matTooltip="{{ 'USER.PROFILE.PHONE_HINT' | translate }}" />
|
||||||
<span cnslError *ngIf="phone?.invalid && phone?.errors?.required">
|
<span cnslError *ngIf="phone?.invalid && phone?.errors?.required">
|
||||||
{{ 'USER.VALIDATION.REQUIRED' | translate }}
|
{{ 'USER.VALIDATION.REQUIRED' | translate }}
|
||||||
</span>
|
</span>
|
||||||
</cnsl-form-field>
|
</cnsl-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="user-create-btn-container">
|
<div class="user-create-btn-container">
|
||||||
<button
|
<button
|
||||||
data-e2e="create-button"
|
data-e2e="create-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 { 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 { AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import parsePhoneNumber from 'libphonenumber-js';
|
import { parsePhoneNumber, CountryCode } from 'libphonenumber-js';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { debounceTime, takeUntil } from 'rxjs/operators';
|
|
||||||
import { AddHumanUserRequest } from 'src/app/proto/generated/zitadel/management_pb';
|
import { AddHumanUserRequest } from 'src/app/proto/generated/zitadel/management_pb';
|
||||||
import { Domain } from 'src/app/proto/generated/zitadel/org_pb';
|
import { Domain } from 'src/app/proto/generated/zitadel/org_pb';
|
||||||
import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_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 { ToastService } from 'src/app/services/toast.service';
|
||||||
|
|
||||||
import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from '../../validators';
|
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 {
|
function passwordConfirmValidator(c: AbstractControl): any {
|
||||||
if (!c.parent || !c) {
|
if (!c.parent || !c) {
|
||||||
@@ -40,10 +41,12 @@ function passwordConfirmValidator(c: AbstractControl): any {
|
|||||||
templateUrl: './user-create.component.html',
|
templateUrl: './user-create.component.html',
|
||||||
styleUrls: ['./user-create.component.scss'],
|
styleUrls: ['./user-create.component.scss'],
|
||||||
})
|
})
|
||||||
export class UserCreateComponent implements OnDestroy {
|
export class UserCreateComponent implements OnInit, OnDestroy {
|
||||||
public user: AddHumanUserRequest.AsObject = new AddHumanUserRequest().toObject();
|
public user: AddHumanUserRequest.AsObject = new AddHumanUserRequest().toObject();
|
||||||
public genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED];
|
public genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED];
|
||||||
public languages: string[] = ['de', 'en', 'it', 'fr'];
|
public languages: string[] = ['de', 'en', 'it', 'fr'];
|
||||||
|
public selected: CountryPhoneCode | undefined;
|
||||||
|
public countryPhoneCodes: CountryPhoneCode[] = [];
|
||||||
public userForm!: UntypedFormGroup;
|
public userForm!: UntypedFormGroup;
|
||||||
public pwdForm!: UntypedFormGroup;
|
public pwdForm!: UntypedFormGroup;
|
||||||
private destroyed$: Subject<void> = new Subject();
|
private destroyed$: Subject<void> = new Subject();
|
||||||
@@ -63,6 +66,7 @@ export class UserCreateComponent implements OnDestroy {
|
|||||||
private mgmtService: ManagementService,
|
private mgmtService: ManagementService,
|
||||||
private changeDetRef: ChangeDetectorRef,
|
private changeDetRef: ChangeDetectorRef,
|
||||||
private _location: Location,
|
private _location: Location,
|
||||||
|
private countryCallingCodesService: CountryCallingCodesService,
|
||||||
breadcrumbService: BreadcrumbService,
|
breadcrumbService: BreadcrumbService,
|
||||||
) {
|
) {
|
||||||
breadcrumbService.setBreadcrumb([
|
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 {
|
public createUser(): void {
|
||||||
@@ -190,7 +183,10 @@ export class UserCreateComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.phone && this.phone.value) {
|
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
|
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 {
|
ngOnDestroy(): void {
|
||||||
this.destroyed$.next();
|
this.destroyed$.next();
|
||||||
this.destroyed$.complete();
|
this.destroyed$.complete();
|
||||||
|
@@ -18,9 +18,11 @@ import { PasswordComplexityViewModule } from 'src/app/modules/password-complexit
|
|||||||
|
|
||||||
import { UserCreateRoutingModule } from './user-create-routing.module';
|
import { UserCreateRoutingModule } from './user-create-routing.module';
|
||||||
import { UserCreateComponent } from './user-create.component';
|
import { UserCreateComponent } from './user-create.component';
|
||||||
|
import { CountryCallingCodesService } from 'src/app/services/country-calling-codes.service';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [UserCreateComponent],
|
declarations: [UserCreateComponent],
|
||||||
|
providers: [CountryCallingCodesService],
|
||||||
imports: [
|
imports: [
|
||||||
UserCreateRoutingModule,
|
UserCreateRoutingModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
@@ -21,6 +21,7 @@ import { Buffer } from 'buffer';
|
|||||||
import { EditDialogComponent, EditDialogType } from './edit-dialog/edit-dialog.component';
|
import { EditDialogComponent, EditDialogType } from './edit-dialog/edit-dialog.component';
|
||||||
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
|
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
|
||||||
import { LoginPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
import { LoginPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
||||||
|
import { formatPhone } from 'src/app/utils/formatPhone';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cnsl-auth-user-detail',
|
selector: 'cnsl-auth-user-detail',
|
||||||
@@ -271,6 +272,9 @@ export class AuthUserDetailComponent implements OnDestroy {
|
|||||||
|
|
||||||
public savePhone(phone: string): void {
|
public savePhone(phone: string): void {
|
||||||
if (this.user?.human) {
|
if (this.user?.human) {
|
||||||
|
// Format phone before save (add +)
|
||||||
|
phone = formatPhone(phone).phone;
|
||||||
|
|
||||||
this.userService
|
this.userService
|
||||||
.setMyPhone(phone)
|
.setMyPhone(phone)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@@ -3,12 +3,31 @@
|
|||||||
</h1>
|
</h1>
|
||||||
<p class="desc cnsl-secondary-text">{{ data.descriptionKey | translate }}</p>
|
<p class="desc cnsl-secondary-text">{{ data.descriptionKey | translate }}</p>
|
||||||
<div mat-dialog-content>
|
<div mat-dialog-content>
|
||||||
<cnsl-form-field class="formfield">
|
<div class="phone-grid">
|
||||||
<cnsl-label
|
<cnsl-form-field *ngIf="isPhone">
|
||||||
>{{ data.labelKey | translate }} <span *ngIf="isPhone && phoneCountry">({{ phoneCountry }})</span>
|
<cnsl-label>{{ 'USER.PROFILE.COUNTRY' | translate }}</cnsl-label>
|
||||||
</cnsl-label>
|
<mat-select [(value)]="selected" (selectionChange)="setCountryCallingCode()">
|
||||||
<input [formControl]="valueControl" cnslInput (keydown.enter)="valueControl.valid ? closeDialogWithValue() : null" />
|
<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-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">
|
<ng-container *ngIf="data.type === EditDialogType.EMAIL && data.isVerifiedTextKey">
|
||||||
<mat-checkbox class="verified-checkbox" [(ngModel)]="isVerified">
|
<mat-checkbox class="verified-checkbox" [(ngModel)]="isVerified">
|
||||||
|
@@ -22,3 +22,22 @@
|
|||||||
border-radius: 0.5rem;
|
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 { UntypedFormControl, Validators } from '@angular/forms';
|
||||||
import {
|
import {
|
||||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||||
MatLegacyDialogRef as MatDialogRef,
|
MatLegacyDialogRef as MatDialogRef,
|
||||||
} from '@angular/material/legacy-dialog';
|
} 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 {
|
export enum EditDialogType {
|
||||||
PHONE = 1,
|
PHONE = 1,
|
||||||
@@ -16,39 +18,37 @@ export enum EditDialogType {
|
|||||||
templateUrl: './edit-dialog.component.html',
|
templateUrl: './edit-dialog.component.html',
|
||||||
styleUrls: ['./edit-dialog.component.scss'],
|
styleUrls: ['./edit-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class EditDialogComponent {
|
export class EditDialogComponent implements OnInit {
|
||||||
public isPhone: boolean = false;
|
public isPhone: boolean = false;
|
||||||
public isVerified: boolean = false;
|
public isVerified: boolean = false;
|
||||||
public phoneCountry: string = 'CH';
|
public phoneCountry: string = 'CH';
|
||||||
public valueControl: UntypedFormControl = new UntypedFormControl(['', [Validators.required]]);
|
public valueControl: UntypedFormControl = new UntypedFormControl(['', [Validators.required]]);
|
||||||
public EditDialogType: any = EditDialogType;
|
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);
|
this.valueControl.setValue(data.value);
|
||||||
if (data.type === EditDialogType.PHONE) {
|
if (data.type === EditDialogType.PHONE) {
|
||||||
this.isPhone = true;
|
this.isPhone = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.valueControl.valueChanges.subscribe((value) => {
|
|
||||||
if (value && value.length > 1) {
|
|
||||||
this.changeValue(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private changeValue(changedValue: string): void {
|
public setCountryCallingCode(): void {
|
||||||
if (this.isPhone && changedValue) {
|
let value = (this.valueControl?.value as string) || '';
|
||||||
try {
|
this.valueControl?.setValue('+' + this.selected?.countryCallingCode + ' ' + value.replace(/\+[0-9]*\s/, ''));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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="contact-method-row">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<span class="label cnsl-secondary-text">{{ 'USER.PHONE' | translate }}</span>
|
<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>
|
<span *ngIf="human.phone?.isPhoneVerified" class="contact-state verified">{{ 'USER.PHONEVERIFIED' | translate }}</span>
|
||||||
<div *ngIf="human.phone?.phone && !human.phone?.isPhoneVerified" class="block">
|
<div *ngIf="human.phone?.phone && !human.phone?.isPhoneVerified" class="block">
|
||||||
<span class="contact-state notverified">{{ 'USER.NOTVERIFIED' | translate }}</span>
|
<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 { MachineSecretDialogComponent } from './user-detail/machine-secret-dialog/machine-secret-dialog.component';
|
||||||
import { MetadataModule } from 'src/app/modules/metadata/metadata.module';
|
import { MetadataModule } from 'src/app/modules/metadata/metadata.module';
|
||||||
import { QRCodeModule } from 'angularx-qrcode';
|
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({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -76,8 +79,10 @@ import { QRCodeModule } from 'angularx-qrcode';
|
|||||||
DialogU2FComponent,
|
DialogU2FComponent,
|
||||||
DialogPasswordlessComponent,
|
DialogPasswordlessComponent,
|
||||||
AuthFactorDialogComponent,
|
AuthFactorDialogComponent,
|
||||||
|
PhoneDetailComponent,
|
||||||
MachineSecretDialogComponent,
|
MachineSecretDialogComponent,
|
||||||
],
|
],
|
||||||
|
providers: [CountryCallingCodesService],
|
||||||
imports: [
|
imports: [
|
||||||
ChangesModule,
|
ChangesModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -122,6 +127,7 @@ import { QRCodeModule } from 'angularx-qrcode';
|
|||||||
InputModule,
|
InputModule,
|
||||||
MachineKeysModule,
|
MachineKeysModule,
|
||||||
InfoSectionModule,
|
InfoSectionModule,
|
||||||
|
MatSelectModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class UserDetailModule {}
|
export class UserDetailModule {}
|
||||||
|
@@ -21,6 +21,7 @@ import { Buffer } from 'buffer';
|
|||||||
import { EditDialogComponent, EditDialogType } from '../auth-user-detail/edit-dialog/edit-dialog.component';
|
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 { ResendEmailDialogComponent } from '../auth-user-detail/resend-email-dialog/resend-email-dialog.component';
|
||||||
import { LoginPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
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';
|
import { MachineSecretDialogComponent } from './machine-secret-dialog/machine-secret-dialog.component';
|
||||||
|
|
||||||
const GENERAL: SidenavSetting = { id: 'general', i18nKey: 'USER.SETTINGS.GENERAL' };
|
const GENERAL: SidenavSetting = { id: 'general', i18nKey: 'USER.SETTINGS.GENERAL' };
|
||||||
@@ -355,6 +356,9 @@ export class UserDetailComponent implements OnInit {
|
|||||||
|
|
||||||
public savePhone(phone: string): void {
|
public savePhone(phone: string): void {
|
||||||
if (this.user.id && phone) {
|
if (this.user.id && phone) {
|
||||||
|
// Format phone before save (add +)
|
||||||
|
phone = formatPhone(phone).phone;
|
||||||
|
|
||||||
this.mgmtUserService
|
this.mgmtUserService
|
||||||
.updateHumanPhone(this.user.id, phone)
|
.updateHumanPhone(this.user.id, phone)
|
||||||
.then(() => {
|
.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 |