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
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
77 changes: 76 additions & 1 deletion 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 @@ -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',
},
}),
Expand Down Expand Up @@ -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('<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 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 (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('<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
4 changes: 3 additions & 1 deletion src/assets/i18n/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@
"messageHtml": "{{description}}<br><small class=\"counter\">Temps restant: {{counter}} segundos</small>",
"organization":"Organització: ",
"holder":"Titular: ",
"expiration":"Data de caducitat: "
"powers":"Poders: ",
"expiration":"Data de caducitat: ",
"unknown": "Desconegut"
}
}
4 changes: 3 additions & 1 deletion src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,9 @@
"messageHtml": "{{description}}<br><small class=\"counter\">Time remaining: {{counter}} seconds</small>",
"organization":"Organization: ",
"holder":"Holder: ",
"expiration":"Expiration date: "
"powers":"Powers: ",
"expiration":"Expiration date: ",
"unknown": "Unknown"
}

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