From db2b4eaabcab4975c1ae447255f00ec25a3cf694 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Fri, 2 Oct 2026 09:27:31 +0200 Subject: [PATCH] feat(portal-self-service): tell the citizen why a diploma upload was refused (refs #192) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../app/registration/registration-page.html | 6 ++-- .../src/app/registration/registration-page.ts | 28 +++++++++++++++---- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/apps/self-service/src/app/registration/registration-page.html b/apps/self-service/src/app/registration/registration-page.html index 255dba4..e91cc80 100644 --- a/apps/self-service/src/app/registration/registration-page.html +++ b/apps/self-service/src/app/registration/registration-page.html @@ -14,10 +14,8 @@ @if (documentsProvided()) {

Uw documenten zijn aangeleverd.

} @else { - @if (provideDocumentsFailed()) { -

- Het aanleveren van uw documenten is niet gelukt. Probeer het opnieuw. -

+ @if (provideDocumentsError(); as error) { +

{{ error }}

}

Lever uw diploma aan (PDF).

diff --git a/apps/self-service/src/app/registration/registration-page.ts b/apps/self-service/src/app/registration/registration-page.ts index b225ba3..dfb0670 100644 --- a/apps/self-service/src/app/registration/registration-page.ts +++ b/apps/self-service/src/app/registration/registration-page.ts @@ -1,5 +1,6 @@ import { Component, inject, type OnInit, signal } from '@angular/core'; -import { BffApiV1Service, type CurrentRegistration, type SubmitAccepted } from 'api-client'; +import { HttpErrorResponse } from '@angular/common/http'; +import { BffApiV1Service, type CurrentRegistration, type Refusal, type SubmitAccepted } from 'api-client'; import { AuthService } from 'auth'; import { UtrechtComponentsModule } from 'ui'; @@ -31,7 +32,8 @@ export class RegistrationPage implements OnInit { protected readonly withdrawFailed = signal(false); protected readonly providingDocuments = signal(false); protected readonly documentsProvided = signal(false); - protected readonly provideDocumentsFailed = signal(false); + /** Why the last upload failed, worded for the citizen; undefined while there is nothing to report. */ + protected readonly provideDocumentsError = signal(undefined); protected readonly selectedFile = signal(undefined); /** Resume an existing in-flight registration after a refresh (S-26): the BFF returns the caller's @@ -80,12 +82,12 @@ export class RegistrationPage implements OnInit { return; } this.providingDocuments.set(true); - this.provideDocumentsFailed.set(false); + this.provideDocumentsError.set(undefined); let contentBase64: string; try { contentBase64 = await readAsBase64(file); } catch { - this.provideDocumentsFailed.set(true); + this.provideDocumentsError.set(uploadFailure()); this.providingDocuments.set(false); return; } @@ -101,8 +103,8 @@ export class RegistrationPage implements OnInit { this.providingDocuments.set(false); }, // Surface the failure instead of swallowing it: keep the action so the user can retry. - error: () => { - this.provideDocumentsFailed.set(true); + error: (err: unknown) => { + this.provideDocumentsError.set(uploadFailure(err)); this.providingDocuments.set(false); }, }); @@ -129,6 +131,20 @@ export class RegistrationPage implements OnInit { } } +/** Word a failed upload for the citizen: the BFF says why a file was refused (422 + reason) or that + * the virus scanner was unreachable (503, S-29); anything else is a generic retry. */ +function uploadFailure(err?: unknown): string { + if (err instanceof HttpErrorResponse && err.status === 422) { + return (err.error as Refusal | null)?.reason === 'infected' + ? 'Er is een virus gevonden in dit bestand. Het is niet opgeslagen; lever een ander bestand aan.' + : 'Dit bestand is geen PDF. Lever uw diploma aan als PDF-bestand.'; + } + if (err instanceof HttpErrorResponse && err.status === 503) { + return 'Uw bestand kan tijdelijk niet worden gecontroleerd. Probeer het later opnieuw.'; + } + return 'Het aanleveren van uw documenten is niet gelukt. Probeer het opnieuw.'; +} + /** Read a file's bytes as a base64 string (without the `data:...;base64,` prefix). */ function readAsBase64(file: File): Promise { return new Promise((resolve, reject) => {