diff --git a/src/gdprApi/actions/__tests__/deleteProfile.test.ts b/src/gdprApi/actions/__tests__/deleteProfile.test.ts index f82fa9bea..bdf388e26 100644 --- a/src/gdprApi/actions/__tests__/deleteProfile.test.ts +++ b/src/gdprApi/actions/__tests__/deleteProfile.test.ts @@ -11,11 +11,14 @@ import { getMockCalls } from '../../../common/test/mockHelper'; import { createDeleteProfileAction, deleteProfileType, - getDeleteProfileResult, + getDeleteProfileResultOrError, + isInsufficientLoaResult, } from '../deleteProfile'; import { getDeleteMyProfileMutationResult } from '../../../common/test/getDeleteMyProfileMutationResult'; import { DeleteResultLists } from '../../../profile/helpers/parseDeleteProfileResult'; +type ActionResults = ReturnType; + describe('deleteProfile.ts', () => { const queryTracker = vi.fn(); const keycloakAuthCode = 'keycloak-auth-code'; @@ -26,12 +29,14 @@ describe('deleteProfile.ts', () => { returnFailed, returnError, returnNoData, + returnInsufficientLoa, }: { noKeycloadAuthCode?: boolean; noTunnistamoAuthCode?: boolean; returnFailed?: boolean; returnError?: boolean; returnNoData?: boolean; + returnInsufficientLoa?: boolean; } = {}) => { fetchMock.mockIf(/.*\/graphql\/.*$/, async (req: Request) => { const payload = await req.json(); @@ -49,6 +54,13 @@ describe('deleteProfile.ts', () => { }), }); } + if (returnInsufficientLoa === true) { + return Promise.reject({ + body: JSON.stringify({ + message: 'insufficientLoa', + }), + }); + } return Promise.resolve({ body: JSON.stringify(response) }); }); @@ -152,6 +164,19 @@ describe('deleteProfile.ts', () => { expect(result).toBeUndefined(); expect(!!error).toBeTruthy(); }); + it('Insufficient loa returns error', async () => { + const { runner, getAction } = initTests({ + returnInsufficientLoa: true, + returnNoData: true, + }); + const [errorMessage] = await to( + getAction().executor(getAction(), runner) + ); + + expect( + isInsufficientLoaResult(({ errorMessage } as unknown) as ActionResults) + ).toBeTruthy(); + }); it('Result should not be stored to sessionStorage', async () => { const { getAction } = initTests(); expect(getOption(getAction(), 'noStorage')).toBeTruthy(); @@ -162,7 +187,8 @@ describe('deleteProfile.ts', () => { await waitFor(() => { expect(runner.isFinished()).toBeTruthy(); }); - const resultArray = getDeleteProfileResult(runner) as DeleteResultLists; + const resultArray = getDeleteProfileResultOrError(runner) + .result as DeleteResultLists; expect(resultArray.successful).toHaveLength(2); expect(resultArray.failures).toHaveLength(0); }); diff --git a/src/gdprApi/actions/__tests__/deleteServiceConnection.test.ts b/src/gdprApi/actions/__tests__/deleteServiceConnection.test.ts index fbbd74d51..3982c784a 100644 --- a/src/gdprApi/actions/__tests__/deleteServiceConnection.test.ts +++ b/src/gdprApi/actions/__tests__/deleteServiceConnection.test.ts @@ -14,6 +14,7 @@ import { deleteServiceConnectionType, getDeleteServiceConnectionResultOrError, isForbiddenResult, + isInsufficientLoaResult, isSuccessResult, } from '../deleteServiceConnection'; @@ -29,12 +30,14 @@ describe('deleteServiceConnection.ts', () => { returnForbidden, returnError, returnNoData, + returnInsufficientLoa, }: { noKeycloadAuthCode?: boolean; noTunnistamoAuthCode?: boolean; returnForbidden?: boolean; returnError?: boolean; returnNoData?: boolean; + returnInsufficientLoa?: boolean; } = {}) => { fetchMock.mockIf(/.*\/graphql\/.*$/, async (req: Request) => { const payload = await req.json(); @@ -56,6 +59,13 @@ describe('deleteServiceConnection.ts', () => { }), }); } + if (returnInsufficientLoa === true) { + return Promise.reject({ + body: JSON.stringify({ + message: 'insufficientLoa', + }), + }); + } return Promise.resolve({ body: JSON.stringify(response) }); }); const queue = [ @@ -163,6 +173,19 @@ describe('deleteServiceConnection.ts', () => { ).toBeFalsy(); expect(isSuccessResult({ result } as ActionResults)).toBeFalsy(); }); + it('Insufficient loa returns error', async () => { + const { runner, getAction } = initTests({ + returnInsufficientLoa: true, + returnNoData: true, + }); + const [errorMessage] = await to( + getAction().executor(getAction(), runner) + ); + + expect( + isInsufficientLoaResult(({ errorMessage } as unknown) as ActionResults) + ).toBeTruthy(); + }); it('Result should not be stored to sessionStorage', async () => { const { getAction } = initTests(); expect(getOption(getAction(), 'noStorage')).toBeTruthy(); diff --git a/src/gdprApi/actions/__tests__/getDownloadData.test.ts b/src/gdprApi/actions/__tests__/getDownloadData.test.ts index f59d3e1fc..10c43c35a 100644 --- a/src/gdprApi/actions/__tests__/getDownloadData.test.ts +++ b/src/gdprApi/actions/__tests__/getDownloadData.test.ts @@ -3,7 +3,8 @@ import { waitFor } from '@testing-library/react'; import { getDownloadDataAction, - getDownloadDataResult, + getDownloadDataResultOrError, + isInsufficientLoaResult, } from '../getDownloadData'; import { createActionQueueRunner } from '../../../common/actionQueue/actionQueueRunner'; import { Action, getOption } from '../../../common/actionQueue/actionQueue'; @@ -13,6 +14,8 @@ import { } from '../authCodeParser'; import { getMockCalls } from '../../../common/test/mockHelper'; +type ActionResults = ReturnType; + describe('getDownloadData.ts', () => { const queryTracker = vi.fn(); const successfulResponse = { variable1: 'variable1' }; @@ -22,11 +25,13 @@ describe('getDownloadData.ts', () => { noKeycloadAuthCode, returnNoData, returnError, + returnInsufficientLoa, }: { noKeycloadAuthCode?: boolean; noTunnistamoAuthCode?: boolean; returnNoData?: boolean; returnError?: boolean; + returnInsufficientLoa?: boolean; } = {}) => { fetchMock.mockIf(/.*\/graphql\/.*$/, async (req: Request) => { const payload = await req.json(); @@ -44,6 +49,13 @@ describe('getDownloadData.ts', () => { }), }); } + if (returnInsufficientLoa === true) { + return Promise.reject({ + body: JSON.stringify({ + message: 'insufficientLoa', + }), + }); + } return Promise.resolve({ body: JSON.stringify(response) }); }); const queue = [ @@ -116,6 +128,21 @@ describe('getDownloadData.ts', () => { const [error] = await to(getAction().executor(getAction(), runner)); expect(error).toBeDefined(); }); + it('Insufficient loa returns error', async () => { + const { runner, getAction } = initTests({ + returnInsufficientLoa: true, + returnNoData: true, + }); + const [errorMessage] = await to( + getAction().executor(getAction(), runner) + ); + + expect( + isInsufficientLoaResult(({ + errorMessage, + } as unknown) as ActionResults) + ).toBeTruthy(); + }); it('Result should not be stored to sessionStorage', async () => { const { getAction } = initTests(); expect(getOption(getAction(), 'noStorage')).toBeTruthy(); @@ -126,7 +153,9 @@ describe('getDownloadData.ts', () => { await waitFor(() => { expect(runner.isFinished()).toBeTruthy(); }); - expect(getDownloadDataResult(runner)).toMatchObject(successfulResponse); + expect(getDownloadDataResultOrError(runner).result).toMatchObject( + successfulResponse + ); }); }); }); diff --git a/src/gdprApi/actions/deleteProfile.ts b/src/gdprApi/actions/deleteProfile.ts index bb24db7ea..be9374b89 100644 --- a/src/gdprApi/actions/deleteProfile.ts +++ b/src/gdprApi/actions/deleteProfile.ts @@ -25,14 +25,27 @@ import parseDeleteProfileResult, { import { convertStringToTranslationLanguage } from '../../profile/helpers/createServiceConnectionsQueryVariables'; import reportErrorsToSentry from '../../common/sentry/reportErrorsToSentry'; import DELETE_PROFILE from '../graphql/GdprDeleteMyProfileMutation.graphql'; +import parseGraphQLError from '../../profile/helpers/parseGraphQLError'; export const deleteProfileType = 'deleteProfile'; -export const getDeleteProfileResult = (queueController: QueueController) => - getActionResultAndErrorMessage( +type DeleteProfileResult = keyof typeof resultTypes; + +const resultTypes = { + insufficientLoa: 'insufficientLoa', +} as const; + +export const getDeleteProfileResultOrError = ( + queueController: QueueController +) => + getActionResultAndErrorMessage( deleteProfileType, queueController - ).result; + ); + +export const isInsufficientLoaResult = ( + resultOrError: ReturnType +) => resultOrError.errorMessage === resultTypes.insufficientLoa; const deleteProfileExecutor: ActionExecutor = async ( action, @@ -66,6 +79,11 @@ const deleteProfileExecutor: ActionExecutor = async ( ); if (error) { reportErrorsToSentry(error); + + if (parseGraphQLError(error).isInsufficientLoaError) { + return Promise.reject(resultTypes.insufficientLoa); + } + return Promise.reject(error); } if (!result || !result.data) { diff --git a/src/gdprApi/actions/deleteServiceConnection.ts b/src/gdprApi/actions/deleteServiceConnection.ts index 776cf6726..855e4d7c2 100644 --- a/src/gdprApi/actions/deleteServiceConnection.ts +++ b/src/gdprApi/actions/deleteServiceConnection.ts @@ -20,6 +20,7 @@ import { } from './authCodeParser'; import reportErrorsToSentry from '../../common/sentry/reportErrorsToSentry'; import DELETE_SERVICE_DATA from '../graphql/GdprDeleteServiceDataMutation.graphql'; +import parseGraphQLError from '../../profile/helpers/parseGraphQLError'; export const deleteServiceConnectionType = 'deleteServiceConnection'; @@ -30,6 +31,7 @@ const resultTypes = { forbidden: 'forbidden', queryError: 'queryError', noAuthCodes: 'noAuthCodes', + insufficientLoa: 'insufficientLoa', } as const; export const getDeleteServiceConnectionResultOrError = ( @@ -44,6 +46,10 @@ export const isForbiddenResult = ( resultOrError: ReturnType ) => resultOrError.errorMessage === resultTypes.forbidden; +export const isInsufficientLoaResult = ( + resultOrError: ReturnType +) => resultOrError.errorMessage === resultTypes.insufficientLoa; + export const isSuccessResult = ( resultOrError: ReturnType ) => resultOrError.result === resultTypes.success; @@ -78,8 +84,14 @@ const deleteServiceConnectionExecutor: ActionExecutor = async ( }, }) ); + if (error) { reportErrorsToSentry(error); + + if (parseGraphQLError(error).isInsufficientLoaError) { + return Promise.reject(resultTypes.insufficientLoa); + } + return Promise.reject(resultTypes.queryError); } diff --git a/src/gdprApi/actions/downloadAsFile.ts b/src/gdprApi/actions/downloadAsFile.ts index e67fb55b3..53b1e260c 100644 --- a/src/gdprApi/actions/downloadAsFile.ts +++ b/src/gdprApi/actions/downloadAsFile.ts @@ -4,7 +4,7 @@ import { ActionExecutor, ActionProps, } from '../../common/actionQueue/actionQueue'; -import { getDownloadDataResult } from './getDownloadData'; +import { getDownloadDataResultOrError } from './getDownloadData'; const downloadAsFile = 'downloadAsFile'; @@ -12,7 +12,7 @@ const downloadAsFileExecutor: ActionExecutor = async ( action, queueController ) => { - const data = getDownloadDataResult(queueController); + const data = getDownloadDataResultOrError(queueController).result; if (!data) { return Promise.reject('No profile data'); } else { diff --git a/src/gdprApi/actions/getDownloadData.ts b/src/gdprApi/actions/getDownloadData.ts index 1cf5e2bea..22b19b426 100644 --- a/src/gdprApi/actions/getDownloadData.ts +++ b/src/gdprApi/actions/getDownloadData.ts @@ -18,12 +18,27 @@ import { } from './authCodeParser'; import reportErrorsToSentry from '../../common/sentry/reportErrorsToSentry'; import DOWNLOAD_MY_PROFILE from '../../profile/graphql/DownloadMyProfileQuery.graphql'; +import parseGraphQLError from '../../profile/helpers/parseGraphQLError'; const downloadDataType = 'downloadData'; -export const getDownloadDataResult = (queueController: QueueController) => - getActionResultAndErrorMessage(downloadDataType, queueController) - .result; +type DownloadDataResult = keyof typeof resultTypes; + +const resultTypes = { + insufficientLoa: 'insufficientLoa', +} as const; + +export const getDownloadDataResultOrError = ( + queueController: QueueController +) => + getActionResultAndErrorMessage( + downloadDataType, + queueController + ); + +export const isInsufficientLoaResult = ( + resultOrError: ReturnType +) => resultOrError.errorMessage === resultTypes.insufficientLoa; const getDownloadDataExecutor: ActionExecutor = async ( action, @@ -51,6 +66,11 @@ const getDownloadDataExecutor: ActionExecutor = async ( ); if (error) { reportErrorsToSentry(error); + + if (parseGraphQLError(error).isInsufficientLoaError) { + return Promise.reject(resultTypes.insufficientLoa); + } + return Promise.reject(error); } if (!result || !result.data) { diff --git a/src/i18n/en.json b/src/i18n/en.json index be2c9736f..5831db1c0 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -56,8 +56,7 @@ "deleteServiceFromPage": "You can delete the data from these services via the {{linkToServicesText}} page.", "unableToDeleteServices": "We cannot delete data from these services, for example due to the transaction status or storage time.", "contactServiceToDelete": "If you still want to delete the data, please contact the service directly and then retry deleting the profile.", - "urlToServiceList": "https://www.hel.fi/search/services", - "explanationforLightAuthentication": "You can only delete data from this service if you are strongly identified. Please log out and log in again using Suomi.fi-identification to delete your data. " + "urlToServiceList": "https://www.hel.fi/search/services" }, "downloadData": { "button": "Download my information", @@ -234,7 +233,8 @@ "connectionRemovalVerificationTitle": "Are you sure you want to delete your data?", "connectionRemovalVerificationButtonText": "Delete data", "connectionRemovalError": "For some reason, the deletion was not successful. Please, try again later!", - "contactServiceToDelete": "If you still want to delete the data, please contact the service directly and then retry deleting the data." + "contactServiceToDelete": "If you still want to delete the data, please contact the service directly and then retry deleting the data.", + "explanationforLightAuthentication": "You can only delete data from this service if you are strongly identified. Please log out and log in again using Suomi.fi-identification to delete your data." }, "skipToContent": "Skip to content", "validation": { @@ -282,4 +282,4 @@ "changeProfilePassword ": { "explanationForStrongAuthentication": "You can only change your password if you are strongly identified. Log out and log in again using Suomi.fi-authentication to change your password." } -} \ No newline at end of file +} diff --git a/src/i18n/fi.json b/src/i18n/fi.json index c4fc6e54d..ed5e0d1b6 100644 --- a/src/i18n/fi.json +++ b/src/i18n/fi.json @@ -31,7 +31,7 @@ "continue": "Jatka" }, "createProfile": { - "heading": "Täydennä tietosi luodaksesi profiili", + "heading": "Täydennä tietosi luodaksesi profiilin", "helpText": "Luomalla profiilin voit hallinnoida omia tietojasi yhdestä paikasta.", "pageTitle": "Uusi profiili" }, @@ -43,7 +43,7 @@ "loadingServices": "Haetaan profiiliin yhdistettyjä palveluita", "deleteSuccessful": "Helsinki-profiili ja kaikki sinusta tallentamamme tiedot on poistettu onnistuneesti!", "deleteFailed": "Jostain syystä poisto ei onnistunut. Kokeile hetken kuluttua uudelleen!", - "deleteInfoforLightAuthentication": "You have services linked to your profile for which you have used strong identification. Please log out and log in again using Suomi.fi-authentication to delete your data. " + "deleteInfoforLightAuthentication": "Profiilissasi on yhdistettynä palveluita joihin olet käyttänyt vahvaa tunnistautumista. Kirjaudu ulos ja takaisin sisään Suomi.fi-tunnistautuen poistaaksesi tiedot." }, "deleteProfileModal": { "delete": "Poista omat tiedot", @@ -56,14 +56,13 @@ "deleteServiceFromPage": "Voit poistaa tiedot näistä palveluista {{linkToServicesText}} -sivulta.", "unableToDeleteServices": "Emme voi poistaa tietoja näistä palveluista esimerkiksi asioinnin tilan tai säilytysajan vuoksi.", "contactServiceToDelete": "Jos haluat silti poistaa tiedot, ole suoraan yhteydessä kyseiseen palveluun ja yritä profiilin poistoa sen jälkeen uudestaan.", - "urlToServiceList": "https://www.hel.fi/search/services", - "explanationforLightAuthentication": "Voit poistaa tietoja tästä palvelusta vain, jos olet vahvasti tunnistautunut. Kirjaudu ulos ja takaisin sisään käyttäen Suomi.fi-tunnistautumista tietojen poistamiseksi. " + "urlToServiceList": "https://www.hel.fi/search/services" }, "downloadData": { "button": "Lataa omat tiedot", "panelText": "Voit ladata kaikki sinuun liittyvät tiedot tästä tiedostona. Tiedosto pitää sisällään kaikki sinusta Helsinki-profiiliin tallennetut tiedot sekä tiedot kaikista siihen liitetyistä asiointipalveluista.", "panelTitle": "Haluatko ladata omat tietosi?", - "extrapanelTextforLightAuthentication": "Profiilissasi on yhdistettynä palveluita joihin olet käyttänyt vahvaa tunnistautumista.Kirjaudu ulos ja takaisin sisään Suomi.fi-tunnistautuen ladataksesi tiedot." + "extrapanelTextforLightAuthentication": "Profiilissasi on yhdistettynä palveluita joihin olet käyttänyt vahvaa tunnistautumista. Kirjaudu ulos ja takaisin sisään Suomi.fi-tunnistautuen ladataksesi tiedot." }, "expandingPanel": { "closeButtonText": "Sulje", @@ -234,7 +233,8 @@ "connectionRemovalVerificationTitle": "Haluatko varmasti poistaa tietosi?", "connectionRemovalVerificationButtonText": "Poista tiedot", "connectionRemovalError": "Jostain syystä poisto ei onnistunut. Yritä hetken kuluttua uudelleen!", - "contactServiceToDelete": "Jos haluat silti poistaa tiedot, ole suoraan yhteydessä kyseiseen palveluun ja yritä tietojen poistoa sen jälkeen uudestaan." + "contactServiceToDelete": "Jos haluat silti poistaa tiedot, ole suoraan yhteydessä kyseiseen palveluun ja yritä tietojen poistoa sen jälkeen uudestaan.", + "explanationforLightAuthentication": "Voit poistaa tietoja tästä palvelusta vain, jos olet vahvasti tunnistautunut. Kirjaudu ulos ja takaisin sisään käyttäen Suomi.fi-tunnistautumista tietojen poistamiseksi." }, "skipToContent": "Siirry suoraan sisältöön", "validation": { @@ -282,4 +282,4 @@ "changeProfilePassword ": { "explanationForStrongAuthentication": "Voit vaihtaa salasanan vain, jos olet vahvasti tunnistautunut. Kirjaudu ulos ja takaisin sisään käyttäen Suomi.fi-tunnistautumista vaihtaaksesi salasanasi." } -} \ No newline at end of file +} diff --git a/src/i18n/sv.json b/src/i18n/sv.json index 37f5ec34a..dc5473359 100644 --- a/src/i18n/sv.json +++ b/src/i18n/sv.json @@ -38,7 +38,7 @@ "deleteProfile": { "accept": "Jag förstår vad att radera min information betyder och vill fortsätta.", "delete": "Radera min information", - "explanation": "Om du vill, kan du ta bort din Helsingforsprofil och alla uppgifter som vi har sparat om dig. Uppgifterna kan inte återställas efter att de har tagits bort. Vi rekommenderar därför att du laddar ner dina egna data innan du raderar din profil och dina egna data.

Efter borttagningen av profilen blir du automatiskt utloggad från denna tjänst.

Om du har loggat in med ett konto hos en utomstående tjänsteleverantör (t.ex. Google eller Facebook), raderas inte kontot i fråga.
För att kunna använda Helsingfors stads tjänster i framtiden måste du skapa en ny Helsingforsprofil.", + "explanation": "Om du vill kan du radera din Helsingforsprofil och all information som vi har om dig. När dina uppgifter har raderats kan de inte återställas. Vi rekommenderar därför att du laddar ner dina egna data innan du raderar din profil och dina egna data.

Efter borttagningen av profilen blir du automatiskt utloggad från denna tjänst.

Om du har loggat in med ett konto hos en utomstående tjänsteleverantör (t.ex. Google eller Facebook), raderas inte kontot i fråga.
För att kunna använda Helsingfors stads tjänster i framtiden måste du skapa en ny Helsingforsprofil.", "title": "Vill du radera din information?", "loadingServices": "Söker efter tjänster som är kopplade till profilen", "deleteSuccessful": "Helsingforsprofilen och alla uppgifter som vi har sparat om dig har tagits bort på ett lyckat sätt.", @@ -56,8 +56,7 @@ "deleteServiceFromPage": "Du kan ta bort uppgifterna från dessa tjänster på sidan {{linkToServicesText}}.", "unableToDeleteServices": "Vi kan inte radera data från dessa tjänster, till exempel på grund av transaktionsstatus eller lagringstid.", "contactServiceToDelete": "Om du ändå vill ta bort uppgifterna, kontakta tjänsten i fråga direkt och försök därefter att ta bort profilen på nytt.", - "urlToServiceList": "https://www.hel.fi/sok/tjanster", - "explanationforLightAuthentication": "Du kan bara radera data från den här tjänsten om du är starkt identifierad. Logga ut och logga in igen med Suomi.fi-autentisering för att radera dina uppgifter." + "urlToServiceList": "https://www.hel.fi/sok/tjanster" }, "downloadData": { "button": "Ladda ner min information", @@ -71,7 +70,7 @@ "showInformation": "Visa information" }, "footer": { - "about": "Om Helsingfors-profilen", + "about": "Om Helsingforsprofilen", "accessibility": "Tillgänglighetsdokument", "copyright": "Upphovsrätt {{year}}", "feedback": "Ge feedback", @@ -97,7 +96,7 @@ }, "loading": "Laddar...", "login": { - "description": "Genom att logga in på de nya Helsingfors stadstjänster skapas en Helsingforsprofil för dig. Här kan du enkelt hitta din profilinformation och se alla tjänster du har loggat in med den.", + "description": "Genom att logga in på de nya Helsingfors stadstjänster skapas Helsingforsprofilen för dig. Här kan du enkelt hitta din profilinformation och se alla tjänster du har loggat in med den.", "login": "Logga in", "title": "Din profilinformation på en adress!" }, @@ -173,7 +172,7 @@ }, "profileInformation": { "address": "Adress", - "addressDescription": "Du har själv lagt dessa uppgifter till Helsingforsprofilen eller någon annan av Helsingfors stads e-tjänster. Vi använder denna adressuppgift när du har använt enkel identifiering, det vill säga någon annan än Suomi.fi-identifiering. Om du vill kan du ta bort uppgifterna från Helsingforsprofilen, varvid de också raderas från de övriga e-tjänsterna. Ta inte bort uppgifter om du har ett pågående ärende.", + "addressDescription": "Du har själv lagt dessa uppgifter till Helsingforsprofilen eller någon annan av Helsingfors stads e-tjänster. Vi använder denna adressuppgift när du har använt enkel identifiering, det vill säga någon annan än Suomi.fi-identifiering. Om du vill kan du radera uppgifterna från Helsingforsprofilen, varvid de också raderas från de övriga e-tjänsterna. Radera inte uppgifter om du har ett pågående ärende.", "addressDescriptionNoWeakAddress": "Du har inte lagt till någon annan adress. Vi använder den andra adressuppgiften när du har använt enkel identifiering, det vill säga någon annan än Suomi.fi-identifiering.", "addressDescriptionNoAddress": "Du har inte lagt till en adress. Vi använder denna adressuppgift när du har använt enkel identifiering, det vill säga någon annan än Suomi.fi-identifiering.", "addressTitleWhenHasVerifiedData": "Annan adress", @@ -184,7 +183,7 @@ "ariaShowOptions": "Visa alternativ", "ariaSelectedOption": "{{value}} har valts", "ariaNoSelectedItemForLabel": "{{label}} har inte valts", - "deleteProfile": "Radera profil", + "deleteProfile": "Radera profilen", "description": "Uppgifterna som sparats i Helsingforsprofilen används i Helsingfors stads e-tjänster. Närmare information hittar du på {{linkToServicesText}} sidan.", "downloadData": "Ladda ner min information", "email": "Epost", @@ -196,7 +195,7 @@ "authenticationMethod": "Autentiseringsmetod", "permanentAddress": "Stadigvarande adress", "permanentForeignAddress": "Stadigvarande utländsk adress", - "verifiedDataInformation": "Du ser de officiella uppgifterna endast när du loggat in med stark identifiering, det vill säga Suomi-fi-identifieringen. Uppgifterna har hämtats från det officiella Befolkningsdatasystemet och de kan inte tas bort via Helsingforsprofilen. Du kan se uppgifterna om dig i befolkningsdatasystemet i webbtjänsten suomi.fi.", + "verifiedDataInformation": "Du ser de officiella uppgifterna endast när du loggat in med stark identifiering, det vill säga Suomi-fi-identifieringen. Uppgifterna har hämtats från det officiella befolkningsdatasystemet och kan inte redigeras via Helsingforsprofilen. Du kan se uppgifterna om dig i befolkningsdatasystemet i webbtjänsten suomi.fi.", "verifiedDataInformationLink": "https://www.suomi.fi/", "verifiedData": "Verifierad uppgift", "verifiedBasicData": "Officiella uppgifter", @@ -234,7 +233,8 @@ "connectionRemovalVerificationTitle": "Är du säker på att du vill radera data", "connectionRemovalVerificationButtonText": "Radera data", "connectionRemovalError": "Raderingen misslyckades av någon anledning. Försök igen senare!", - "contactServiceToDelete": "Om du ändå vill ta bort uppgifterna, kontakta tjänsten och försök att radera uppgifterna igen efteråt." + "contactServiceToDelete": "Om du ändå vill ta bort uppgifterna, kontakta tjänsten och försök att radera uppgifterna igen efteråt.", + "explanationforLightAuthentication": "Du kan bara radera data från den här tjänsten om du är starkt identifierad. Logga ut och logga in igen med Suomi.fi-autentisering för att radera dina uppgifter." }, "skipToContent": "Hoppa till innehåll", "validation": { @@ -259,7 +259,7 @@ "openInNewTabAriaLabel": "Öppnas i en ny flik.", "openInExternalDomainAriaLabel": "Gå till en annan webbplats.", "accessibilityStatement": "Tillgänglighetsutlåtande", - "aboutPage": "Om Helsingfors-profilen", + "aboutPage": "Om Helsingforsprofilen", "cityOfHelsinki": "Helsingfors stad", "pageNotFoundTitle": "Vi kunde inte hitta webbplatsen.", "pageNotFoundText": "Kontrollera att URL-adressen är rätt skriven (kontrollera stora och små bokstäver och skiljetecken). Eller gå tillbaka till föregående sida genom att klicka på tangenten Föregående i webbläsaren. Du kan också gå tillbaka till första sidan via länken här nedan.", diff --git a/src/profile/components/deleteProfile/DeleteProfile.tsx b/src/profile/components/deleteProfile/DeleteProfile.tsx index b7032b351..71be7f86c 100644 --- a/src/profile/components/deleteProfile/DeleteProfile.tsx +++ b/src/profile/components/deleteProfile/DeleteProfile.tsx @@ -25,7 +25,7 @@ import useAuthCodeQueues, { AuthCodeQueuesProps, } from '../../../gdprApi/useAuthCodeQueues'; import config from '../../../config'; -import { getDeleteProfileResult } from '../../../gdprApi/actions/deleteProfile'; +import { getDeleteProfileResultOrError } from '../../../gdprApi/actions/deleteProfile'; import reportErrorsToSentry from '../../../common/sentry/reportErrorsToSentry'; import SERVICE_CONNECTIONS from '../../graphql/ServiceConnectionsQuery.graphql'; @@ -49,7 +49,9 @@ function DeleteProfile(): React.ReactElement { >(undefined); const onCompleted: AuthCodeQueuesProps['onCompleted'] = useCallback( controller => { - const { failures, successful } = getDeleteProfileResult(controller) || { + const { failures, successful } = (getDeleteProfileResultOrError( + controller + ).result as DeleteResultLists) || { failures: [], successful: [], }; @@ -65,6 +67,7 @@ function DeleteProfile(): React.ReactElement { const onError: AuthCodeQueuesProps['onError'] = useCallback(controller => { const failed = controller.getFailed(); const error = new Error(failed ? failed.errorMessage : 'Unknown error'); + if (error) { Sentry.captureException(error); } @@ -213,6 +216,7 @@ function DeleteProfile(): React.ReactElement { )} ', () => { const errorDescriptionSelector: ElementSelector = { testId: 'delete-profile-generic-error', }; + + const errorLoaDescriptionSelector: ElementSelector = { + testId: 'delete-profile-insufficient-loa', + }; + const serviceConnectionsPageLinkSelector: ElementSelector = { testId: 'delete-profile-service-connections-page-link', }; @@ -236,4 +242,22 @@ describe(' ', () => { await waitForElement(errorDescriptionSelector); }); }); + it(`When deletion fails because of insufficient loa, error message is shown`, async () => { + initQueueAndLocationForResume({ + error: true, + overrides: [ + { + type: deleteProfileType, + rejectValue: new Error('insufficientLoa'), + }, + ], + }); + + await act(async () => { + const testTools = await initTests(1); + const { waitForElement } = testTools; + + await waitForElement(errorLoaDescriptionSelector); + }); + }); }); diff --git a/src/profile/components/downloadData/DownloadData.tsx b/src/profile/components/downloadData/DownloadData.tsx index a846d432e..8e541fd32 100644 --- a/src/profile/components/downloadData/DownloadData.tsx +++ b/src/profile/components/downloadData/DownloadData.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Notification } from 'hds-react'; @@ -6,10 +6,22 @@ import commonFormStyles from '../../../common/cssHelpers/form.module.css'; import contentStyles from '../../../common/cssHelpers/content.module.css'; import ProfileSection from '../../../common/profileSection/ProfileSection'; import { useScrollIntoView } from '../../hooks/useScrollIntoView'; -import useAuthCodeQueues from '../../../gdprApi/useAuthCodeQueues'; +import useAuthCodeQueues, { + AuthCodeQueuesProps, +} from '../../../gdprApi/useAuthCodeQueues'; import config from '../../../config'; +import { isInsufficientLoaResult } from '../../../gdprApi/actions/getDownloadData'; function DownloadData(): React.ReactElement { + const [errorMessage, setErrorMessage] = useState(); + + const onError: AuthCodeQueuesProps['onError'] = useCallback(controller => { + const failed = controller.getFailed(); + const message = (failed && failed.errorMessage) || 'unknown'; + + setErrorMessage(message); + }, []); + const { startOrRestart, canStart, @@ -21,6 +33,7 @@ function DownloadData(): React.ReactElement { } = useAuthCodeQueues({ startPagePath: config.downloadPath, queueName: 'downloadProfile', + onError, }); const canUserDoSomething = canStart() || shouldRestart(); const { t } = useTranslation(); @@ -41,13 +54,26 @@ function DownloadData(): React.ReactElement {
- {hasError && ( - - )} + {hasError && + (isInsufficientLoaResult({ errorMessage, result: undefined }) ? ( + + {t('downloadData.extrapanelTextforLightAuthentication')} + + ) : ( + + {t('notification.defaultErrorText')} + + ))} - )} {closeButtonText !== '' && closeButtonLabelText && ( + )} )} diff --git a/src/profile/components/modals/deleteProfileError/DeleteProfileError.tsx b/src/profile/components/modals/deleteProfileError/DeleteProfileError.tsx index 1e97c45c9..c3f6cd233 100644 --- a/src/profile/components/modals/deleteProfileError/DeleteProfileError.tsx +++ b/src/profile/components/modals/deleteProfileError/DeleteProfileError.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import { Button, Dialog, DialogProps, IconAlertCircle } from 'hds-react'; +import { Button, Dialog, DialogProps, IconError } from 'hds-react'; import { useTranslation } from 'react-i18next'; import { ApolloError } from '@apollo/client'; import { getModalProps } from '../getModalProps'; import { DeleteResultLists } from '../../../helpers/parseDeleteProfileResult'; import DeleteFailureList from '../../deleteProfile/DeleteFailureList'; +import { isInsufficientLoaResult } from '../../../../gdprApi/actions/deleteProfile'; export type Props = { error?: ApolloError | Error | DeleteResultLists; @@ -23,6 +24,10 @@ function DeleteProfileError({ } const failureList = (error as DeleteResultLists).failures || []; const errorIsListOfServices = !!failureList.length; + const errorIsInsufficientLoa = isInsufficientLoaResult({ + errorMessage: (error as Error).message, + result: undefined, + }); const id = 'delete-profile-error-modal'; const closeButtonText = t('notification.closeButtonText'); const { @@ -45,6 +50,7 @@ function DeleteProfileError({ return (