diff --git a/src/haapi-react-app/src/shared/util/css/styles.css b/src/haapi-react-app/src/shared/util/css/styles.css
index a6a897a1..d5c13a7c 100644
--- a/src/haapi-react-app/src/shared/util/css/styles.css
+++ b/src/haapi-react-app/src/shared/util/css/styles.css
@@ -358,14 +358,18 @@ svg {
width: min(100%, 100px);
}
-.haapi-stepper-polling-progress {
- @extend .mt2;
+.haapi-stepper-polling-progress-bar {
+ @extend .mt2, .mb2;
+ display: block;
+ width: 100%;
+ max-width: 250px;
+ margin-inline: auto;
+ border: none;
border-radius: var(--form-field-border-radius);
background-color: var(--color-grey-subtle);
- outline: 1px solid var(--color-grey-light);
- outline-offset: 2px;
- border: none;
+ /* Set the track/fill at class specificity — the base `progress` element rules lose the cascade
+ here, which left the fill as the browser default (blue) instead of the brand colour. */
&::-webkit-progress-bar {
background-color: var(--color-grey-subtle);
border-radius: var(--form-field-border-radius);
@@ -382,6 +386,11 @@ svg {
}
}
+.haapi-stepper-polling-progress-duration {
+ @extend .center;
+ margin-block-start: var(--space-1);
+}
+
.haapi-stepper-link-qr-code-button {
background: none;
border: none;
diff --git a/src/haapi-react-sdk/haapi-stepper/README.md b/src/haapi-react-sdk/haapi-stepper/README.md
index cbb4fe5e..ec6e2d80 100644
--- a/src/haapi-react-sdk/haapi-stepper/README.md
+++ b/src/haapi-react-sdk/haapi-stepper/README.md
@@ -238,7 +238,8 @@ The Curity utility composition shown above is just how *this* project chose to i
| `.haapi-stepper-heading` | `HaapiStepperMessagesUI` | Heading messages |
| `.haapi-stepper-userName` | `HaapiStepperMessagesUI` | User name display |
| `.haapi-stepper-userCode` | `HaapiStepperMessagesUI` | User code display (e.g. recovery codes) |
-| `.haapi-stepper-polling-progress` | `HaapiStepperClientOperationUI` | Remaining polling time indicator (e.g. recovery codes) |
+| `.haapi-stepper-polling-progress-bar` | `HaapiStepperBankIdPollingProgressUI` | Remaining polling time indicator (the "authentication time" bar, e.g. BankID) |
+| `.haapi-stepper-polling-progress-duration` | `HaapiStepperBankIdPollingProgressUI` | Numeric time-left readout shown below the bar (e.g. "24 seconds left") |
| `.haapi-stepper-webauthn-registration-attachment` | `HaapiStepperWebAuthnRegistrationAttachmentCard` | WebAuthn registration attachment-selection option card (icon + title + description) |
| `.haapi-stepper-webauthn-registration-attachment-icon` | `HaapiStepperWebAuthnRegistrationAttachmentCard` | Attachment card icon |
| `.haapi-stepper-webauthn-registration-attachment-title` | `HaapiStepperWebAuthnRegistrationAttachmentCard` | Attachment card option label |
diff --git a/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.spec.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.spec.tsx
index 0d31fb4a..4a9a9ccf 100644
--- a/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.spec.tsx
+++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.spec.tsx
@@ -15,7 +15,6 @@ import userEvent from '@testing-library/user-event';
import { HAAPI_STEPS } from '../../../data-access/types/haapi-step.types';
import {
- createMockBankIdAction,
createMockExternalBrowserFlowAction,
createMockStep,
createMockWebAuthnAnyDeviceBothOptionsAction,
@@ -89,26 +88,6 @@ describe('HaapiStepperClientOperationUI', () => {
});
});
- describe('BankID polling progress', () => {
- it('renders a progress bar reflecting the session remaining time', () => {
- const action = createMockBankIdAction({ maxWaitTime: 60, maxWaitRemainingTime: 30 });
-
- render();
-
- const progress = screen.getByRole('progressbar');
- expect(progress).toHaveAttribute('value', '30');
- expect(progress).toHaveAttribute('max', '60');
- });
-
- it('hides the progress bar when showBankIdSessionTimeLeft is false', () => {
- const action = createMockBankIdAction({ maxWaitTime: 60, maxWaitRemainingTime: 30 });
-
- render();
-
- expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
- });
- });
-
describe('WebAuthn', () => {
afterEach(() => {
vi.unstubAllGlobals();
diff --git a/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.tsx
index c1b935fa..ade371ec 100644
--- a/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.tsx
+++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.tsx
@@ -16,7 +16,6 @@ import { useIsClientOperationAvailable } from './useIsClientOperationAvailable';
interface HaapiStepperClientOperationUIProps {
action: HaapiStepperClientOperationAction;
onAction: (action: HaapiStepperClientOperationAction | HaapiStepperFormAction) => void;
- showBankIdSessionTimeLeft?: boolean;
}
/**
@@ -44,11 +43,7 @@ interface HaapiStepperClientOperationUIProps {
*
* ```
*/
-export function HaapiStepperClientOperationUI({
- action,
- onAction,
- showBankIdSessionTimeLeft = true,
-}: HaapiStepperClientOperationUIProps) {
+export function HaapiStepperClientOperationUI({ action, onAction }: HaapiStepperClientOperationUIProps) {
const isAvailable = useIsClientOperationAvailable(action);
if (action.webauthn?.registrationAttachment) {
@@ -63,13 +58,6 @@ export function HaapiStepperClientOperationUI({
return (
- {showBankIdSessionTimeLeft && action.maxWaitRemainingTime !== undefined && (
-
- )}
diff --git a/src/haapi-react-sdk/haapi-stepper/feature/index.ts b/src/haapi-react-sdk/haapi-stepper/feature/index.ts
index d657fd5f..724e8d49 100644
--- a/src/haapi-react-sdk/haapi-stepper/feature/index.ts
+++ b/src/haapi-react-sdk/haapi-stepper/feature/index.ts
@@ -20,6 +20,7 @@ export * from './stepper/step-handlers/polling-step';
export * from './stepper/data-formatters/problem-step';
export * from './steps/HaapiStepperStepUI';
+export * from './viewnames/HaapiStepperBankIdPollingProgressUI';
export * from './actions/form/HaapiStepperFormUI';
export * from './actions/form/HaapiStepperFormValidationErrorInputWrapper';
export * from './actions/form/HaapiStepperFormHook';
diff --git a/src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/format-next-step-data.ts b/src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/format-next-step-data.ts
index de5363d6..a909b3f1 100644
--- a/src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/format-next-step-data.ts
+++ b/src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/format-next-step-data.ts
@@ -22,7 +22,6 @@ import {
} from '../../../data-access/types';
import {
HaapiStepperAction,
- HaapiStepperClientOperationAction,
HaapiStepperDataHelpers,
HaapiStepperDataHelpersActionsMap,
HaapiStepperLink,
@@ -110,18 +109,6 @@ function addActionDataHelpers(
};
}
- if (step.type === HAAPI_STEPS.POLLING && actionWithDataHelpers.subtype === HAAPI_ACTION_TYPES.CLIENT_OPERATION) {
- const clientOperationPollingAction = {
- ...actionWithDataHelpers,
- ...(step.properties.maxWaitTime != null && { maxWaitTime: step.properties.maxWaitTime }),
- ...(step.properties.maxWaitRemainingTime != null && {
- maxWaitRemainingTime: step.properties.maxWaitRemainingTime,
- }),
- };
-
- return clientOperationPollingAction as HaapiStepperClientOperationAction;
- }
-
return { ...action, ...actionWithDataHelpers };
}
diff --git a/src/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types.ts b/src/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types.ts
index 37814389..0ba82bcc 100644
--- a/src/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types.ts
+++ b/src/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types.ts
@@ -139,10 +139,6 @@ export type HaapiStepperSelectorAction = Omit
&
};
export type HaapiStepperClientOperationAction = HaapiClientOperationAction &
HaapiStepperDataHelpersDetails & {
- /** Polling session maximum time in seconds before the session expires. */
- maxWaitTime?: number;
- /** Polling session remaining time in seconds before the session expires. */
- maxWaitRemainingTime?: number;
/**
* WebAuthn data resolved during step-data formatting. Present only on any-device
* `webauthn-registration` actions.
diff --git a/src/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI.spec.tsx b/src/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI.spec.tsx
index 34dc2f0e..f4226d9c 100644
--- a/src/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI.spec.tsx
+++ b/src/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI.spec.tsx
@@ -2017,7 +2017,83 @@ describe('HaapiStepperStepUI', () => {
expect(screen.queryByTestId('messages')).toBeInTheDocument();
expect(screen.queryByTestId('links')).toBeInTheDocument();
});
+
+ it('should render the polling progress bar between the QR code and the actions', () => {
+ const step = createPollingStep({
+ links: [createMockQrLink()],
+ actions: [createMockClientOperationAction({ title: 'Launch BankID App' })],
+ maxWaitTime: '60',
+ maxWaitRemainingTime: '30',
+ });
+
+ renderWithContext(, { currentStep: step });
+
+ const progress = screen.getByRole('progressbar', { hidden: true });
+ expect(progress).toHaveAttribute('value', '30');
+ expect(progress).toHaveAttribute('max', '60');
+
+ const qrCode = screen.getByTestId('qr-code-button');
+ const actions = screen.getByTestId('client-operation-action');
+ expect(follows(progress, qrCode)).toBe(true);
+ expect(follows(actions, progress)).toBe(true);
+ });
+
+ it('should render the progress bar sourced from the step even without a client-operation (QR-only mode)', () => {
+ const step = createPollingStep({
+ links: [createMockQrLink()],
+ actions: [],
+ maxWaitTime: '60',
+ maxWaitRemainingTime: '30',
+ });
+
+ renderWithContext(, { currentStep: step });
+
+ expect(screen.getByRole('progressbar', { hidden: true })).toHaveAttribute('value', '30');
+ expect(screen.queryByTestId('client-operation-action')).not.toBeInTheDocument();
+ });
+
+ it('should not render the progress bar without a QR code (client-operation-only mode)', () => {
+ const step = createPollingStep({
+ links: [],
+ actions: [createMockClientOperationAction({ title: 'Launch BankID App' })],
+ maxWaitTime: '60',
+ maxWaitRemainingTime: '30',
+ });
+
+ renderWithContext(, { currentStep: step });
+
+ expect(screen.queryByRole('progressbar', { hidden: true })).not.toBeInTheDocument();
+ expect(screen.queryByTestId('qr-code-button')).not.toBeInTheDocument();
+ });
+
+ it('should not render a progress bar when the step exposes no remaining wait time', () => {
+ const step = createPollingStep({ links: [createMockQrLink()] });
+
+ renderWithContext(, { currentStep: step });
+
+ expect(screen.queryByRole('progressbar', { hidden: true })).not.toBeInTheDocument();
+ });
+
+ it.each([HAAPI_POLLING_STATUS.DONE, HAAPI_POLLING_STATUS.FAILED])(
+ 'should not render a progress bar for a %s polling step',
+ status => {
+ const step = createPollingStep({
+ status,
+ links: [createMockQrLink()],
+ maxWaitTime: '60',
+ maxWaitRemainingTime: '30',
+ });
+
+ renderWithContext(, { currentStep: step });
+
+ expect(screen.queryByRole('progressbar', { hidden: true })).not.toBeInTheDocument();
+ }
+ );
});
});
});
});
+
+/** True when `node` appears after `reference` in document order. */
+const follows = (node: Element, reference: Element) =>
+ Boolean(reference.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING);
diff --git a/src/haapi-react-sdk/haapi-stepper/feature/viewnames/BankIdViewNameBuiltInUI.tsx b/src/haapi-react-sdk/haapi-stepper/feature/viewnames/BankIdViewNameBuiltInUI.tsx
index 997cba32..2afcc5f6 100644
--- a/src/haapi-react-sdk/haapi-stepper/feature/viewnames/BankIdViewNameBuiltInUI.tsx
+++ b/src/haapi-react-sdk/haapi-stepper/feature/viewnames/BankIdViewNameBuiltInUI.tsx
@@ -9,8 +9,10 @@
* For further information, please contact Curity AB.
*/
+import { HAAPI_STEPS } from '../../data-access/types/haapi-step.types';
import { isQrCodeLink } from '../../util/link-predicates';
import { getLinksElement } from '../steps/step-element-factories';
+import { HaapiStepperBankIdPollingProgressUI } from './HaapiStepperBankIdPollingProgressUI';
import { HaapiStepperBankIdQrCodeAccessibilityMessages } from './HaapiStepperBankIdQrCodeAccessibilityMessages';
import type { ViewNameBuiltInUIProps } from './typings';
@@ -18,6 +20,8 @@ import type { ViewNameBuiltInUIProps } from './typings';
* Built-in UI for the BankID viewName (`HaapiStepperViewNameBuiltInUI.BANKID`).
*
* - Lifts the QR code link above the actions so it's the primary element on the screen.
+ * - Renders the polling "authentication time" progress bar under the QR code, only when a QR code is
+ * present.
* - Renders the QR-code accessibility messages (`metadata.viewData.messages`) as collapsible
* sections below the QR code.
*/
@@ -32,9 +36,14 @@ export const BankIdViewNameBuiltInUI = (props: ViewNameBuiltInUIProps) => {
{loadingElement}
{errorElement}
{messagesElement}
- {qrCodeLink && getLinksElement(props, [qrCodeLink], linkRenderInterceptor)}
{qrCodeLink && (
-
+ <>
+ {getLinksElement(props, [qrCodeLink], linkRenderInterceptor)}
+ {currentStep.type === HAAPI_STEPS.POLLING && (
+
+ )}
+
+ >
)}
{actionsElement}
{nonQrCodeLinks.length > 0 && getLinksElement(props, nonQrCodeLinks, linkRenderInterceptor)}
diff --git a/src/haapi-react-sdk/haapi-stepper/feature/viewnames/HaapiStepperBankIdPollingProgressUI.spec.tsx b/src/haapi-react-sdk/haapi-stepper/feature/viewnames/HaapiStepperBankIdPollingProgressUI.spec.tsx
new file mode 100644
index 00000000..085f45ee
--- /dev/null
+++ b/src/haapi-react-sdk/haapi-stepper/feature/viewnames/HaapiStepperBankIdPollingProgressUI.spec.tsx
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2026 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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { act, render, screen } from '@testing-library/react';
+
+import { HaapiStepperBankIdPollingProgressUI } from './HaapiStepperBankIdPollingProgressUI';
+import { createPollingStep } from '../../util/tests/mocks';
+import { HAAPI_POLLING_STATUS } from '../../data-access/types/haapi-step.types';
+
+const QR_MINUTES_LEFT_KEY = 'authenticator.bankid.launch.view.qr.minutes-left';
+const QR_SECONDS_LEFT_KEY = 'authenticator.bankid.launch.view.qr.seconds-left';
+
+describe('HaapiStepperBankIdPollingProgressUI', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.clearAllTimers();
+ vi.useRealTimers();
+ });
+
+ const tick = async (ms: number) => {
+ await act(async () => {
+ vi.advanceTimersByTime(ms);
+ await Promise.resolve();
+ });
+ };
+
+ it('fills the bar with the elapsed portion of the session', () => {
+ render(
+
+ );
+
+ const progress = screen.getByRole('progressbar', { hidden: true });
+ expect(progress).toHaveAttribute('value', '40');
+ expect(progress).toHaveAttribute('max', '60');
+ });
+
+ it('fills the bar one second at a time, clamped at max', async () => {
+ render(
+
+ );
+
+ expect(screen.getByRole('progressbar', { hidden: true })).toHaveAttribute('value', '57');
+
+ await tick(1000);
+ expect(screen.getByRole('progressbar', { hidden: true })).toHaveAttribute('value', '58');
+
+ await tick(5000);
+ expect(screen.getByRole('progressbar', { hidden: true })).toHaveAttribute('value', '60');
+ });
+
+ it('progresses on the local countdown and ignores the server value on later polls', async () => {
+ const { rerender } = render(
+
+ );
+
+ await tick(3000);
+ expect(screen.getByRole('progressbar', { hidden: true })).toHaveAttribute('value', '33');
+
+ rerender(
+
+ );
+ expect(screen.getByRole('progressbar', { hidden: true })).toHaveAttribute('value', '33');
+
+ rerender(
+
+ );
+ expect(screen.getByRole('progressbar', { hidden: true })).toHaveAttribute('value', '33');
+ });
+
+ // "1:00" at exactly 60s matches the Velocity reference (curity-ui.js _qrTimer).
+ it.each([
+ ['90', '1:30 minutes left'],
+ ['65', '1:05 minutes left'],
+ ['60', '1:00 minutes left'],
+ ['600', '10:00 minutes left'],
+ ['59', '59 seconds left'],
+ ['24', '24 seconds left'],
+ ['0', '0 seconds left'],
+ ])('formats %s seconds remaining as "%s"', (maxWaitRemainingTime, expected) => {
+ render(
+
+ );
+
+ expect(screen.getByTestId('polling-progress-duration')).toHaveTextContent(expected);
+ });
+
+ it('renders only the bar when the step has no unit labels', () => {
+ render(
+
+ );
+
+ expect(screen.getByRole('progressbar', { hidden: true })).toBeInTheDocument();
+ expect(screen.queryByTestId('polling-progress-duration')).not.toBeInTheDocument();
+ });
+
+ it('renders the readout but no bar when the step has no max wait time', () => {
+ render(
+
+ );
+
+ expect(screen.queryByRole('progressbar', { hidden: true })).not.toBeInTheDocument();
+ expect(screen.getByTestId('polling-progress-duration')).toHaveTextContent('20 seconds left');
+ });
+
+ it('renders nothing when the polling step has no remaining wait time', () => {
+ const { container } = render(
+
+ );
+
+ expect(screen.queryByRole('progressbar', { hidden: true })).not.toBeInTheDocument();
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it.each([HAAPI_POLLING_STATUS.DONE, HAAPI_POLLING_STATUS.FAILED])('renders nothing for a %s polling step', status => {
+ const { container } = render(
+
+ );
+
+ expect(screen.queryByRole('progressbar', { hidden: true })).not.toBeInTheDocument();
+ expect(container).toBeEmptyDOMElement();
+ });
+});
diff --git a/src/haapi-react-sdk/haapi-stepper/feature/viewnames/HaapiStepperBankIdPollingProgressUI.tsx b/src/haapi-react-sdk/haapi-stepper/feature/viewnames/HaapiStepperBankIdPollingProgressUI.tsx
new file mode 100644
index 00000000..cc5ca203
--- /dev/null
+++ b/src/haapi-react-sdk/haapi-stepper/feature/viewnames/HaapiStepperBankIdPollingProgressUI.tsx
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2026 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 { useEffect, useState } from 'react';
+import { HAAPI_POLLING_STATUS } from '../../data-access/types/haapi-step.types';
+import type { HaapiStepperPollingStep } from '../stepper/haapi-stepper.types';
+
+interface HaapiStepperBankIdPollingProgressUIProps {
+ /** The BankID polling step whose session time this bar reflects. */
+ currentStep: HaapiStepperPollingStep;
+}
+
+// BankID emits the countdown unit labels under this namespace in `metadata.viewData.messages`
+// (see `WaitBankIdRepresentationFunction` in the server) — the same namespace as the QR accessibility copy.
+const qrMessageKey = (suffix: string) => `authenticator.bankid.launch.view.qr.${suffix}`;
+
+const toSeconds = (value?: number | string): number | undefined => {
+ if (value === undefined) {
+ return undefined;
+ }
+
+ const seconds = Number(value);
+
+ return Number.isFinite(seconds) ? seconds : undefined;
+};
+
+/**
+ * @description
+ * # BANKID POLLING PROGRESS COMPONENT
+ *
+ * Renders the BankID "authentication time" progress bar for a *pending* polling step. Done and failed
+ * steps render nothing — they show their own outcome UI.
+ *
+ * The remaining time is seeded from the step's `maxWaitRemainingTime` once and counted down locally,
+ * one second at a time, clamped at 0. It is not re-synced on each poll: the server's remaining time
+ * can only arrive later/higher than the local count (poll cadence + latency), so re-syncing would only
+ * ever jerk the bar backwards. The bar shows elapsed time (so it fills toward timeout, matching the
+ * Velocity reference) while the readout counts remaining time down; the server decides when the session
+ * actually ends by returning a failed polling step.
+ *
+ * Accessibility: the bar is decorative (`aria-hidden`) — the adjacent numeric readout carries the
+ * remaining-time text for assistive tech, and only renders when the localized unit label is present in
+ * the step's `metadata.viewData.messages` (`minutes-left` for a minute or more left, `seconds-left`
+ * otherwise).
+ */
+export function HaapiStepperBankIdPollingProgressUI({ currentStep }: HaapiStepperBankIdPollingProgressUIProps) {
+ const { maxWaitTime, maxWaitRemainingTime, status } = currentStep.properties;
+ const viewDataMessages = currentStep.metadata?.viewData?.messages;
+ const minutesLeftLabel = viewDataMessages?.[qrMessageKey('minutes-left')];
+ const secondsLeftLabel = viewDataMessages?.[qrMessageKey('seconds-left')];
+
+ const [remaining, setRemaining] = useState(() => toSeconds(maxWaitRemainingTime));
+
+ const shouldCountDown = status === HAAPI_POLLING_STATUS.PENDING && remaining !== undefined && remaining > 0;
+
+ useEffect(() => {
+ if (!shouldCountDown) {
+ return;
+ }
+
+ const intervalId = setInterval(() => {
+ setRemaining(previous => (previous === undefined ? previous : Math.max(0, previous - 1)));
+ }, 1000);
+
+ return () => clearInterval(intervalId);
+ }, [shouldCountDown]);
+
+ if (remaining === undefined || status !== HAAPI_POLLING_STATUS.PENDING) {
+ return null;
+ }
+
+ const maxValue = toSeconds(maxWaitTime);
+ const totalSeconds = Math.floor(remaining);
+ const showMinutes = totalSeconds >= 60;
+ const readoutLabel = showMinutes ? minutesLeftLabel : secondsLeftLabel;
+ const readoutValue = showMinutes
+ ? `${String(Math.floor(totalSeconds / 60))}:${String(totalSeconds % 60).padStart(2, '0')}`
+ : String(totalSeconds);
+
+ return (
+ <>
+ {maxValue !== undefined && (
+
+ )}
+ {readoutLabel && (
+
+ {readoutValue} {readoutLabel}
+
+ )}
+ >
+ );
+}
diff --git a/src/haapi-react-sdk/haapi-stepper/util/tests/mocks.ts b/src/haapi-react-sdk/haapi-stepper/util/tests/mocks.ts
index 7e7f8221..6658f7f9 100644
--- a/src/haapi-react-sdk/haapi-stepper/util/tests/mocks.ts
+++ b/src/haapi-react-sdk/haapi-stepper/util/tests/mocks.ts
@@ -12,6 +12,7 @@
import { HAAPI_FORM_FIELDS, HTTP_METHODS } from '../../data-access/types/haapi-form.types';
import type {
HaapiStepperStep,
+ HaapiStepperPollingStep,
HaapiStepperAction,
HaapiStepperFormAction,
HaapiStepperSelectorAction,
@@ -205,20 +206,6 @@ export const createMockExternalBrowserFlowAction = (
...overrides,
}) as HaapiStepperExternalBrowserFlowClientOperationAction;
-export const createMockBankIdAction = (
- overrides: Partial = {}
-): HaapiStepperClientOperationAction =>
- createMockClientOperationAction({
- title: bankIdActionTitle,
- kind: 'bankid',
- model: {
- name: HAAPI_ACTION_CLIENT_OPERATIONS.BANKID,
- arguments: { href: '/bankid', autoStartToken: 'token' },
- continueActions: [continueAction],
- },
- ...overrides,
- });
-
const PUBLIC_KEY = { publicKey: WEBAUTHN_PUBLIC_KEY };
const webAuthnActionMetadata = {
@@ -283,17 +270,25 @@ export const createPollingStep = (
links?: HaapiStepperLink[];
actions?: HaapiStepperAction[];
viewName?: string;
+ maxWaitTime?: string;
+ maxWaitRemainingTime?: string;
+ viewDataMessages?: Record;
} = {}
-) => {
+): HaapiStepperPollingStep => {
return createMockStep(HAAPI_STEPS.POLLING, {
metadata: {
templateArea: 'lwa',
viewName: overrides.viewName ?? HaapiStepperViewNameBuiltInUI.BANKID,
+ ...(overrides.viewDataMessages !== undefined && { viewData: { messages: overrides.viewDataMessages } }),
+ },
+ properties: {
+ status: overrides.status ?? HAAPI_POLLING_STATUS.PENDING,
+ ...(overrides.maxWaitTime !== undefined && { maxWaitTime: overrides.maxWaitTime }),
+ ...(overrides.maxWaitRemainingTime !== undefined && { maxWaitRemainingTime: overrides.maxWaitRemainingTime }),
},
- properties: { status: overrides.status ?? HAAPI_POLLING_STATUS.PENDING },
...(overrides.links !== undefined && { links: overrides.links }),
...(overrides.actions !== undefined && { actions: overrides.actions }),
- });
+ }) as HaapiStepperPollingStep;
};
export const createMockQrLink = (overrides: Partial = {}) => {