From 8af3129ca76205e3949e4c792e038ff2b37c518c Mon Sep 17 00:00:00 2001 From: Aleix Suau Date: Fri, 22 May 2026 13:19:11 +0200 Subject: [PATCH] IS-11347 Unify completed-with-success and completed-with-error handling Merges both terminal OAuth response steps under a single `handleCompletedStep` with one config knob: `autoRedirectOnAuthenticationComplete`. Default `true` (auto-redirect both); accepts `false`, `'ONLY_ON_SUCCESS'`, `'ONLY_ON_ERROR'` for finer control. Replaces `redirectOnAuthenticationCompletedWithSuccess`. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../feature/stepper/HaapiStepper.spec.tsx | 103 +++++++++++++----- .../feature/stepper/HaapiStepper.tsx | 15 +-- .../stepper/step-handlers/completed-step.ts | 40 +++++++ .../completed-with-success-step.ts | 34 ------ .../haapi-stepper/feature/stepper/typings.ts | 15 +++ .../src/shared/util/api-responses.ts | 19 ++++ 6 files changed, 160 insertions(+), 66 deletions(-) create mode 100644 src/login-web-app/src/haapi-stepper/feature/stepper/step-handlers/completed-step.ts delete mode 100644 src/login-web-app/src/haapi-stepper/feature/stepper/step-handlers/completed-with-success-step.ts create mode 100644 src/login-web-app/src/haapi-stepper/feature/stepper/typings.ts diff --git a/src/login-web-app/src/haapi-stepper/feature/stepper/HaapiStepper.spec.tsx b/src/login-web-app/src/haapi-stepper/feature/stepper/HaapiStepper.spec.tsx index 1b04f134..e8f97007 100644 --- a/src/login-web-app/src/haapi-stepper/feature/stepper/HaapiStepper.spec.tsx +++ b/src/login-web-app/src/haapi-stepper/feature/stepper/HaapiStepper.spec.tsx @@ -16,6 +16,8 @@ import { HTTP_METHODS } from '../../data-access/types/haapi-form.types'; import { MEDIA_TYPES } from '../../../shared/util/types/media.types'; import { authenticationStep, + completedWithErrorStep, + completedWithErrorStepWithoutLinks, completedWithSuccessStep, completedWithSuccessStepWithoutLinks, continueSameStep, @@ -30,6 +32,7 @@ import { useHaapiStepper } from './HaapiStepperHook'; import type { HaapiStepperHistoryEntry, HaapiStepperNextStepAction } from './haapi-stepper.types'; import { HaapiStepperActionStep, HaapiStepperFormAction } from './haapi-stepper.types'; import type { BootstrapConfiguration } from '../../data-access/bootstrap-configuration'; +import { AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE } from './typings'; describe('HaapiStepper', () => { const initialStepType = HAAPI_STEPS.AUTHENTICATION; @@ -372,13 +375,30 @@ describe('HaapiStepper', () => { }); }); - describe('Completed With Success Step', () => { - const authorizationResponseUrl = completedWithSuccessStep.links?.find( - link => link.rel === 'authorization-response' - )?.href; - - describe('redirectOnAuthenticationCompletedWithSuccess enabled (default)', () => { - it('should redirect to the authorization-response URL', async () => { + describe.each([ + { + label: 'success', + stepType: HAAPI_STEPS.COMPLETED_WITH_SUCCESS, + stepFixture: completedWithSuccessStep, + }, + { + label: 'error', + stepType: HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR, + stepFixture: completedWithErrorStep, + }, + ] as const)('Completed With $label Step', ({ label, stepType, stepFixture }) => { + const authorizationResponseUrl = stepFixture.links?.find(link => link.rel === 'authorization-response')?.href; + const redirectSetting = + label === 'success' + ? AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE.ONLY_ON_SUCCESS + : AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE.ONLY_ON_ERROR; + const skipRedirectSetting = + label === 'success' + ? AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE.ONLY_ON_ERROR + : AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE.ONLY_ON_SUCCESS; + + describe('autoRedirectOnAuthenticationComplete', () => { + it('redirects to the authorization-response URL when set to true (default)', async () => { render( @@ -386,65 +406,95 @@ describe('HaapiStepper', () => { ); await screen.findByTestId('step-type'); - await goToNextStep(HAAPI_STEPS.COMPLETED_WITH_SUCCESS); + await goToNextStep(stepType); await waitFor(() => { expect(window.location.href).toBe(authorizationResponseUrl); }); }); - it('should throw error to the error boundary when no authorization-response link exists', async () => { + it(`redirects when set to '${redirectSetting}'`, async () => { render( - + ); await screen.findByTestId('step-type'); - await goToNextStep(HAAPI_STEPS.COMPLETED_WITH_SUCCESS, { noLinks: true }); + await goToNextStep(stepType); await waitFor(() => { - expect(mockThrowErrorToAppErrorBoundary).toHaveBeenCalledWith( - 'redirectOnAuthenticationCompletedWithSuccess is enabled, but the completed-with-success step did not include an authorization-response link.' - ); + expect(window.location.href).toBe(authorizationResponseUrl); }); }); - it('should not update the current step when redirecting', async () => { + it('renders the completed step instead of redirecting when set to false', async () => { render( - + ); await screen.findByTestId('step-type'); - await goToNextStep(HAAPI_STEPS.COMPLETED_WITH_SUCCESS); + await goToNextStep(stepType); await waitFor(() => { - expect(window.location.href).toBe(authorizationResponseUrl); + expect(screen.getByTestId('step-type')).toHaveTextContent(stepType); }); - expect(screen.getByTestId('step-type')).toHaveTextContent(initialStepType); + expect(window.location.href).not.toBe(authorizationResponseUrl); }); - }); - describe('redirectOnAuthenticationCompletedWithSuccess disabled', () => { - it('should render the completed step instead of redirecting', async () => { + it(`renders the completed step instead of redirecting when set to '${skipRedirectSetting}'`, async () => { render( - + ); await screen.findByTestId('step-type'); - await goToNextStep(HAAPI_STEPS.COMPLETED_WITH_SUCCESS); + await goToNextStep(stepType); await waitFor(() => { - expect(screen.getByTestId('step-type')).toHaveTextContent(HAAPI_STEPS.COMPLETED_WITH_SUCCESS); + expect(screen.getByTestId('step-type')).toHaveTextContent(stepType); }); expect(window.location.href).not.toBe(authorizationResponseUrl); }); + + it('throws to the error boundary when redirect is enabled but no authorization-response link exists', async () => { + render( + + + + ); + + await screen.findByTestId('step-type'); + await goToNextStep(stepType, { noLinks: true }); + + await waitFor(() => { + expect(mockThrowErrorToAppErrorBoundary).toHaveBeenCalledWith( + `autoRedirectOnAuthenticationComplete is enabled, but the completed-with-${label} step did not include an authorization-response link.` + ); + }); + }); + + it('does not update the current step when redirecting', async () => { + render( + + + + ); + + await screen.findByTestId('step-type'); + await goToNextStep(stepType); + + await waitFor(() => { + expect(window.location.href).toBe(authorizationResponseUrl); + }); + + expect(screen.getByTestId('step-type')).toHaveTextContent(initialStepType); + }); }); }); }); @@ -810,6 +860,9 @@ function getStepMock(stepType: HAAPI_STEPS | HAAPI_PROBLEM_STEPS, config?: Recor case HAAPI_STEPS.COMPLETED_WITH_SUCCESS: stepMock = config?.noLinks ? completedWithSuccessStepWithoutLinks : completedWithSuccessStep; break; + case HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR: + stepMock = config?.noLinks ? completedWithErrorStepWithoutLinks : completedWithErrorStep; + break; case HAAPI_STEPS.REGISTRATION: stepMock = createRegistrationStep(); break; diff --git a/src/login-web-app/src/haapi-stepper/feature/stepper/HaapiStepper.tsx b/src/login-web-app/src/haapi-stepper/feature/stepper/HaapiStepper.tsx index 840fac7b..fb0bf066 100644 --- a/src/login-web-app/src/haapi-stepper/feature/stepper/HaapiStepper.tsx +++ b/src/login-web-app/src/haapi-stepper/feature/stepper/HaapiStepper.tsx @@ -25,7 +25,8 @@ import { formatContinueSameStepData } from './data-formatters/continue-same-step import { handlePollingStep } from './data-formatters/polling-step'; import { formatErrorStepData } from './data-formatters/problem-step'; import { formatNextStepData } from './data-formatters/format-next-step-data'; -import { handleCompletedWithSuccessStep } from './step-handlers/completed-with-success-step'; +import { handleCompletedStep } from './step-handlers/completed-step'; +import { AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE } from './typings'; import { sendHaapiFetchRequest } from '../../data-access/happi-fetch-request'; import { configuration } from '../../data-access/bootstrap-configuration'; import type { @@ -46,13 +47,13 @@ import { useRefCallback } from '../../util/useRefCallBack'; const DEFAULT_CONFIG: Required = { pollingInterval: 3000, bankIdAutostart: true, - redirectOnAuthenticationCompletedWithSuccess: true, + autoRedirectOnAuthenticationComplete: true, }; export interface HaapiStepperConfig { pollingInterval: number; bankIdAutostart: boolean; - redirectOnAuthenticationCompletedWithSuccess: boolean; + autoRedirectOnAuthenticationComplete: boolean | AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE; } interface HaapiStepperProps { @@ -404,14 +405,10 @@ async function processHaapiNextStep( case HAAPI_STEPS.POLLING: return handlePollingStep(nextStepResponse, pendingOperation, nextStep, config, history); - case HAAPI_STEPS.COMPLETED_WITH_SUCCESS: - return handleCompletedWithSuccessStep(nextStepResponse, config); - case HAAPI_STEPS.AUTHENTICATION: case HAAPI_STEPS.REGISTRATION: case HAAPI_STEPS.USER_CONSENT: case HAAPI_STEPS.CONSENTOR: - case HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR: return { nextStepData: formatNextStepData(nextStepResponse) }; case HAAPI_STEPS.CONTINUE_SAME: @@ -420,6 +417,10 @@ async function processHaapiNextStep( } return { nextStepData: formatContinueSameStepData(action, nextStepResponse, currentStep as HaapiStepperStep) }; + case HAAPI_STEPS.COMPLETED_WITH_SUCCESS: + case HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR: + return handleCompletedStep(nextStepResponse, config); + case HAAPI_PROBLEM_STEPS.INVALID_INPUT: case HAAPI_PROBLEM_STEPS.INCORRECT_CREDENTIALS: case HAAPI_PROBLEM_STEPS.AUTHENTICATION_FAILED: diff --git a/src/login-web-app/src/haapi-stepper/feature/stepper/step-handlers/completed-step.ts b/src/login-web-app/src/haapi-stepper/feature/stepper/step-handlers/completed-step.ts new file mode 100644 index 00000000..796b64f0 --- /dev/null +++ b/src/login-web-app/src/haapi-stepper/feature/stepper/step-handlers/completed-step.ts @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +import { HAAPI_PROBLEM_STEPS, HAAPI_STEPS, type HaapiCompletedStep } from '../../../data-access/types/haapi-step.types'; +import type { HaapiStepperConfig } from '../HaapiStepper'; +import { AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE } from '../typings'; +import { formatNextStepData } from '../data-formatters/format-next-step-data'; + +export function handleCompletedStep(step: HaapiCompletedStep, config: HaapiStepperConfig) { + const isSuccess = step.type === HAAPI_STEPS.COMPLETED_WITH_SUCCESS; + const isError = step.type === HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR; + const setting = config.autoRedirectOnAuthenticationComplete; + const shouldRedirect = + setting === true || + (setting === AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE.ONLY_ON_SUCCESS && isSuccess) || + (setting === AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE.ONLY_ON_ERROR && isError); + + if (!shouldRedirect) { + return { nextStepData: formatNextStepData(step) }; + } + + const redirectHref = step.links?.find(link => link.rel === 'authorization-response')?.href; + + if (!redirectHref) { + throw new Error( + `autoRedirectOnAuthenticationComplete is enabled, but the completed-with-${isSuccess ? 'success' : 'error'} step did not include an authorization-response link.` + ); + } + + window.location.href = redirectHref; + return { nextStepData: undefined }; +} diff --git a/src/login-web-app/src/haapi-stepper/feature/stepper/step-handlers/completed-with-success-step.ts b/src/login-web-app/src/haapi-stepper/feature/stepper/step-handlers/completed-with-success-step.ts deleted file mode 100644 index 5191e855..00000000 --- a/src/login-web-app/src/haapi-stepper/feature/stepper/step-handlers/completed-with-success-step.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2025 Curity AB. All rights reserved. - * - * The contents of this file are the property of Curity AB. - * You may not copy or use this file, in either source code - * or executable form, except in compliance with terms - * set by Curity AB. - * - * For further information, please contact Curity AB. - */ - -import type { HaapiCompletedWithSuccessStep } from '../../../data-access/types/haapi-step.types'; -import type { HaapiStepperConfig } from '../HaapiStepper'; -import { formatNextStepData } from '../data-formatters/format-next-step-data'; - -export function handleCompletedWithSuccessStep( - nextStepResponse: HaapiCompletedWithSuccessStep, - config: HaapiStepperConfig -) { - if (config.redirectOnAuthenticationCompletedWithSuccess) { - const redirectHref = nextStepResponse.links?.find(link => link.rel === 'authorization-response')?.href; - - if (!redirectHref) { - throw new Error( - 'redirectOnAuthenticationCompletedWithSuccess is enabled, but the completed-with-success step did not include an authorization-response link.' - ); - } - - window.location.href = redirectHref; - return { nextStepData: undefined }; - } - - return { nextStepData: formatNextStepData(nextStepResponse) }; -} diff --git a/src/login-web-app/src/haapi-stepper/feature/stepper/typings.ts b/src/login-web-app/src/haapi-stepper/feature/stepper/typings.ts new file mode 100644 index 00000000..174fa583 --- /dev/null +++ b/src/login-web-app/src/haapi-stepper/feature/stepper/typings.ts @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2025 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +export enum AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE { + ONLY_ON_SUCCESS = 'ONLY_ON_SUCCESS', + ONLY_ON_ERROR = 'ONLY_ON_ERROR', +} diff --git a/src/login-web-app/src/shared/util/api-responses.ts b/src/login-web-app/src/shared/util/api-responses.ts index 72164a0f..35a680cf 100644 --- a/src/login-web-app/src/shared/util/api-responses.ts +++ b/src/login-web-app/src/shared/util/api-responses.ts @@ -320,6 +320,25 @@ export const completedWithSuccessStepWithoutLinks = { } as HaapiCompletedWithSuccessStep; export const completedWithErrorStep = { + type: HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR, + title: 'Authorization Error', + messages: [ + { + text: 'The authorization process completed with an error.', + classList: ['error'], + }, + ], + links: [ + { + href: 'http://client-callback?error=server_error', + rel: 'authorization-response', + }, + ], + error: 'server_error', + error_description: 'An error occurred during authorization', +} as HaapiCompletedWithErrorStep; + +export const completedWithErrorStepWithoutLinks = { type: HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR, title: 'Authorization Error', messages: [