diff --git a/package-lock.json b/package-lock.json index 0c262dd4..15d4a0f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "eudistack-wallet-ui", - "version": "2.0.13", + "version": "2.0.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "eudistack-wallet-ui", - "version": "2.0.13", + "version": "2.0.14", "dependencies": { "@angular/common": "^18.2.2", "@angular/core": "^18.2.2", diff --git a/package.json b/package.json index 4a410e49..710d654d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eudistack-wallet-ui", - "version": "2.0.13", + "version": "2.0.14", "author": "Ionic Framework", "homepage": "https://ionicframework.com/", "scripts": { diff --git a/src/app/interfaces/websocket-data.ts b/src/app/interfaces/websocket-data.ts index f7b8e93d..4c203778 100644 --- a/src/app/interfaces/websocket-data.ts +++ b/src/app/interfaces/websocket-data.ts @@ -3,14 +3,22 @@ interface PinRequestData { timeout?: number; } -interface NotificationData { +export interface Power { + function: string; + action: string[]; +} + +export interface CredentialPreview { + power: Power[]; + subjectName: string; + organization: string; + expirationDate: string; +} + + +export interface NotificationData { decision: boolean; - credentialPreview?: { - subjectName?: string; - organization?: string; - issuer?: string; - expirationDate?: string; - }; + credentialPreview?: CredentialPreview; timeout?: number; expiresAt?: number; } diff --git a/src/app/services/websocket.service.spec.ts b/src/app/services/websocket.service.spec.ts index 610695f9..72ab1f6b 100644 --- a/src/app/services/websocket.service.spec.ts +++ b/src/app/services/websocket.service.spec.ts @@ -7,6 +7,7 @@ import { environment } from 'src/environments/environment'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { WEBSOCKET_PIN_PATH } from '../constants/api.constants'; import { LoaderService } from './loader.service'; +import { Power } from '../interfaces/verifiable-credential'; //todo mock broadcast channel @@ -415,8 +416,8 @@ describe('WebsocketService', () => { timeout: 60, credentialPreview: { subjectName: 'John Doe', + power: [{ function: 'Administrar', action: ['create', 'update'] }], organization: 'Test Org', - issuer: 'Test Issuer', expirationDate: '2025-12-31', }, }), @@ -670,4 +671,78 @@ describe('WebsocketService', () => { clearIntervalSpy.mockRestore(); })); + it('should map powers to human readable format with single power', () => { + const powers = [ + { function: 'Administrar', action: ['create', 'update'] } + ]; + + const result = service['mapPowersToHumanReadable'](powers); + + expect(translateMock.instant).toHaveBeenCalledWith('vc-fields.power.administrar'); + expect(translateMock.instant).toHaveBeenCalledWith('vc-fields.power.create'); + expect(translateMock.instant).toHaveBeenCalledWith('vc-fields.power.update'); + expect(result).toContain(':'); + }); + + it('should map powers to human readable format with multiple powers', () => { + const powers = [ + { function: 'Administrar', action: ['crear'] }, + { function: 'Gestionar', action: ['borrar', 'modificar'] } + ]; + + const result = service['mapPowersToHumanReadable'](powers); + + expect(result).toContain('
'); + expect(translateMock.instant).toHaveBeenCalledWith('vc-fields.power.administrar'); + expect(translateMock.instant).toHaveBeenCalledWith('vc-fields.power.gestionar'); + }); + + it('should return empty string for empty powers array', () => { + expect(service['mapPowersToHumanReadable']([])).toBe(''); + }); + + it('should normalize key by removing special characters and converting to lowercase', () => { + expect(service['normalizeKey']('Administrar')).toBe('administrar'); + expect(service['normalizeKey']('Test Key-123')).toBe('testkey123'); + expect(service['normalizeKey']('Special@#$Chars')).toBe('specialchars'); + expect(service['normalizeKey'](' spaces ')).toBe('spaces'); + }); + + it('should normalize key with null or undefined', () => { + expect(service['normalizeKey'](null)).toBe(''); + expect(service['normalizeKey'](undefined)).toBe(''); + }); + + it('should normalize key with numbers', () => { + expect(service['normalizeKey'](123)).toBe('123'); + expect(service['normalizeKey'](0)).toBe('0'); + }); + + it('should normalize action keys from array', () => { + const actions = ['Crear', 'Editar', 'Borrar']; + const result = service['normalizeActionKeys'](actions); + + expect(result).toEqual(['crear', 'editar', 'borrar']); + }); + + it('should normalize action keys with special characters', () => { + const actions = ['Test-Action', 'Special@Char', '123Number']; + const result = service['normalizeActionKeys'](actions); + + expect(result).toEqual(['testaction', 'specialchar', '123number']); + }); + + it('should return empty array for non-array action keys', () => { + expect(service['normalizeActionKeys'](null)).toEqual([]); + expect(service['normalizeActionKeys'](undefined)).toEqual([]); + expect(service['normalizeActionKeys']('not-array')).toEqual([]); + }); + + it('should filter out empty strings from normalized action keys', () => { + const actions = ['valid', '', ' ', 'another']; + const result = service['normalizeActionKeys'](actions); + + expect(result).toEqual(['valid', 'another']); + }); + }); diff --git a/src/app/services/websocket.service.ts b/src/app/services/websocket.service.ts index 71fa5f65..f015bbda 100644 --- a/src/app/services/websocket.service.ts +++ b/src/app/services/websocket.service.ts @@ -6,7 +6,7 @@ import { TranslateService } from '@ngx-translate/core'; import { WEBSOCKET_NOTIFICATION_PATH, WEBSOCKET_PIN_PATH } from '../constants/api.constants'; import { LoaderService } from './loader.service'; import { ToastServiceHandler } from './toast.service'; -import { isPinRequest, isNotificationRequest } from '../interfaces/websocket-data'; +import { isPinRequest, isNotificationRequest, Power, CredentialPreview } from '../interfaces/websocket-data'; @Injectable({ providedIn: 'root', @@ -200,9 +200,10 @@ export class WebsocketService { const counter = data.timeout || 80; - const preview = data.credentialPreview; + const preview = data.credentialPreview as CredentialPreview; const subjectLabel = this.translate.instant('confirmation.holder'); const organizationLabel = this.translate.instant('confirmation.organization'); + const powersLabel = this.translate.instant('confirmation.powers'); const expirationLabel = this.translate.instant('confirmation.expiration'); @@ -219,9 +220,13 @@ export class WebsocketService { ${organizationLabel}${this.escapeHtml(preview.organization)} -
- ${expirationLabel}${this.formatDateHuman(preview.expirationDate)} -
+
+ ${powersLabel}${this.mapPowersToHumanReadable(preview.power)} +
+ +
+ ${expirationLabel}${this.formatDateHuman(preview.expirationDate)} +
`; } @@ -278,13 +283,69 @@ export class WebsocketService { this.toastServiceHandler .showErrorAlert("The QR session expired") .subscribe(); - window.location.reload(); } }); interval = this.startCountdown(alert, descriptionWithPreview, counter); } + private mapPowersToHumanReadable(powers: Power[]): string { + if (powers.length === 0) return ''; + + const unknown = this.translate.instant('confirmation.unknown'); + + const lines = powers + .map((p) => { + const fnKey = this.normalizeKey(p?.function); + const actionKeys = this.normalizeActionKeys(p?.action); + + const functionLabelRaw = + this.getSafeTranslation(`vc-fields.power.${fnKey}`, p?.function, unknown); + + const actionLabelsRaw = actionKeys + .map((a) => this.getSafeTranslation(`vc-fields.power.${a}`, a, unknown)) + .filter((x) => x && x !== unknown); + + const functionLabel = this.escapeHtml(functionLabelRaw); + const actionLabels = this.escapeHtml(actionLabelsRaw.join(', ')); + + if (!functionLabel || !actionLabels) return ''; + + return `${functionLabel}: ${actionLabels}`; + }) + .filter(Boolean); + + return lines.join('
'); + } + + private getSafeTranslation(key: string, fallbackText: unknown, unknown: string): string { + const translated = this.translate.instant(key); + + const hasRealTranslation = translated && translated !== key; + + if (hasRealTranslation) return String(translated); + + const fb = String(fallbackText ?? '').trim(); + const looksLikeKey = fb.includes('.') || fb.includes('_') || fb.includes('-'); + if (!fb || looksLikeKey) return unknown; + + return fb; + } + + private normalizeKey(value: unknown): string { + return String(value ?? '') + .trim() + .toLowerCase() + .replace(/[^a-z0-9]/g, ''); + } + + private normalizeActionKeys(actions: unknown): string[] { + if (!Array.isArray(actions)) return []; + return actions + .map((a) => this.normalizeKey(a)) + .filter(Boolean); + } + private async showTempOkMessage(message: string): Promise { const alert = await this.alertController.create({ message: ` diff --git a/src/assets/i18n/ca.json b/src/assets/i18n/ca.json index 6450a2a1..e1be1f3b 100644 --- a/src/assets/i18n/ca.json +++ b/src/assets/i18n/ca.json @@ -233,6 +233,8 @@ "messageHtml": "{{description}}
Temps restant: {{counter}} segundos", "organization":"Organització: ", "holder":"Titular: ", - "expiration":"Data de caducitat: " + "powers":"Poders: ", + "expiration":"Data de caducitat: ", + "unknown": "Desconegut" } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index a5e25816..856958f8 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -231,7 +231,9 @@ "messageHtml": "{{description}}
Time remaining: {{counter}} seconds", "organization":"Organization: ", "holder":"Holder: ", - "expiration":"Expiration date: " + "powers":"Powers: ", + "expiration":"Expiration date: ", + "unknown": "Unknown" } } diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 1041d85e..01d39bd3 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -232,6 +232,8 @@ "messageHtml": "{{description}}
Tiempo restante: {{counter}} segundos", "organization":"Organización: ", "holder":"Titular: ", - "expiration":"Fecha de caducidad: " + "powers":"Poderes: ", + "expiration":"Fecha de caducidad: ", + "unknown": "Desconocido" } }