Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eudistack-wallet-ui",
"version": "2.0.13",
"version": "2.0.14",
"author": "Ionic Framework",
"homepage": "https://ionicframework.com/",
"scripts": {
Expand Down
22 changes: 15 additions & 7 deletions src/app/interfaces/websocket-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
81 changes: 81 additions & 0 deletions src/app/services/websocket.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -670,4 +671,84 @@ 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('<br/>');
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 return empty string for non-array powers', () => {
expect(service['mapPowersToHumanReadable'](null as any)).toBe('');
expect(service['mapPowersToHumanReadable'](undefined as any)).toBe('');
expect(service['mapPowersToHumanReadable']('not-array' as any)).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']);
});

});
73 changes: 67 additions & 6 deletions src/app/services/websocket.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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');


Expand All @@ -219,9 +220,13 @@ export class WebsocketService {
<span class="cred-label"><strong>${organizationLabel}</strong>${this.escapeHtml(preview.organization)}</span>
</div>

<div class="cred-row">
<span class="cred-label"><strong>${expirationLabel}</strong>${this.formatDateHuman(preview.expirationDate)}</span>
</div>
<div class="cred-row">
<span class="cred-label"><strong>${powersLabel}</strong>${this.mapPowersToHumanReadable(preview.power)}</span>
</div>

<div class="cred-row">
<span class="cred-label"><strong>${expirationLabel}</strong>${this.formatDateHuman(preview.expirationDate)}</span>
</div>
</div>
`;
}
Expand Down Expand Up @@ -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 (!Array.isArray(powers) || powers.length === 0) return '';

const unknown = this.translate.instant('common.unknown') || 'Desconocido';

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('<br/>');
}

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<void> {
const alert = await this.alertController.create({
message: `
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@
"messageHtml": "{{description}}<br><small class=\"counter\">Temps restant: {{counter}} segundos</small>",
"organization":"Organització: ",
"holder":"Titular: ",
"powers":"Poders: ",
"expiration":"Data de caducitat: "
}
}
1 change: 1 addition & 0 deletions src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@
"messageHtml": "{{description}}<br><small class=\"counter\">Time remaining: {{counter}} seconds</small>",
"organization":"Organization: ",
"holder":"Holder: ",
"powers":"Powers: ",
"expiration":"Expiration date: "
}

Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@
"messageHtml": "{{description}}<br><small class=\"counter\">Tiempo restante: {{counter}} segundos</small>",
"organization":"Organización: ",
"holder":"Titular: ",
"powers":"Poderes: ",
"expiration":"Fecha de caducidad: "
}
}
Loading