Skip to content

Commit a1038c5

Browse files
authored
Show compliance badge in product spec form (Ficodes#250)
1 parent cee311c commit a1038c5

3 files changed

Lines changed: 86 additions & 43 deletions

File tree

src/app/pages/seller-offerings/offerings/seller-product-spec/update-product-spec/update-product-spec.component.html

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,29 @@ <h2 class="text-3xl font-bold text-primary-100 ml-4 pb-4 dark:text-white">{{ 'UP
268268
</div>-->
269269
}
270270
@if (isCurrentStep('compliance')) {
271-
<div class="flex w-full justify-end items-center gap-2 px-4 mb-3">
271+
<div class="flex w-full items-center px-4 mb-3">
272+
<div class="flex items-center">
273+
@if(complianceVC && complianceLevel=='BL'){
274+
<img
275+
class="h-[1.9rem] max-w-[12rem] object-contain rounded-md"
276+
src="assets/logos/baseline.png"
277+
alt="Baseline logo"
278+
/>
279+
} @else if(complianceVC && complianceLevel=='P') {
280+
<img
281+
class="h-[1.9rem] max-w-[12rem] object-contain rounded-md"
282+
src="assets/logos/prof.png"
283+
alt="Professional logo"
284+
/>
285+
} @else if(complianceVC && complianceLevel=='PP') {
286+
<img
287+
class="h-[1.9rem] max-w-[12rem] object-contain rounded-md"
288+
src="assets/logos/profplus.png"
289+
alt="Professional plus logo"
290+
/>
291+
}
292+
</div>
293+
<div class="ml-auto flex items-center gap-2">
272294
<button type="button" (click)="openRequestValidationModal(); $event.stopPropagation();"
273295
[disabled]="!hasSelfAttestation() || hasUnsavedComplianceProfileChanges()"
274296
[ngClass]="(!hasSelfAttestation() || hasUnsavedComplianceProfileChanges()) ? 'opacity-50 cursor-not-allowed' : 'hover:bg-primary-50'"
@@ -284,6 +306,7 @@ <h2 class="text-3xl font-bold text-primary-100 ml-4 pb-4 dark:text-white">{{ 'UP
284306
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11h2v5m-2 0h4m-2.592-8.5h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
285307
</svg>
286308
</div>
309+
</div>
287310
</div>
288311
@if (hasUnsavedComplianceProfileChanges()) {
289312
<div class="mx-4 mb-3 p-3 text-sm text-amber-800 rounded-lg bg-amber-100 dark:bg-amber-900 dark:text-amber-200" role="alert">

src/app/pages/seller-offerings/offerings/seller-product-spec/update-product-spec/update-product-spec.component.spec.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { AttachmentServiceService } from 'src/app/services/attachment-service.se
1313
import { ServiceSpecServiceService } from 'src/app/services/service-spec-service.service';
1414
import { ResourceSpecServiceService } from 'src/app/services/resource-spec-service.service';
1515
import { PaginationService } from 'src/app/services/pagination.service';
16-
import { QrVerifierService } from 'src/app/services/qr-verifier.service';
1716

1817
class SyncFileReaderMock {
1918
onload: ((event: any) => void) | null = null;
@@ -25,6 +24,13 @@ class SyncFileReaderMock {
2524
}
2625
}
2726

27+
const asJwt = (payload: any): string => {
28+
const encode = (value: any) =>
29+
btoa(JSON.stringify(value)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
30+
31+
return `${encode({ alg: 'none', typ: 'JWT' })}.${encode(payload)}.`;
32+
};
33+
2834
describe('UpdateProductSpecComponent', () => {
2935
let component: UpdateProductSpecComponent;
3036
let fixture: ComponentFixture<UpdateProductSpecComponent>;
@@ -37,7 +43,6 @@ describe('UpdateProductSpecComponent', () => {
3743
let attachmentServiceSpy: jasmine.SpyObj<AttachmentServiceService>;
3844
let servSpecServiceSpy: jasmine.SpyObj<ServiceSpecServiceService>;
3945
let resSpecServiceSpy: jasmine.SpyObj<ResourceSpecServiceService>;
40-
let qrVerifierSpy: jasmine.SpyObj<QrVerifierService>;
4146
let paginationServiceSpy: jasmine.SpyObj<PaginationService>;
4247
let originalFileReader: any;
4348

@@ -67,15 +72,12 @@ describe('UpdateProductSpecComponent', () => {
6772
attachmentServiceSpy = jasmine.createSpyObj<AttachmentServiceService>('AttachmentServiceService', ['uploadFile']);
6873
servSpecServiceSpy = jasmine.createSpyObj<ServiceSpecServiceService>('ServiceSpecServiceService', ['getServiceSpecByUser']);
6974
resSpecServiceSpy = jasmine.createSpyObj<ResourceSpecServiceService>('ResourceSpecServiceService', ['getResourceSpecByUser']);
70-
qrVerifierSpy = jasmine.createSpyObj<QrVerifierService>('QrVerifierService', ['launchPopup', 'pollCertCredential']);
7175
paginationServiceSpy = jasmine.createSpyObj<PaginationService>('PaginationService', ['getItemsPaginated']);
7276

7377
localStorageSpy.getObject.and.returnValue({});
7478
attachmentServiceSpy.uploadFile.and.returnValue(of({ content: 'https://uploaded.file' }));
7579
prodSpecServiceSpy.getResSpecById.and.resolveTo({ id: 'rel-prod', name: 'Rel Prod' } as any);
7680
prodSpecServiceSpy.updateProdSpec.and.returnValue(of({ id: 'created' }));
77-
qrVerifierSpy.launchPopup.and.returnValue({} as Window);
78-
qrVerifierSpy.pollCertCredential.and.resolveTo({ subject: { compliance: [] }, vc: 'vc-token' });
7981
paginationServiceSpy.getItemsPaginated.and.resolveTo(defaultPaginationData);
8082

8183
await TestBed.configureTestingModule({
@@ -90,7 +92,6 @@ describe('UpdateProductSpecComponent', () => {
9092
{ provide: AttachmentServiceService, useValue: attachmentServiceSpy },
9193
{ provide: ServiceSpecServiceService, useValue: servSpecServiceSpy },
9294
{ provide: ResourceSpecServiceService, useValue: resSpecServiceSpy },
93-
{ provide: QrVerifierService, useValue: qrVerifierSpy },
9495
{ provide: PaginationService, useValue: paginationServiceSpy }
9596
]
9697
}).compileComponents();
@@ -508,6 +509,33 @@ describe('UpdateProductSpecComponent', () => {
508509
expect(component.prodChars[0].id).toBe('urn:ngsi-ld:characteristic:platinum-id');
509510
});
510511

512+
it('populateProductInfo should decode Compliance:VC and expose compliance badge level', () => {
513+
const vcToken = asJwt({
514+
vc: {
515+
credentialSubject: {
516+
'gx:labelLevel': 'P'
517+
}
518+
}
519+
});
520+
521+
component.prod = {
522+
...component.prod,
523+
productSpecCharacteristic: [
524+
{
525+
id: 'urn:ngsi-ld:characteristic:vc-id',
526+
name: 'Compliance:VC',
527+
productSpecCharacteristicValue: [{ isDefault: true, value: vcToken }]
528+
}
529+
]
530+
} as any;
531+
532+
component.populateProductInfo();
533+
534+
expect(component.complianceVCId).toBe('urn:ngsi-ld:characteristic:vc-id');
535+
expect(component.complianceVC).toBe(vcToken);
536+
expect(component.complianceLevel).toBe('P');
537+
});
538+
511539
it('hasUnsavedComplianceProfileChanges should return false when compliance profile matches persisted data', () => {
512540
component.prod = {
513541
...component.prod,

src/app/pages/seller-offerings/offerings/seller-product-spec/update-product-spec/update-product-spec.component.ts

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { NgxFileDropEntry, FileSystemFileEntry, FileSystemDirectoryEntry } from
1717
import { certifications } from 'src/app/models/certification-standards.const'
1818
import * as moment from 'moment';
1919
import { v4 as uuidv4 } from 'uuid';
20-
import { QrVerifierService } from 'src/app/services/qr-verifier.service';
2120
import { jwtDecode } from "jwt-decode";
2221
import { noWhitespaceValidator } from 'src/app/validators/validators';
2322
import { Subject } from 'rxjs';
@@ -114,6 +113,7 @@ export class UpdateProductSpecComponent implements OnInit, OnDestroy {
114113
selectedISOS:any[]=[];
115114
additionalISOS:any[]=[];
116115
verifiedISO:string[] = [];
116+
complianceLevel:string='NL';
117117
selectedISO:any;
118118
complianceVC:any = null;
119119
complianceVCId:string = '';
@@ -213,7 +213,6 @@ export class UpdateProductSpecComponent implements OnInit, OnDestroy {
213213
private attachmentService: AttachmentServiceService,
214214
private servSpecService: ServiceSpecServiceService,
215215
private resSpecService: ResourceSpecServiceService,
216-
private qrVerifier: QrVerifierService,
217216
private paginationService: PaginationService
218217
) {
219218
for(let i=0; i<certifications.length; i++){
@@ -300,26 +299,10 @@ export class UpdateProductSpecComponent implements OnInit, OnDestroy {
300299
// Check if this is a VC
301300
if (this.prod.productSpecCharacteristic[i].name == 'Compliance:VC') {
302301
this.complianceVCId = this.prod.productSpecCharacteristic[i].id || '';
302+
this.complianceVC = this.prod.productSpecCharacteristic[i].productSpecCharacteristicValue?.[0]?.value ?? null;
303303
// Decode the token
304304
try {
305-
const decoded = jwtDecode(this.prod.productSpecCharacteristic[i].productSpecCharacteristicValue[0].value)
306-
let credential: any = null
307-
308-
if ('verifiableCredential' in decoded) {
309-
credential = decoded.verifiableCredential;
310-
} else if('vc' in decoded) {
311-
credential = decoded.vc;
312-
}
313-
314-
if (credential != null) {
315-
const subject = credential.credentialSubject;
316-
317-
if ('compliance' in subject) {
318-
this.verifiedISO = subject.compliance.map((comp: any) => {
319-
return comp.standard
320-
})
321-
}
322-
}
305+
this.applyComplianceDataFromVcToken(this.complianceVC);
323306
} catch (e) {
324307
console.log(e)
325308
}
@@ -638,27 +621,36 @@ export class UpdateProductSpecComponent implements OnInit, OnDestroy {
638621
console.log(this.selectedISOS)
639622
}
640623

641-
verifyCredential() {
642-
console.log('verifing credential')
643-
const state = `cert:${uuidv4()}`
624+
private applyComplianceDataFromVcToken(vcToken: any) {
625+
if (!vcToken || typeof vcToken !== 'string') {
626+
this.complianceLevel = 'NL';
627+
return;
628+
}
629+
630+
const allowedLevels = ['NL', 'BL', 'P', 'PP'];
644631

645-
const qrWin = this.qrVerifier.launchPopup(`${environment.SIOP_INFO.verifierHost}${environment.SIOP_INFO.verifierQRCodePath}?state=${state}&client_callback=${environment.SIOP_INFO.callbackURL}&client_id=${environment.SIOP_INFO.clientID}`, 'Scan QR code', 500, 500)
646-
this.qrVerifier.pollCertCredential(qrWin, state).then((data) => {
647-
// Process the VC to verify the cerficates
648-
// Validate the product ID and company
649-
const subject = data.subject
632+
try {
633+
const decoded: any = jwtDecode(vcToken);
634+
let credential: any = null;
650635

651-
if (subject.compliance) {
652-
subject.compliance.forEach((comp: any) => {
653-
this.verifiedISO.push(comp.standard)
654-
})
636+
if ('verifiableCredential' in decoded) {
637+
credential = decoded.verifiableCredential;
638+
} else if ('vc' in decoded) {
639+
credential = decoded.vc;
640+
}
655641

656-
this.complianceVC = data.vc;
642+
const subject = credential?.credentialSubject;
643+
if (!subject) {
644+
this.complianceLevel = 'NL';
645+
return;
657646
}
658647

659-
//this.verifiedISO[sel.name] = data.vc
660-
console.log(`We got the vc: ${data['vc']}`)
661-
})
648+
const level = subject['gx:labelLevel'];
649+
this.complianceLevel = (typeof level === 'string' && allowedLevels.includes(level)) ? level : 'NL';
650+
} catch (error) {
651+
this.complianceLevel = 'NL';
652+
console.log(error);
653+
}
662654
}
663655

664656
openRequestValidationModal() {

0 commit comments

Comments
 (0)