diff --git a/src/components/message/ErrorReport.tsx b/src/components/message/ErrorReport.tsx
index 98c112cc6c..05841b2526 100644
--- a/src/components/message/ErrorReport.tsx
+++ b/src/components/message/ErrorReport.tsx
@@ -8,6 +8,8 @@ import { FullWidthWrapper } from 'src/app-components/FullWidthWrapper/FullWidthW
import classes from 'src/components/message/ErrorReport.module.css';
import { useAllAttachments } from 'src/features/attachments/hooks';
import { FileScanResults } from 'src/features/attachments/types';
+import { InstantiationClosedText } from 'src/features/instantiate/containers/InstantiationClosedError';
+import { isInstantiationClosedError, useInstantiationClosedReason } from 'src/features/instantiate/instantiationClosed';
import {
InstantiationValidation,
isInstantiationValidationResult,
@@ -22,6 +24,7 @@ import { HttpStatusCodes } from 'src/utils/network/networking';
import { splitDashedKey } from 'src/utils/splitDashedKey';
import { useGetUniqueKeyFromObject } from 'src/utils/useGetKeyFromObject';
import type { UploadedAttachment } from 'src/features/attachments';
+import type { InstantiationClosedReason } from 'src/features/instantiate/instantiationClosed';
import type { AnyValidation, BaseValidation, NodeRefValidation } from 'src/features/validation';
export interface IErrorReportProps extends PropsWithChildren {
@@ -142,11 +145,24 @@ export function ErrorReportList({ formErrors, taskErrors }: ErrorReportListProps
);
}
+export function ErrorListForClosedInstantiation({ reason }: { reason: InstantiationClosedReason }) {
+ return (
+
+
+
+ );
+}
+
/**
* @see InstantiateContainer Contains somewhat similar logic, but for a full-screen error page.
*/
export function ErrorListFromInstantiation({ error }: { error: unknown }) {
const selectedParty = useSelectedParty();
+ const closedReason = useInstantiationClosedReason();
+
+ if (isInstantiationClosedError(error)) {
+ return ;
+ }
if (isAxiosError(error) && error.response?.status === HttpStatusCodes.Forbidden) {
if (isInstantiationValidationResult(error.response.data)) {
diff --git a/src/features/applicationMetadata/types.ts b/src/features/applicationMetadata/types.ts
index 61ffb5143b..9eda9ab050 100644
--- a/src/features/applicationMetadata/types.ts
+++ b/src/features/applicationMetadata/types.ts
@@ -22,6 +22,9 @@ export interface IncomingApplicationMetadata {
altinnNugetVersion?: string;
logo?: ILogoOptions;
messageBoxConfig?: MessageBoxConfig;
+ validFrom?: string | null;
+ validTo?: string | null;
+ disallowUserInstantiation?: boolean;
}
type HideAlwaysSettings = {
diff --git a/src/features/instantiate/containers/InstantiateContainer.test.tsx b/src/features/instantiate/containers/InstantiateContainer.test.tsx
new file mode 100644
index 0000000000..cc97a7d7be
--- /dev/null
+++ b/src/features/instantiate/containers/InstantiateContainer.test.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+
+import { jest } from '@jest/globals';
+import { screen, waitFor } from '@testing-library/react';
+
+import { getIncomingApplicationMetadataMock } from 'src/__mocks__/getApplicationMetadataMock';
+import { InstantiateContainer } from 'src/features/instantiate/containers/InstantiateContainer';
+import { fetchApplicationMetadata } from 'src/queries/queries';
+import { renderWithoutInstanceAndLayout } from 'src/test/renderWithProviders';
+
+describe('InstantiateContainer', () => {
+ it('should show that the service cannot be started when instantiation responds with 410', async () => {
+ jest.spyOn(window, 'logError').mockImplementation(() => {});
+ const { mutations } = await renderWithoutInstanceAndLayout({
+ renderer: () => ,
+ waitUntilLoaded: false,
+ });
+
+ await waitFor(() => expect(mutations.doInstantiate.mock).toHaveBeenCalledTimes(1));
+ mutations.doInstantiate.reject(Object.assign(new Error('Gone'), { isAxiosError: true, response: { status: 410 } }));
+
+ expect(await screen.findByText('Du kan ikke starte denne tjenesten')).toBeInTheDocument();
+ expect(screen.getByTestId('StatusCode')).toHaveTextContent('Feil 410');
+ expect(screen.queryByText(/du mangler rettigheter/i)).not.toBeInTheDocument();
+ });
+
+ it('should not try to instantiate when validTo has passed', async () => {
+ jest
+ .mocked(fetchApplicationMetadata)
+ .mockImplementationOnce(async () => getIncomingApplicationMetadataMock({ validTo: '2020-01-01T00:00:00Z' }));
+ const { mutations } = await renderWithoutInstanceAndLayout({
+ renderer: () => ,
+ waitUntilLoaded: false,
+ });
+
+ expect(await screen.findByText('Du kan ikke starte denne tjenesten')).toBeInTheDocument();
+ expect(mutations.doInstantiate.mock).not.toHaveBeenCalled();
+ });
+
+ it('should not try to instantiate before validFrom, and say when the service opens', async () => {
+ jest
+ .mocked(fetchApplicationMetadata)
+ .mockImplementationOnce(async () => getIncomingApplicationMetadataMock({ validFrom: '2099-01-01T10:00:00Z' }));
+ const { mutations } = await renderWithoutInstanceAndLayout({
+ renderer: () => ,
+ waitUntilLoaded: false,
+ });
+
+ expect(await screen.findByText('Denne tjenesten har ikke åpnet ennå')).toBeInTheDocument();
+ expect(screen.getByText(/Tjenesten åpner 01\.01\.2099/)).toBeInTheDocument();
+ expect(mutations.doInstantiate.mock).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/features/instantiate/containers/InstantiateContainer.tsx b/src/features/instantiate/containers/InstantiateContainer.tsx
index 1ea2c0990e..d014dc27a1 100644
--- a/src/features/instantiate/containers/InstantiateContainer.tsx
+++ b/src/features/instantiate/containers/InstantiateContainer.tsx
@@ -2,8 +2,10 @@ import React, { useEffect } from 'react';
import { Loader } from 'src/core/loading/Loader';
import { InstantiateValidationError } from 'src/features/instantiate/containers/InstantiateValidationError';
+import { InstantiationClosedError } from 'src/features/instantiate/containers/InstantiationClosedError';
import { MissingRolesError } from 'src/features/instantiate/containers/MissingRolesError';
import { UnknownError } from 'src/features/instantiate/containers/UnknownError';
+import { isInstantiationClosedError, useInstantiationClosedReason } from 'src/features/instantiate/instantiationClosed';
import { isInstantiationValidationResult } from 'src/features/instantiate/InstantiationValidation';
import { useInstantiation } from 'src/features/instantiate/useInstantiation';
import { useSelectedParty } from 'src/features/party/PartiesProvider';
@@ -17,13 +19,18 @@ export const InstantiateContainer = () => {
changeBodyBackground(AltinnPalette.greyLight);
const party = useSelectedParty();
const instantiation = useInstantiation();
+ const closedReason = useInstantiationClosedReason();
useEffect(() => {
- const shouldCreateInstance = !!party;
+ const shouldCreateInstance = !!party && !closedReason;
if (shouldCreateInstance) {
instantiation.instantiate(party.partyId);
}
- }, [instantiation, party]);
+ }, [instantiation, party, closedReason]);
+
+ if (closedReason) {
+ return ;
+ }
// A 403 with RequiredAuthenticationLevel triggers a step-up redirect (see useInstantiation.onError). Block further
// rendering with a loader until the browser navigates away, so we never flash the "missing roles" error page. After a
@@ -33,6 +40,10 @@ export const InstantiateContainer = () => {
return ;
}
+ if (isInstantiationClosedError(instantiation.error)) {
+ return ;
+ }
+
if (isAxiosError(instantiation.error) && instantiation.error.response?.status === HttpStatusCodes.Forbidden) {
if (isInstantiationValidationResult(instantiation.error.response?.data)) {
return ;
diff --git a/src/features/instantiate/containers/InstantiationClosedError.tsx b/src/features/instantiate/containers/InstantiationClosedError.tsx
new file mode 100644
index 0000000000..84572131c7
--- /dev/null
+++ b/src/features/instantiate/containers/InstantiationClosedError.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+
+import { InstantiationErrorPage } from 'src/features/instantiate/containers/InstantiationErrorPage';
+import { Lang } from 'src/features/language/Lang';
+import { useCurrentLanguage } from 'src/features/language/LanguageProvider';
+import { useLanguage } from 'src/features/language/useLanguage';
+import { formatDateLocale } from 'src/utils/dateUtils';
+import { HttpStatusCodes } from 'src/utils/network/networking';
+import type { InstantiationClosedReason } from 'src/features/instantiate/instantiationClosed';
+
+/**
+ * Explains why the service cannot be started, as a text fragment for use in error reports and on the error page.
+ */
+export function InstantiationClosedText({ reason }: { reason: InstantiationClosedReason }) {
+ const language = useCurrentLanguage();
+
+ if (reason.reason === 'notYetOpen') {
+ return (
+
+ );
+ }
+
+ return ;
+}
+
+export function InstantiationClosedError({ reason }: { reason: InstantiationClosedReason }) {
+ const { langAsString } = useLanguage();
+
+ return (
+
+ }
+ content={}
+ showContactInfo={true}
+ statusCode={`${langAsString('party_selection.error_caption_prefix')} ${HttpStatusCodes.Gone}`}
+ />
+ );
+}
diff --git a/src/features/instantiate/instantiationClosed.test.ts b/src/features/instantiate/instantiationClosed.test.ts
new file mode 100644
index 0000000000..d682380b6e
--- /dev/null
+++ b/src/features/instantiate/instantiationClosed.test.ts
@@ -0,0 +1,52 @@
+import { getInstantiationClosedReason } from 'src/features/instantiate/instantiationClosed';
+
+describe('getInstantiationClosedReason', () => {
+ const now = new Date('2026-10-21T12:00:00Z');
+ const tt02Host = 'ttd.apps.tt02.altinn.no';
+ const prodHost = 'ttd.apps.altinn.no';
+
+ it.each([
+ [undefined, undefined],
+ [null, undefined],
+ ['2026-10-21T11:59:59Z', 'closed'],
+ ['2026-10-21T12:00:01Z', undefined],
+ ['2026-10-21T11:59:59.1504337Z', 'closed'],
+ ['2026-10-21T13:59:59+02:00', 'closed'],
+ ['2026-10-21T14:00:01+02:00', undefined],
+ ['2026-10-21T11:59:59', 'closed'],
+ ['2026-10-21T12:00:01', undefined],
+ ['2026-10-21', 'closed'],
+ ['2026-10-22', undefined],
+ ['not a date', undefined],
+ ])('validTo %s gives %s', (validTo, expected) => {
+ expect(getInstantiationClosedReason({ validTo }, now, tt02Host)?.reason).toBe(expected);
+ });
+
+ it.each([
+ [undefined, undefined],
+ [null, undefined],
+ ['2026-10-21T12:00:01Z', 'notYetOpen'],
+ ['2026-10-21T11:59:59Z', undefined],
+ ['2026-10-21T14:00:01+02:00', 'notYetOpen'],
+ ['2026-10-21T12:00:01', 'notYetOpen'],
+ ['2026-10-22', 'notYetOpen'],
+ ['2026-10-21', undefined],
+ ['not a date', undefined],
+ ])('validFrom %s gives %s', (validFrom, expected) => {
+ expect(getInstantiationClosedReason({ validFrom }, now, tt02Host)?.reason).toBe(expected);
+ });
+
+ it('reports when a not yet open service opens', () => {
+ const result = getInstantiationClosedReason({ validFrom: '2026-10-22T08:00:00Z' }, now, tt02Host);
+ expect(result).toEqual({ reason: 'notYetOpen', opensAt: new Date('2026-10-22T08:00:00Z') });
+ });
+
+ it.each([
+ [prodHost, true, 'closed'],
+ [prodHost, false, undefined],
+ [tt02Host, true, undefined],
+ ['local.altinn.cloud:8000', true, undefined],
+ ])('on %s with disallowUserInstantiation %s gives %s', (host, disallowUserInstantiation, expected) => {
+ expect(getInstantiationClosedReason({ disallowUserInstantiation }, now, host)?.reason).toBe(expected);
+ });
+});
diff --git a/src/features/instantiate/instantiationClosed.ts b/src/features/instantiate/instantiationClosed.ts
new file mode 100644
index 0000000000..219c5f9104
--- /dev/null
+++ b/src/features/instantiate/instantiationClosed.ts
@@ -0,0 +1,65 @@
+import { useApplicationMetadata } from 'src/features/applicationMetadata/ApplicationMetadataProvider';
+import { isAxiosError } from 'src/utils/isAxiosError';
+import { HttpStatusCodes } from 'src/utils/network/networking';
+import { isProductionHost } from 'src/utils/urls/urlHelper';
+import type { IncomingApplicationMetadata } from 'src/features/applicationMetadata/types';
+
+type InstantiationPreconditions = Pick<
+ IncomingApplicationMetadata,
+ 'validFrom' | 'validTo' | 'disallowUserInstantiation'
+>;
+
+export type InstantiationClosedReason = { reason: 'notYetOpen'; opensAt: Date } | { reason: 'closed' };
+
+// The app backend reads a validity date without an offset as UTC, while JavaScript reads a date-time without an
+// offset as local time.
+const hasTimeWithoutOffset = /T[^Z+-]*$/i;
+
+function parseValidityDate(value: string): Date {
+ return new Date(hasTimeWithoutOffset.test(value) ? `${value}Z` : value);
+}
+
+/**
+ * Mirrors the checks the app backend does before creating an instance, so that we can tell the user why the service
+ * cannot be started instead of sending a request we know will be rejected. The backend responds with 410 Gone in
+ * every case. Service owners are exempt from disallowUserInstantiation, but they do not use the app frontend.
+ *
+ * An unparsable validity date is left for the backend to judge.
+ */
+export function getInstantiationClosedReason(
+ { validFrom, validTo, disallowUserInstantiation }: InstantiationPreconditions,
+ now: Date,
+ host: string,
+): InstantiationClosedReason | undefined {
+ if (validFrom) {
+ const opensAt = parseValidityDate(validFrom);
+ if (opensAt.getTime() > now.getTime()) {
+ return { reason: 'notYetOpen', opensAt };
+ }
+ }
+
+ if (validTo && parseValidityDate(validTo).getTime() < now.getTime()) {
+ return { reason: 'closed' };
+ }
+
+ // The app backend only enforces disallowUserInstantiation in production, so that apps can still be tried out in
+ // test environments. Mirror that here, or the frontend would block instantiation where the backend allows it.
+ if (disallowUserInstantiation && isProductionHost(host)) {
+ return { reason: 'closed' };
+ }
+
+ return undefined;
+}
+
+/**
+ * The app backend responds with 410 Gone when it refuses to create an instance because the service is closed, see
+ * getInstantiationClosedReason for the conditions.
+ */
+export function isInstantiationClosedError(error: unknown): boolean {
+ return isAxiosError(error) && error.response?.status === HttpStatusCodes.Gone;
+}
+
+export function useInstantiationClosedReason(): InstantiationClosedReason | undefined {
+ const applicationMetadata = useApplicationMetadata();
+ return getInstantiationClosedReason(applicationMetadata, new Date(), window.location.host);
+}
diff --git a/src/features/instantiate/selection/InstanceSelection.test.tsx b/src/features/instantiate/selection/InstanceSelection.test.tsx
index e928585cbc..ccbfb80415 100644
--- a/src/features/instantiate/selection/InstanceSelection.test.tsx
+++ b/src/features/instantiate/selection/InstanceSelection.test.tsx
@@ -1,11 +1,13 @@
import React from 'react';
-import { screen, within } from '@testing-library/react';
+import { screen, waitFor, within } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
+import { getIncomingApplicationMetadataMock } from 'src/__mocks__/getApplicationMetadataMock';
import { InstanceSelectionWrapper } from 'src/features/instantiate/selection/InstanceSelection';
+import { fetchApplicationMetadata } from 'src/queries/queries';
import { mockMediaQuery } from 'src/test/mockMediaQuery';
-import { renderWithInstanceAndLayout } from 'src/test/renderWithProviders';
+import { renderWithInstanceAndLayout, renderWithoutInstanceAndLayout } from 'src/test/renderWithProviders';
import type { ISimpleInstance } from 'src/types';
const mockActiveInstances: ISimpleInstance[] = [
@@ -75,6 +77,45 @@ describe('InstanceSelection', () => {
expect(mutations.doInstantiate.mock).toHaveBeenCalledTimes(1);
});
+ it('should explain that new instances cannot be started when instantiation responds with 410', async () => {
+ jest.spyOn(window, 'logError').mockImplementation(() => {});
+ const { mutations } = await renderWithoutInstanceAndLayout({
+ renderer: () => ,
+ queries: { fetchActiveInstances: () => Promise.resolve(mockActiveInstances) },
+ });
+ await userEvent.click(screen.getByText(/start på nytt/i));
+ mutations.doInstantiate.reject(Object.assign(new Error('Gone'), { isAxiosError: true, response: { status: 410 } }));
+
+ await waitFor(() =>
+ expect(screen.getByText(/det er ikke mulig å starte nye skjema i denne tjenesten/i)).toBeInTheDocument(),
+ );
+ expect(screen.queryByText(/ikke har rettigheter til å starte denne tjenesten/i)).not.toBeInTheDocument();
+ });
+
+ it('should list active instances but not offer to start a new one when validTo has passed', async () => {
+ jest
+ .mocked(fetchApplicationMetadata)
+ .mockImplementationOnce(async () => getIncomingApplicationMetadataMock({ validTo: '2020-01-01T00:00:00Z' }));
+ const { mutations } = await render();
+
+ expect(await screen.findByText(mockActiveInstances[0].lastChangedBy)).toBeInTheDocument();
+ expect(screen.getByText(/det er ikke mulig å starte nye skjema i denne tjenesten/i)).toBeInTheDocument();
+ expect(screen.queryByText(/start på nytt/i)).not.toBeInTheDocument();
+ expect(mutations.doInstantiate.mock).not.toHaveBeenCalled();
+ });
+
+ it('should list active instances but not offer to start a new one before validFrom', async () => {
+ jest
+ .mocked(fetchApplicationMetadata)
+ .mockImplementationOnce(async () => getIncomingApplicationMetadataMock({ validFrom: '2099-01-01T10:00:00Z' }));
+ const { mutations } = await render();
+
+ expect(await screen.findByText(mockActiveInstances[0].lastChangedBy)).toBeInTheDocument();
+ expect(screen.getByText(/Tjenesten åpner 01\.01\.2099/)).toBeInTheDocument();
+ expect(screen.queryByText(/start på nytt/i)).not.toBeInTheDocument();
+ expect(mutations.doInstantiate.mock).not.toHaveBeenCalled();
+ });
+
it('should trigger openInstance on editButton click', async () => {
const { mutations, routerRef } = await render();
const row = screen.getByRole('row', {
diff --git a/src/features/instantiate/selection/InstanceSelection.tsx b/src/features/instantiate/selection/InstanceSelection.tsx
index 90770ba8b9..9fb9a5dc9d 100644
--- a/src/features/instantiate/selection/InstanceSelection.tsx
+++ b/src/features/instantiate/selection/InstanceSelection.tsx
@@ -12,6 +12,8 @@ import { ReadyForPrint } from 'src/components/ReadyForPrint';
import { useIsProcessing } from 'src/core/contexts/processingContext';
import { useAppName, useAppOwner } from 'src/core/texts/appTexts';
import { useApplicationMetadata } from 'src/features/applicationMetadata/ApplicationMetadataProvider';
+import { InstantiationClosedText } from 'src/features/instantiate/containers/InstantiationClosedError';
+import { useInstantiationClosedReason } from 'src/features/instantiate/instantiationClosed';
import {
ActiveInstancesProvider,
useActiveInstances,
@@ -58,6 +60,7 @@ function InstanceSelection() {
const mobileView = useIsMobileOrTablet();
const rowsPerPageOptions = instanceSelectionOptions?.rowsPerPageOptions ?? [10, 25, 50];
const instantiation = useInstantiation();
+ const instantiationClosedReason = useInstantiationClosedReason();
const selectedParty = useSelectedParty();
const setNavigationEffect = useSetNavigationEffect();
const { performProcess, isAnyProcessing, isThisProcessing: isLoading } = useIsProcessing();
@@ -251,34 +254,40 @@ function InstanceSelection() {
{mobileView && renderMobileTable()}
{!mobileView && renderTable()}
- : undefined}
- >
-
-
+
+
+ )}
diff --git a/src/language/texts/en.ts b/src/language/texts/en.ts
index 4f5863f306..aabe325885 100644
--- a/src/language/texts/en.ts
+++ b/src/language/texts/en.ts
@@ -243,6 +243,11 @@ export function en() {
'instantiate.unknown_error_status': 'Unknown error',
'instantiate.unknown_error_customer_support': 'If the problem persists, contact us at customer service at {0}.',
'instantiate.unknown_error_show_details': 'Show error details',
+ 'instantiate.not_yet_open_error_title': 'This service has not opened yet',
+ 'instantiate.not_yet_open_error_text': 'The service opens {0}. It is not possible to start new forms before then.',
+ 'instantiate.closed_error_title': 'You cannot start this service',
+ 'instantiate.closed_error_text':
+ 'It is not possible to start new forms in this service. You can still complete forms you have already started from your inbox.',
'instantiate.forbidden_action_error_title': 'You do not have permission to perform this action.',
'instantiate.forbidden_action_error_text': 'It looks like you do not have permission to perform this action.',
'instantiate.forbidden_action_error_status': '403 - Forbidden',
diff --git a/src/language/texts/nb.ts b/src/language/texts/nb.ts
index 077184fe62..006e29aab5 100644
--- a/src/language/texts/nb.ts
+++ b/src/language/texts/nb.ts
@@ -244,6 +244,11 @@ export function nb() {
'instantiate.unknown_error_status': 'Ukjent feil',
'instantiate.unknown_error_customer_support': 'Om problemet vedvarer, ta kontakt med oss på brukerservice {0}.',
'instantiate.unknown_error_show_details': 'Vis detaljer om feilen',
+ 'instantiate.not_yet_open_error_title': 'Denne tjenesten har ikke åpnet ennå',
+ 'instantiate.not_yet_open_error_text': 'Tjenesten åpner {0}. Det er ikke mulig å starte nye skjema før det.',
+ 'instantiate.closed_error_title': 'Du kan ikke starte denne tjenesten',
+ 'instantiate.closed_error_text':
+ 'Det er ikke mulig å starte nye skjema i denne tjenesten. Skjema du allerede har startet, kan du fortsatt fullføre fra innboksen.',
'instantiate.forbidden_action_error_title': 'Du mangler rettigheter til å utføre denne handlingen',
'instantiate.forbidden_action_error_text':
'Det ser ut til at du mangler rettigheter til å utføre denne handlingen.',
diff --git a/src/language/texts/nn.ts b/src/language/texts/nn.ts
index 1035e2e2b4..2ff67db754 100644
--- a/src/language/texts/nn.ts
+++ b/src/language/texts/nn.ts
@@ -244,6 +244,11 @@ export function nn() {
'instantiate.unknown_error_status': 'Ukjent feil',
'instantiate.unknown_error_customer_support': 'Om problemet hald fram, ta kontakt med oss på brukarservice {0}.',
'instantiate.unknown_error_show_details': 'Vis detaljar om feilen',
+ 'instantiate.not_yet_open_error_title': 'Denne tenesta har ikkje opna enno',
+ 'instantiate.not_yet_open_error_text': 'Tenesta opnar {0}. Det er ikkje mogleg å starte nye skjema før det.',
+ 'instantiate.closed_error_title': 'Du kan ikkje starte denne tenesta',
+ 'instantiate.closed_error_text':
+ 'Det er ikkje mogleg å starte nye skjema i denne tenesta. Skjema du allereie har starta, kan du framleis fullføre frå innboksen.',
'instantiate.forbidden_action_error_title': 'Du manglar rett til å utføre denne handlinga',
'instantiate.forbidden_action_error_text': 'Det ser ut til at du ikkje har rett til å utføre denne handlinga.',
'instantiate.forbidden_action_error_status': '403 - Forbidden',
diff --git a/src/layout/InstantiationButton/InstantiationButton.test.tsx b/src/layout/InstantiationButton/InstantiationButton.test.tsx
index d30fa21dbc..3e6fa31313 100644
--- a/src/layout/InstantiationButton/InstantiationButton.test.tsx
+++ b/src/layout/InstantiationButton/InstantiationButton.test.tsx
@@ -12,12 +12,13 @@ import { InstantiationButtonComponent } from 'src/layout/InstantiationButton/Ins
import { fetchApplicationMetadata } from 'src/queries/queries';
import { renderGenericComponentTest } from 'src/test/renderWithProviders';
-const render = async () => {
+const render = async (validity: { validFrom?: string; validTo?: string } = {}) => {
jest.mocked(fetchApplicationMetadata).mockImplementationOnce(async () =>
getIncomingApplicationMetadataMock({
onEntry: {
show: 'stateless',
},
+ ...validity,
}),
);
return await renderGenericComponentTest({
@@ -80,4 +81,34 @@ describe('InstantiationButton', () => {
expect(screen.getByText('You are now looking at the instance')).toBeInTheDocument();
});
});
+
+ it('should explain that the service cannot be started, and disable the button, when validTo has passed', async () => {
+ const { mutations } = await render({ validTo: '2020-01-01T00:00:00Z' });
+
+ expect(await screen.findByText(/det er ikke mulig å starte nye skjema i denne tjenesten/i)).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Instantiate' })).toBeDisabled();
+ expect(mutations.doInstantiateWithPrefill.mock).not.toHaveBeenCalled();
+ });
+
+ it('should explain that the service cannot be started, and disable the button, after a 410 response', async () => {
+ jest.spyOn(window, 'logError').mockImplementation(() => {});
+ const { mutations } = await render();
+
+ await userEvent.click(screen.getByRole('button', { name: 'Instantiate' }));
+ await waitFor(() => expect(mutations.doInstantiateWithPrefill.mock).toHaveBeenCalledTimes(1));
+ mutations.doInstantiateWithPrefill.reject(
+ Object.assign(new Error('Gone'), { isAxiosError: true, response: { status: 410 } }),
+ );
+
+ expect(await screen.findByText(/det er ikke mulig å starte nye skjema i denne tjenesten/i)).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Instantiate' })).toBeDisabled();
+ });
+
+ it('should say when the service opens, and disable the button, before validFrom', async () => {
+ const { mutations } = await render({ validFrom: '2099-01-01T10:00:00Z' });
+
+ expect(await screen.findByText(/Tjenesten åpner 01\.01\.2099/)).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Instantiate' })).toBeDisabled();
+ expect(mutations.doInstantiateWithPrefill.mock).not.toHaveBeenCalled();
+ });
});
diff --git a/src/layout/InstantiationButton/InstantiationButton.tsx b/src/layout/InstantiationButton/InstantiationButton.tsx
index 7d9397c865..76b255f795 100644
--- a/src/layout/InstantiationButton/InstantiationButton.tsx
+++ b/src/layout/InstantiationButton/InstantiationButton.tsx
@@ -1,15 +1,21 @@
import React from 'react';
import { Button } from 'src/app-components/Button/Button';
-import { ErrorListFromInstantiation, ErrorReport } from 'src/components/message/ErrorReport';
+import {
+ ErrorListForClosedInstantiation,
+ ErrorListFromInstantiation,
+ ErrorReport,
+} from 'src/components/message/ErrorReport';
import { useIsProcessing } from 'src/core/contexts/processingContext';
import { DataModels } from 'src/features/datamodel/DataModelsProvider';
import { FD } from 'src/features/formData/FormDataWrite';
+import { isInstantiationClosedError, useInstantiationClosedReason } from 'src/features/instantiate/instantiationClosed';
import { useInstantiation } from 'src/features/instantiate/useInstantiation';
import { useSetNavigationEffect } from 'src/features/navigation/NavigationEffectContext';
import { useSelectedParty } from 'src/features/party/PartiesProvider';
import { focusMainContent } from 'src/hooks/useNavigatePage';
import { useIndexedId } from 'src/utils/layout/DataModelLocation';
+import type { InstantiationClosedReason } from 'src/features/instantiate/instantiationClosed';
import type { IInstantiationButtonComponentProvidedProps } from 'src/layout/InstantiationButton/InstantiationButtonComponent';
type Props = Omit, 'text'>;
@@ -21,11 +27,20 @@ export const InstantiationButton = ({ children, ...props }: Props) => {
const prefill = FD.useMapping(props.mapping, DataModels.useDefaultDataType());
const party = useSelectedParty();
const setNavigationEffect = useSetNavigationEffect();
+ const metadataClosedReason = useInstantiationClosedReason();
+ const closedReason: InstantiationClosedReason | undefined =
+ metadataClosedReason ?? (isInstantiationClosedError(instantiation.error) ? { reason: 'closed' } : undefined);
return (
: undefined}
+ show={closedReason !== undefined || instantiation.error !== undefined}
+ errors={
+ closedReason ? (
+
+ ) : instantiation.error ? (
+
+ ) : undefined
+ }
>