From dac1ddb56cbdf605dcc08de721eeaaa8bdf7ab76 Mon Sep 17 00:00:00 2001 From: mikkojamG Date: Mon, 15 Apr 2024 13:12:57 +0300 Subject: [PATCH 1/4] feat: show insufficient loa error on remove service connection and delete profile HP-2268 --- .../actions/__tests__/deleteProfile.test.ts | 30 +++++++++++++++++-- .../__tests__/deleteServiceConnection.test.ts | 23 ++++++++++++++ src/gdprApi/actions/deleteProfile.ts | 24 +++++++++++++-- .../actions/deleteServiceConnection.ts | 12 ++++++++ src/i18n/en.json | 4 ++- src/i18n/fi.json | 4 ++- src/i18n/sv.json | 4 ++- .../deleteProfile/DeleteProfile.tsx | 7 +++-- .../__tests__/DeleteProfile.test.tsx | 24 +++++++++++++++ .../deleteProfileError/DeleteProfileError.tsx | 9 ++++++ .../DeleteServiceConnectionModal.tsx | 18 +++++++++-- .../ServiceConnectionRemover.tsx | 21 +++++++++---- .../ServiceConnectionRemover.test.tsx | 28 +++++++++++++++-- src/profile/helpers/parseGraphQLError.ts | 14 +++++++-- 14 files changed, 200 insertions(+), 22 deletions(-) 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/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/i18n/en.json b/src/i18n/en.json index be2c9736f..d0ae97cea 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -43,6 +43,7 @@ "loadingServices": "Searching for services connected to the profile", "deleteSuccessful": "The Helsinki profile and all the data we hold on you have been deleted successfully!", "deleteFailed": "The deletion failed for some reason. Please try again after a while!", + "deleteInsufficientLoa": "Profiilissasi on yhdistettyjä palveluita joihin olet käyttänyt vahvaa tunnistautumista. Kirjaudu ulos ja takaisin sisään käyttäen Suomi.fi-tunnistautumista tietojen poistamiseksi.", "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. " }, "deleteProfileModal": { @@ -231,6 +232,7 @@ "connectionRemovalVerification": "By continuing, you will lose all the data stored in the {{serviceName}} service and will no longer be able to use the service until you re-register to the service.", "connectionRemovalSuccess": "All the data we had stored in the {{serviceName}} service has been deleted successfully!", "connectionRemovalForbidden": "We cannot delete data from this service, for example due to the transaction status or storage time.", + "connectionInsufficientLoa": "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.", "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!", @@ -282,4 +284,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..2ce4848e3 100644 --- a/src/i18n/fi.json +++ b/src/i18n/fi.json @@ -43,6 +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!", + "deleteInsufficientLoa": "Profiilissasi on yhdistettyjä palveluita joihin olet käyttänyt vahvaa tunnistautumista. Kirjaudu ulos ja takaisin sisään käyttäen Suomi.fi-tunnistautumista tietojen poistamiseksi.", "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. " }, "deleteProfileModal": { @@ -231,6 +232,7 @@ "connectionRemovalVerification": "Jatkamalla menetät kaikki {{serviceName}} -palveluun tallennetut tiedot, etkä voi enää käyttää palvelua, ennen kuin rekisteröidyt palveluun uudelleen.", "connectionRemovalSuccess": "Kaikki {{serviceName}} -palveluun tallentamamme tiedot on poistettu onnistuneesti!", "connectionRemovalForbidden": "Emme voi poistaa tietoja tästä palvelusta esimerkiksi asioinnin tilan tai säilytysajan vuoksi.", + "connectionInsufficientLoa": "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.", "connectionRemovalVerificationTitle": "Haluatko varmasti poistaa tietosi?", "connectionRemovalVerificationButtonText": "Poista tiedot", "connectionRemovalError": "Jostain syystä poisto ei onnistunut. Yritä hetken kuluttua uudelleen!", @@ -282,4 +284,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..5514878fd 100644 --- a/src/i18n/sv.json +++ b/src/i18n/sv.json @@ -43,6 +43,7 @@ "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.", "deleteFailed": "Av någon anledning lyckades borttagningen inte. Försök på nytt om en stund!", + "deleteInsufficientLoa": "Profiilissasi on yhdistettyjä palveluita joihin olet käyttänyt vahvaa tunnistautumista. Kirjaudu ulos ja takaisin sisään käyttäen Suomi.fi-tunnistautumista tietojen poistamiseksi.", "deleteInfoforLightAuthentication": "Du har tjänster kopplade till din Helsingforsprofil för vilka du har använt stark autentisering. Logga ut och logga in igen med Suomi.fi-autentisering för att radera dina uppgifter." }, "deleteProfileModal": { @@ -231,6 +232,7 @@ "connectionRemovalVerification": "Om du fortsätter, förlorar du alla uppgifter som du sparat i tjänsten {{serviceName}} och du kan inte längre använda tjänsten förrän du registrerar dig i tjänsten på nytt.", "connectionRemovalSuccess": "Alla uppgifter som vi sparat i tjänsten {{serviceName}} har tagits bort på ett lyckat sätt!", "connectionRemovalForbidden": "Vi kan inte radera data från denna tjänst, till exempel på grund av transaktionsstatus eller lagringstid.", + "connectionInsufficientLoa": "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.", "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!", @@ -282,4 +284,4 @@ "changeProfilePassword ": { "explanationForStrongAuthentication": "Du kan bara ändra ditt lösenord om du är starkt identifierad. Logga ut och logga in igen med Suomi.fi-autentisering för att ändra ditt lösenord." } -} \ No newline at end of file +} diff --git a/src/profile/components/deleteProfile/DeleteProfile.tsx b/src/profile/components/deleteProfile/DeleteProfile.tsx index b7032b351..599d41b9d 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); } diff --git a/src/profile/components/deleteProfile/__tests__/DeleteProfile.test.tsx b/src/profile/components/deleteProfile/__tests__/DeleteProfile.test.tsx index c8180fcc3..f0741f7a8 100644 --- a/src/profile/components/deleteProfile/__tests__/DeleteProfile.test.tsx +++ b/src/profile/components/deleteProfile/__tests__/DeleteProfile.test.tsx @@ -32,6 +32,7 @@ import { getServiceConnectionsAction } from '../../../../gdprApi/actions/getServ import { defaultRedirectionCatcherActionType } from '../../../../gdprApi/actions/redirectionHandlers'; import { createNextActionParams } from '../../../../gdprApi/actions/utils'; import { tunnistamoAuthCodeRedirectionAction } from '../../../../gdprApi/actions/authCodeRedirectionHandler'; +import { deleteProfileType } from '../../../../gdprApi/actions/deleteProfile'; vi.mock('../../../../gdprApi/actions/queues'); @@ -84,6 +85,11 @@ describe(' ', () => { 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/modals/deleteProfileError/DeleteProfileError.tsx b/src/profile/components/modals/deleteProfileError/DeleteProfileError.tsx index 1e97c45c9..0c5f0ec26 100644 --- a/src/profile/components/modals/deleteProfileError/DeleteProfileError.tsx +++ b/src/profile/components/modals/deleteProfileError/DeleteProfileError.tsx @@ -6,6 +6,7 @@ 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 { @@ -59,6 +64,10 @@ function DeleteProfileError({ {errorIsListOfServices ? ( + ) : errorIsInsufficientLoa ? ( +

+ {t('deleteProfile.deleteInsufficientLoa')} +

) : (

{t('deleteProfile.deleteFailed')} diff --git a/src/profile/components/serviceConnections/DeleteServiceConnectionModal.tsx b/src/profile/components/serviceConnections/DeleteServiceConnectionModal.tsx index 6cc165db2..2fdd46b1a 100644 --- a/src/profile/components/serviceConnections/DeleteServiceConnectionModal.tsx +++ b/src/profile/components/serviceConnections/DeleteServiceConnectionModal.tsx @@ -12,6 +12,7 @@ import { STATUS_ERROR, STATUS_LOADING, STATUS_PENDING_CONFIRMATION, + STATUS_INSUFFICIENT_LOA, } from './ServiceConnectionRemover'; function DeleteServiceConnectionModal(props: { @@ -24,13 +25,17 @@ function DeleteServiceConnectionModal(props: { const { t } = useTranslation(); const cannotDelete = status === STATUS_DELETE_FORBIDDEN; - const hasError = status === STATUS_ERROR || cannotDelete; + const isInsufficientLoa = status === STATUS_INSUFFICIENT_LOA; + const isError = cannotDelete || isInsufficientLoa; + const hasError = status === STATUS_ERROR || isError; const isLoading = status === STATUS_LOADING; const isDone = status === STATUS_DONE; const isPendingUserConfirmation = status === STATUS_PENDING_CONFIRMATION; const isFinished = isDone || hasError; + const shouldShowModal = - isLoading || cannotDelete || isPendingUserConfirmation || isFinished; + isLoading || isError || isPendingUserConfirmation || isFinished; + const getModalTitle = () => { if (hasError) { return t('notification.removeError'); @@ -105,6 +110,15 @@ function DeleteServiceConnectionModal(props: { ); } + + if (isInsufficientLoa) { + return ( +

+ {t('serviceConnections.connectionInsufficientLoa')} +

+ ); + } + return (

{t('serviceConnections.connectionRemovalError')} diff --git a/src/profile/components/serviceConnections/ServiceConnectionRemover.tsx b/src/profile/components/serviceConnections/ServiceConnectionRemover.tsx index 0ffae7301..e415d3cfc 100644 --- a/src/profile/components/serviceConnections/ServiceConnectionRemover.tsx +++ b/src/profile/components/serviceConnections/ServiceConnectionRemover.tsx @@ -6,7 +6,10 @@ import useAuthCodeQueues, { AuthCodeQueuesProps, } from '../../../gdprApi/useAuthCodeQueues'; import config from '../../../config'; -import { isForbiddenResult } from '../../../gdprApi/actions/deleteServiceConnection'; +import { + isForbiddenResult, + isInsufficientLoaResult, +} from '../../../gdprApi/actions/deleteServiceConnection'; export const STATUS_NONE = 0; export const STATUS_PENDING_CONFIRMATION = 1; @@ -16,6 +19,7 @@ export const STATUS_DELETE_FORBIDDEN = 4; export const STATUS_DONE = 5; export const STATUS_ACKNOWLEDGED = 6; export const STATUS_CLOSED = 7; +export const STATUS_INSUFFICIENT_LOA = 8; export type DeletionStatus = | typeof STATUS_NONE @@ -25,7 +29,8 @@ export type DeletionStatus = | typeof STATUS_DELETE_FORBIDDEN | typeof STATUS_DONE | typeof STATUS_CLOSED - | typeof STATUS_ACKNOWLEDGED; + | typeof STATUS_ACKNOWLEDGED + | typeof STATUS_INSUFFICIENT_LOA; function ServiceConnectionRemover(props: { service: ServiceConnectionData; @@ -65,9 +70,15 @@ function ServiceConnectionRemover(props: { const getModalStatus = () => { if (hasError) { - return isForbiddenResult({ errorMessage, result: undefined }) - ? STATUS_DELETE_FORBIDDEN - : STATUS_ERROR; + if (isForbiddenResult({ errorMessage, result: undefined })) { + return STATUS_DELETE_FORBIDDEN; + } + + if (isInsufficientLoaResult({ errorMessage, result: undefined })) { + return STATUS_INSUFFICIENT_LOA; + } + + return STATUS_ERROR; } if (isLoading) { diff --git a/src/profile/components/serviceConnections/__tests__/ServiceConnectionRemover.test.tsx b/src/profile/components/serviceConnections/__tests__/ServiceConnectionRemover.test.tsx index 57525313d..9ae9b6b40 100644 --- a/src/profile/components/serviceConnections/__tests__/ServiceConnectionRemover.test.tsx +++ b/src/profile/components/serviceConnections/__tests__/ServiceConnectionRemover.test.tsx @@ -48,6 +48,8 @@ describe(' ', () => { loadIndicator: 'service-connection-delete-load-indicator', }; + const ERROR_MODAL_TITLE_TEXT = 'notification.removeError'; + const t = i18n.getFixedT('fi'); const getTestId = (key: keyof typeof testIds): ElementSelector => ({ @@ -171,7 +173,7 @@ describe(' ', () => { ); await waitForElement(getTestId('loadIndicator')); - await waitForElement({ text: t('notification.removeError') }); + await waitForElement({ text: t(ERROR_MODAL_TITLE_TEXT) }); await waitForElement({ text: t('serviceConnections.connectionRemovalError'), }); @@ -198,7 +200,7 @@ describe(' ', () => { defaultServiceConnectionData ); - await waitForElement({ text: t('notification.removeError') }); + await waitForElement({ text: t(ERROR_MODAL_TITLE_TEXT) }); await waitForElement({ text: t('serviceConnections.connectionRemovalForbidden'), }); @@ -208,4 +210,26 @@ describe(' ', () => { }); }); }); + + it(`If deletion query succeeds, but result indicates user has insufficient loa, + a error message is shown`, async () => { + initQueueAndLocationForResume({ + overrides: [ + { + type: deleteServiceConnectionType, + rejectValue: 'insufficientLoa', + resolveValue: undefined, + }, + ], + }); + + await act(async () => { + const { waitForElement } = await initTests(defaultServiceConnectionData); + + await waitForElement({ text: t(ERROR_MODAL_TITLE_TEXT) }); + await waitForElement({ + text: t('serviceConnections.connectionInsufficientLoa'), + }); + }); + }); }); diff --git a/src/profile/helpers/parseGraphQLError.ts b/src/profile/helpers/parseGraphQLError.ts index 438b103b9..9d5971449 100644 --- a/src/profile/helpers/parseGraphQLError.ts +++ b/src/profile/helpers/parseGraphQLError.ts @@ -1,7 +1,10 @@ import { ApolloError } from '@apollo/client'; import { GraphQLError } from 'graphql'; -type ParsingResult = { isAllowedError: boolean }; +type ParsingResult = { + isAllowedError: boolean; + isInsufficientLoaError: boolean; +}; function getGraphQLErrors(error: ApolloError | Error): readonly GraphQLError[] { return (error as ApolloError).graphQLErrors || []; @@ -14,9 +17,14 @@ function parseGraphQLError(error: ApolloError | Error): ParsingResult { errorData.extensions?.code === 'PERMISSION_DENIED_ERROR' && errorData.path?.join('.') === 'myProfile.verifiedPersonalInformation' ) { - return { isAllowedError: true }; + return { isAllowedError: true, isInsufficientLoaError: false }; } - return { isAllowedError: false }; + + if (errorData && errorData.extensions?.code === 'INSUFFICIENT_LOA_ERROR') { + return { isAllowedError: false, isInsufficientLoaError: true }; + } + + return { isAllowedError: false, isInsufficientLoaError: true }; } export default parseGraphQLError; From 52a3bff07fe758b1952050ebac8b6ca9eb805dcd Mon Sep 17 00:00:00 2001 From: mikkojamG Date: Thu, 18 Apr 2024 11:16:51 +0300 Subject: [PATCH 2/4] chore: update translations HP-2268 --- src/i18n/en.json | 10 ++++---- src/i18n/fi.json | 16 ++++++------- src/i18n/sv.json | 24 +++++++++---------- .../deleteProfileError/DeleteProfileError.tsx | 2 +- .../DeleteServiceConnectionModal.tsx | 2 +- .../ServiceConnectionRemover.test.tsx | 2 +- 6 files changed, 25 insertions(+), 31 deletions(-) diff --git a/src/i18n/en.json b/src/i18n/en.json index d0ae97cea..98f33a004 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -43,7 +43,6 @@ "loadingServices": "Searching for services connected to the profile", "deleteSuccessful": "The Helsinki profile and all the data we hold on you have been deleted successfully!", "deleteFailed": "The deletion failed for some reason. Please try again after a while!", - "deleteInsufficientLoa": "Profiilissasi on yhdistettyjä palveluita joihin olet käyttänyt vahvaa tunnistautumista. Kirjaudu ulos ja takaisin sisään käyttäen Suomi.fi-tunnistautumista tietojen poistamiseksi.", "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. " }, "deleteProfileModal": { @@ -57,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", @@ -232,11 +230,11 @@ "connectionRemovalVerification": "By continuing, you will lose all the data stored in the {{serviceName}} service and will no longer be able to use the service until you re-register to the service.", "connectionRemovalSuccess": "All the data we had stored in the {{serviceName}} service has been deleted successfully!", "connectionRemovalForbidden": "We cannot delete data from this service, for example due to the transaction status or storage time.", - "connectionInsufficientLoa": "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.", "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": { @@ -284,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 2ce4848e3..f76f75eda 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,8 +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!", - "deleteInsufficientLoa": "Profiilissasi on yhdistettyjä palveluita joihin olet käyttänyt vahvaa tunnistautumista. Kirjaudu ulos ja takaisin sisään käyttäen Suomi.fi-tunnistautumista tietojen poistamiseksi.", - "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", @@ -57,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", @@ -232,11 +230,11 @@ "connectionRemovalVerification": "Jatkamalla menetät kaikki {{serviceName}} -palveluun tallennetut tiedot, etkä voi enää käyttää palvelua, ennen kuin rekisteröidyt palveluun uudelleen.", "connectionRemovalSuccess": "Kaikki {{serviceName}} -palveluun tallentamamme tiedot on poistettu onnistuneesti!", "connectionRemovalForbidden": "Emme voi poistaa tietoja tästä palvelusta esimerkiksi asioinnin tilan tai säilytysajan vuoksi.", - "connectionInsufficientLoa": "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.", "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": { @@ -284,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 5514878fd..dc5473359 100644 --- a/src/i18n/sv.json +++ b/src/i18n/sv.json @@ -38,12 +38,11 @@ "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.", "deleteFailed": "Av någon anledning lyckades borttagningen inte. Försök på nytt om en stund!", - "deleteInsufficientLoa": "Profiilissasi on yhdistettyjä palveluita joihin olet käyttänyt vahvaa tunnistautumista. Kirjaudu ulos ja takaisin sisään käyttäen Suomi.fi-tunnistautumista tietojen poistamiseksi.", "deleteInfoforLightAuthentication": "Du har tjänster kopplade till din Helsingforsprofil för vilka du har använt stark autentisering. Logga ut och logga in igen med Suomi.fi-autentisering för att radera dina uppgifter." }, "deleteProfileModal": { @@ -57,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", @@ -72,7 +70,7 @@ "showInformation": "Visa information" }, "footer": { - "about": "Om Helsingfors-profilen", + "about": "Om Helsingforsprofilen", "accessibility": "Tillgänglighetsdokument", "copyright": "Upphovsrätt {{year}}", "feedback": "Ge feedback", @@ -98,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!" }, @@ -174,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", @@ -185,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", @@ -197,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", @@ -232,11 +230,11 @@ "connectionRemovalVerification": "Om du fortsätter, förlorar du alla uppgifter som du sparat i tjänsten {{serviceName}} och du kan inte längre använda tjänsten förrän du registrerar dig i tjänsten på nytt.", "connectionRemovalSuccess": "Alla uppgifter som vi sparat i tjänsten {{serviceName}} har tagits bort på ett lyckat sätt!", "connectionRemovalForbidden": "Vi kan inte radera data från denna tjänst, till exempel på grund av transaktionsstatus eller lagringstid.", - "connectionInsufficientLoa": "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.", "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": { @@ -261,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.", @@ -284,4 +282,4 @@ "changeProfilePassword ": { "explanationForStrongAuthentication": "Du kan bara ändra ditt lösenord om du är starkt identifierad. Logga ut och logga in igen med Suomi.fi-autentisering för att ändra ditt lösenord." } -} +} \ No newline at end of file diff --git a/src/profile/components/modals/deleteProfileError/DeleteProfileError.tsx b/src/profile/components/modals/deleteProfileError/DeleteProfileError.tsx index 0c5f0ec26..85c537362 100644 --- a/src/profile/components/modals/deleteProfileError/DeleteProfileError.tsx +++ b/src/profile/components/modals/deleteProfileError/DeleteProfileError.tsx @@ -66,7 +66,7 @@ function DeleteProfileError({ ) : errorIsInsufficientLoa ? (

- {t('deleteProfile.deleteInsufficientLoa')} + {t('deleteProfile.deleteInfoforLightAuthentication')}

) : (

diff --git a/src/profile/components/serviceConnections/DeleteServiceConnectionModal.tsx b/src/profile/components/serviceConnections/DeleteServiceConnectionModal.tsx index 2fdd46b1a..e3f2d31a8 100644 --- a/src/profile/components/serviceConnections/DeleteServiceConnectionModal.tsx +++ b/src/profile/components/serviceConnections/DeleteServiceConnectionModal.tsx @@ -114,7 +114,7 @@ function DeleteServiceConnectionModal(props: { if (isInsufficientLoa) { return (

- {t('serviceConnections.connectionInsufficientLoa')} + {t('serviceConnections.explanationforLightAuthentication')}

); } diff --git a/src/profile/components/serviceConnections/__tests__/ServiceConnectionRemover.test.tsx b/src/profile/components/serviceConnections/__tests__/ServiceConnectionRemover.test.tsx index 9ae9b6b40..059f1e43b 100644 --- a/src/profile/components/serviceConnections/__tests__/ServiceConnectionRemover.test.tsx +++ b/src/profile/components/serviceConnections/__tests__/ServiceConnectionRemover.test.tsx @@ -228,7 +228,7 @@ describe(' ', () => { await waitForElement({ text: t(ERROR_MODAL_TITLE_TEXT) }); await waitForElement({ - text: t('serviceConnections.connectionInsufficientLoa'), + text: t('serviceConnections.explanationforLightAuthentication'), }); }); }); From d837e7db55a3af5f19a6e5867875dc62374502bd Mon Sep 17 00:00:00 2001 From: mikkojamG Date: Thu, 18 Apr 2024 11:55:19 +0300 Subject: [PATCH 3/4] feat: show insufficient loa error on download data HP-2268 --- .../actions/__tests__/getDownloadData.test.ts | 33 +++++++++++++- src/gdprApi/actions/downloadAsFile.ts | 4 +- src/gdprApi/actions/getDownloadData.ts | 26 +++++++++-- src/i18n/en.json | 4 +- src/i18n/fi.json | 4 +- .../components/downloadData/DownloadData.tsx | 44 +++++++++++++++---- .../__tests__/DownloadData.test.tsx | 24 ++++++++++ 7 files changed, 119 insertions(+), 20 deletions(-) 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/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 98f33a004..5831db1c0 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -234,7 +234,7 @@ "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.", - "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. " + "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 f76f75eda..ed5e0d1b6 100644 --- a/src/i18n/fi.json +++ b/src/i18n/fi.json @@ -234,7 +234,7 @@ "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.", - "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. " + "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/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')} + + ))} )} void; onConfirm: () => void; title?: string; + variant?: DialogVariant; + hasError?: boolean; content?: React.FC | string; actionButtonText?: string; closeButtonText?: string; @@ -20,6 +30,8 @@ function ConfirmationModal({ onClose, onConfirm, title, + variant = 'primary', + hasError, content, actionButtonText, closeButtonText, @@ -51,6 +63,7 @@ function ConfirmationModal({ return ( diff --git a/src/profile/components/modals/deleteProfileError/DeleteProfileError.tsx b/src/profile/components/modals/deleteProfileError/DeleteProfileError.tsx index 85c537362..c3f6cd233 100644 --- a/src/profile/components/modals/deleteProfileError/DeleteProfileError.tsx +++ b/src/profile/components/modals/deleteProfileError/DeleteProfileError.tsx @@ -1,5 +1,5 @@ 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'; @@ -50,6 +50,7 @@ function DeleteProfileError({ return (