richie-education 2.25.0-b2.dev31 → 2.25.0-b2.dev34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/js/api/joanie.ts +26 -3
  2. package/js/hooks/useContractArchive/index.download.spec.tsx +119 -0
  3. package/js/hooks/useContractArchive/index.spec.tsx +91 -0
  4. package/js/hooks/useContractArchive/index.ts +83 -0
  5. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +136 -0
  6. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +144 -0
  7. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +73 -0
  8. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +166 -0
  9. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +23 -8
  10. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +74 -0
  11. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +124 -0
  12. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +73 -0
  13. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +85 -0
  14. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +50 -0
  15. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +266 -0
  16. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +153 -0
  17. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.spec.tsx +100 -0
  18. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +23 -0
  19. package/js/settings.ts +7 -0
  20. package/js/types/Joanie.ts +5 -0
  21. package/js/utils/errors/HttpError.ts +1 -0
  22. package/package.json +1 -1
@@ -0,0 +1,73 @@
1
+ import { Button } from '@openfun/cunningham-react';
2
+ import { FormattedMessage, defineMessages } from 'react-intl';
3
+ import { useEffect } from 'react';
4
+ import useDownloadContractArchive, {
5
+ ContractDownloadStatus,
6
+ } from 'pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive';
7
+ import { Organization } from 'types/Joanie';
8
+
9
+ const messages = defineMessages({
10
+ bulkDownloadButtonDownloadLabel: {
11
+ defaultMessage: 'Download contracts archive',
12
+ description: 'The label of the bulk download button when the zip archive is ready for download',
13
+ id: 'pages.TeacherDashboardContractsLayout.BulkDownloadContractButton.bulkDownloadButtonDownloadLabel',
14
+ },
15
+ bulkDownloadButtonPendingLabel: {
16
+ defaultMessage: 'Generating contracts archive...',
17
+ description: 'The label of the bulk download button when archive generation is pending',
18
+ id: 'pages.TeacherDashboardContractsLayout.BulkDownloadContractButton.bulkDownloadButtonPendingLabel',
19
+ },
20
+ bulkDownloadButtonRequestArchiveLabel: {
21
+ defaultMessage: 'Request contracts archive',
22
+ description: 'The label of the bulk download button to request the generation of a zip archive',
23
+ id: 'pages.TeacherDashboardContractsLayout.BulkDownloadContractButton.bulkDownloadButtonRequestArchiveLabel',
24
+ },
25
+ });
26
+
27
+ interface BulkDownloadContractButtonProps {
28
+ organizationId: Organization['id'];
29
+ }
30
+
31
+ const BulkDownloadContractButton = ({ organizationId }: BulkDownloadContractButtonProps) => {
32
+ const { downloadContractArchive, createContractArchive, status } = useDownloadContractArchive({
33
+ organizationId,
34
+ });
35
+
36
+ useEffect(() => {
37
+ // Trigger contract's archive polling when generation had already been requested
38
+ if (status === ContractDownloadStatus.PENDING) {
39
+ createContractArchive();
40
+ }
41
+ }, [status]);
42
+
43
+ if (status === ContractDownloadStatus.PENDING) {
44
+ return (
45
+ <Button
46
+ disabled={true}
47
+ color="tertiary"
48
+ size="small"
49
+ icon={<div className="spinner spinner--small" />}
50
+ >
51
+ <FormattedMessage {...messages.bulkDownloadButtonPendingLabel} />
52
+ </Button>
53
+ );
54
+ }
55
+
56
+ return (
57
+ <Button
58
+ onClick={downloadContractArchive}
59
+ disabled={status === ContractDownloadStatus.INITIALIZING}
60
+ color={status === ContractDownloadStatus.READY ? 'primary' : 'tertiary'}
61
+ size="small"
62
+ icon={<span className="material-icons">download</span>}
63
+ >
64
+ <FormattedMessage
65
+ {...(status === ContractDownloadStatus.IDLE
66
+ ? messages.bulkDownloadButtonRequestArchiveLabel
67
+ : messages.bulkDownloadButtonDownloadLabel)}
68
+ />
69
+ </Button>
70
+ );
71
+ };
72
+
73
+ export default BulkDownloadContractButton;
@@ -0,0 +1,166 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { IntlProvider } from 'react-intl';
4
+ import { PropsWithChildren } from 'react';
5
+ import { QueryClientProvider } from '@tanstack/react-query';
6
+ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
7
+ import JoanieApiProvider from 'contexts/JoanieApiContext';
8
+
9
+ import { createTestQueryClient } from 'utils/test/createTestQueryClient';
10
+ import { ContractDownloadStatus } from 'pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive';
11
+ import ContractActionsBar from '.';
12
+
13
+ jest.mock('utils/context', () => ({
14
+ __esModule: true,
15
+ default: mockRichieContextFactory({
16
+ authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
17
+ joanie_backend: { endpoint: 'https://joanie.test' },
18
+ }).one(),
19
+ }));
20
+
21
+ let mockCanSignContracts: boolean;
22
+ let mockContractsToSignCount: number;
23
+ jest.mock('pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign', () => ({
24
+ __esModule: true,
25
+ default: () => ({
26
+ canSignContracts: mockCanSignContracts,
27
+ contractsToSignCount: mockContractsToSignCount,
28
+ }),
29
+ }));
30
+
31
+ let mockDownloadContractArchive: () => Promise<void>;
32
+ let mockCreateContractArchive: () => Promise<void>;
33
+ let mockDownloadContractArchiveStatus: ContractDownloadStatus;
34
+ jest.mock('pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive', () => ({
35
+ __esModule: true,
36
+ ...jest.requireActual('pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive'),
37
+ default: () => ({
38
+ downloadArchive: mockDownloadContractArchive,
39
+ createContractArchive: mockCreateContractArchive,
40
+ status: mockDownloadContractArchiveStatus,
41
+ }),
42
+ }));
43
+
44
+ let mockHasContractToDownload: boolean;
45
+ jest.mock('pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx', () => ({
46
+ __esModule: true,
47
+ default: () => mockHasContractToDownload,
48
+ }));
49
+
50
+ describe('TeacherDashboardContractsLayout/ContractActionsBar', () => {
51
+ const Wrapper = ({ children }: PropsWithChildren) => {
52
+ return (
53
+ <IntlProvider locale="en">
54
+ <QueryClientProvider client={createTestQueryClient({ user: true })}>
55
+ <JoanieApiProvider>{children}</JoanieApiProvider>
56
+ </QueryClientProvider>
57
+ </IntlProvider>
58
+ );
59
+ };
60
+
61
+ beforeAll(() => {
62
+ const modalExclude = document.createElement('div');
63
+ modalExclude.setAttribute('id', 'modal-exclude');
64
+ document.body.appendChild(modalExclude);
65
+ });
66
+
67
+ beforeEach(() => {
68
+ // useTeacherContractsToSign mocked values
69
+ mockCanSignContracts = true;
70
+ mockContractsToSignCount = 1;
71
+
72
+ // useDownloadContractArchive mocked values
73
+ mockHasContractToDownload = false;
74
+ mockDownloadContractArchive = jest.fn(() => Promise.resolve());
75
+ mockCreateContractArchive = jest.fn(() => Promise.resolve());
76
+ mockDownloadContractArchiveStatus = ContractDownloadStatus.IDLE;
77
+ });
78
+
79
+ afterEach(() => {
80
+ jest.resetAllMocks();
81
+ });
82
+
83
+ it("shouldn't display both sign and download button", () => {
84
+ mockHasContractToDownload = true;
85
+ mockCanSignContracts = true;
86
+ mockContractsToSignCount = 1;
87
+
88
+ render(
89
+ <Wrapper>
90
+ <ContractActionsBar
91
+ courseProductRelationId={faker.string.uuid()}
92
+ organizationId={faker.string.uuid()}
93
+ />
94
+ </Wrapper>,
95
+ );
96
+
97
+ expect(screen.getByTestId('teacher-contracts-list-actionsBar')).toBeInTheDocument();
98
+ expect(screen.getByRole('button', { name: /Sign all pending contracts/ })).toBeInTheDocument();
99
+ expect(screen.getByRole('button', { name: /Request contracts archive/ })).toBeInTheDocument();
100
+ });
101
+
102
+ it("shouldn't only display sign button", () => {
103
+ mockHasContractToDownload = false;
104
+ mockCanSignContracts = true;
105
+ mockContractsToSignCount = 1;
106
+
107
+ render(
108
+ <Wrapper>
109
+ <ContractActionsBar
110
+ courseProductRelationId={faker.string.uuid()}
111
+ organizationId={faker.string.uuid()}
112
+ />
113
+ </Wrapper>,
114
+ );
115
+
116
+ expect(screen.getByTestId('teacher-contracts-list-actionsBar')).toBeInTheDocument();
117
+ expect(screen.getByRole('button', { name: /Sign all pending contracts/ })).toBeInTheDocument();
118
+ expect(
119
+ screen.queryByRole('button', { name: /Request contracts archive/ }),
120
+ ).not.toBeInTheDocument();
121
+ });
122
+
123
+ it("shouldn't only display download button", () => {
124
+ mockHasContractToDownload = true;
125
+ mockCanSignContracts = false;
126
+ mockContractsToSignCount = 0;
127
+
128
+ render(
129
+ <Wrapper>
130
+ <ContractActionsBar
131
+ courseProductRelationId={faker.string.uuid()}
132
+ organizationId={faker.string.uuid()}
133
+ />
134
+ </Wrapper>,
135
+ );
136
+
137
+ expect(screen.getByTestId('teacher-contracts-list-actionsBar')).toBeInTheDocument();
138
+ expect(screen.getByRole('button', { name: /Request contracts archive/ })).toBeInTheDocument();
139
+ expect(
140
+ screen.queryByRole('button', { name: /Sign all pending contracts/ }),
141
+ ).not.toBeInTheDocument();
142
+ });
143
+
144
+ it('should return nothing when no actions are available', () => {
145
+ mockHasContractToDownload = false;
146
+ mockCanSignContracts = false;
147
+ mockContractsToSignCount = 0;
148
+
149
+ render(
150
+ <Wrapper>
151
+ <ContractActionsBar
152
+ courseProductRelationId={faker.string.uuid()}
153
+ organizationId={faker.string.uuid()}
154
+ />
155
+ </Wrapper>,
156
+ );
157
+
158
+ expect(screen.queryByTestId('teacher-contracts-list-actionsBar')).not.toBeInTheDocument();
159
+ expect(
160
+ screen.queryByRole('button', { name: /Request contracts archive/ }),
161
+ ).not.toBeInTheDocument();
162
+ expect(
163
+ screen.queryByRole('button', { name: /Sign all pending contracts/ }),
164
+ ).not.toBeInTheDocument();
165
+ });
166
+ });
@@ -1,6 +1,9 @@
1
+ import classNames from 'classnames';
1
2
  import { Organization, CourseProductRelation } from 'types/Joanie';
2
3
  import useTeacherContractsToSign from 'pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign';
4
+ import useHasContractToDownload from 'pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload';
3
5
  import SignOrganizationContractButton from '../SignOrganizationContractButton';
6
+ import BulkDownloadContractButton from '../BulkDownloadContractButton';
4
7
 
5
8
  interface ContractActionsProps {
6
9
  organizationId: Organization['id'];
@@ -12,15 +15,27 @@ const ContractActionsBar = ({ organizationId, courseProductRelationId }: Contrac
12
15
  organizationId,
13
16
  courseProductRelationId,
14
17
  });
18
+ const hasContractToDownload = useHasContractToDownload(organizationId);
19
+
20
+ const nbAvailableActions = [canSignContracts, hasContractToDownload].filter((val) => val).length;
15
21
  return (
16
- canSignContracts && (
17
- <div className="dashboard__page__actions-row dashboard__page__actions-row--space-between">
18
- <div>
19
- <SignOrganizationContractButton
20
- organizationId={organizationId}
21
- contractToSignCount={contractsToSignCount}
22
- />
23
- </div>
22
+ nbAvailableActions > 0 && (
23
+ <div
24
+ className={classNames('dashboard__page__actions-row', {
25
+ 'dashboard__page__actions-row--space-between': nbAvailableActions > 1,
26
+ 'dashboard__page__actions-row--end': nbAvailableActions === 1,
27
+ })}
28
+ data-testid="teacher-contracts-list-actionsBar"
29
+ >
30
+ {canSignContracts && (
31
+ <div>
32
+ <SignOrganizationContractButton
33
+ organizationId={organizationId}
34
+ contractToSignCount={contractsToSignCount}
35
+ />
36
+ </div>
37
+ )}
38
+ {hasContractToDownload && <BulkDownloadContractButton organizationId={organizationId} />}
24
39
  </div>
25
40
  )
26
41
  );
@@ -0,0 +1,74 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { IntlProvider } from 'react-intl';
4
+ import { PropsWithChildren } from 'react';
5
+ import { QueryClientProvider } from '@tanstack/react-query';
6
+ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
7
+ import JoanieApiProvider from 'contexts/JoanieApiContext';
8
+ import { createTestQueryClient } from 'utils/test/createTestQueryClient';
9
+
10
+ import SignOrganizationContractButton from '.';
11
+
12
+ jest.mock('utils/context', () => ({
13
+ __esModule: true,
14
+ default: mockRichieContextFactory({
15
+ authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
16
+ joanie_backend: { endpoint: 'https://joanie.test' },
17
+ }).one(),
18
+ }));
19
+
20
+ describe('TeacherDashboardContractsLayout/SignOrganizationContractButton', () => {
21
+ const Wrapper = ({ children }: PropsWithChildren) => {
22
+ return (
23
+ <IntlProvider locale="en">
24
+ <QueryClientProvider client={createTestQueryClient({ user: true })}>
25
+ <JoanieApiProvider>{children}</JoanieApiProvider>
26
+ </QueryClientProvider>
27
+ </IntlProvider>
28
+ );
29
+ };
30
+
31
+ beforeAll(() => {
32
+ const modalExclude = document.createElement('div');
33
+ modalExclude.setAttribute('id', 'modal-exclude');
34
+ document.body.appendChild(modalExclude);
35
+ });
36
+
37
+ afterEach(() => {
38
+ jest.resetAllMocks();
39
+ });
40
+
41
+ it("shouldn't render sign button and <OrganizationContractFrame/> when contractToSignCount > 0", () => {
42
+ render(
43
+ <Wrapper>
44
+ <SignOrganizationContractButton
45
+ organizationId={faker.string.uuid()}
46
+ contractToSignCount={1}
47
+ />
48
+ </Wrapper>,
49
+ );
50
+
51
+ expect(screen.getByRole('button', { name: /Sign all pending contracts/ })).toBeInTheDocument();
52
+
53
+ const DashboardContractFramePortal = document.getElementsByClassName('ReactModalPortal');
54
+ expect(DashboardContractFramePortal).toHaveLength(1);
55
+ });
56
+
57
+ it("shouldn't only render <OrganizationContractFrame/> when contractToSignCount is 0", () => {
58
+ render(
59
+ <Wrapper>
60
+ <SignOrganizationContractButton
61
+ organizationId={faker.string.uuid()}
62
+ contractToSignCount={0}
63
+ />
64
+ </Wrapper>,
65
+ );
66
+
67
+ expect(
68
+ screen.queryByRole('button', { name: /Sign all pending contracts/ }),
69
+ ).not.toBeInTheDocument();
70
+
71
+ const DashboardContractFramePortal = document.getElementsByClassName('ReactModalPortal');
72
+ expect(DashboardContractFramePortal).toHaveLength(1);
73
+ });
74
+ });
@@ -0,0 +1,124 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import fetchMock from 'fetch-mock';
3
+ import { act, renderHook, waitFor } from '@testing-library/react';
4
+ import { IntlProvider } from 'react-intl';
5
+ import { PropsWithChildren } from 'react';
6
+ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
7
+ import JoanieApiProvider from 'contexts/JoanieApiContext';
8
+ import {
9
+ storeContractArchiveId,
10
+ unstoreContractArchiveId,
11
+ } from '../useDownloadContractArchive/contractArchiveLocalStorage';
12
+ import useCheckContractArchiveExists from '.';
13
+
14
+ jest.mock('utils/context', () => ({
15
+ __esModule: true,
16
+ default: mockRichieContextFactory({
17
+ authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
18
+ joanie_backend: { endpoint: 'https://joanie.test' },
19
+ }).one(),
20
+ }));
21
+
22
+ jest.mock('settings', () => ({
23
+ ...jest.requireActual('settings'),
24
+ CONTRACT_DOWNLOAD_SETTINGS: {
25
+ ...jest.requireActual('settings').CONTRACT_DOWNLOAD_SETTINGS,
26
+ pollInterval: 100,
27
+ },
28
+ }));
29
+
30
+ const mockCheckArchive = jest.fn();
31
+ jest.mock('hooks/useContractArchive', () => ({
32
+ __esModule: true,
33
+ default: () => ({
34
+ methods: { get: jest.fn(), create: jest.fn(), check: mockCheckArchive },
35
+ }),
36
+ }));
37
+
38
+ describe('useCheckContractArchiveExists', () => {
39
+ const Wrapper = ({ children }: PropsWithChildren) => {
40
+ return (
41
+ <IntlProvider locale="en">
42
+ <JoanieApiProvider>{children}</JoanieApiProvider>
43
+ </IntlProvider>
44
+ );
45
+ };
46
+ beforeEach(() => {
47
+ // Joanie providers calls
48
+ fetchMock.get('https://joanie.test/api/v1.0/orders/', []);
49
+ fetchMock.get('https://joanie.test/api/v1.0/credit-cards/', []);
50
+ fetchMock.get('https://joanie.test/api/v1.0/addresses/', []);
51
+ });
52
+
53
+ afterEach(() => {
54
+ jest.resetAllMocks();
55
+ fetchMock.restore();
56
+ unstoreContractArchiveId();
57
+ });
58
+
59
+ it('should do nothing and return default value when no contractArchiveId is stored', () => {
60
+ const { result } = renderHook(useCheckContractArchiveExists, {
61
+ wrapper: Wrapper,
62
+ });
63
+
64
+ expect(result.current.isContractArchiveExists).toBe(false);
65
+ expect(mockCheckArchive).not.toHaveBeenCalled();
66
+ });
67
+
68
+ it('should check if archive exist when a id is stored', async () => {
69
+ storeContractArchiveId(faker.string.uuid());
70
+ mockCheckArchive.mockResolvedValue(true);
71
+
72
+ const { result } = renderHook(useCheckContractArchiveExists, {
73
+ wrapper: Wrapper,
74
+ });
75
+
76
+ expect(result.current.isContractArchiveExists).toBeNull();
77
+ await waitFor(() => {
78
+ expect(mockCheckArchive).toHaveBeenCalledTimes(1);
79
+ });
80
+
81
+ expect(result.current.isPolling).toBe(false);
82
+ expect(result.current.isContractArchiveExists).toBe(true);
83
+ });
84
+
85
+ it('should do nothing when enable is false', () => {
86
+ storeContractArchiveId(faker.string.uuid());
87
+ mockCheckArchive.mockResolvedValue(true);
88
+
89
+ const { result } = renderHook(() => useCheckContractArchiveExists({ enable: false }), {
90
+ wrapper: Wrapper,
91
+ });
92
+
93
+ expect(result.current.isContractArchiveExists).toBe(false);
94
+ expect(mockCheckArchive).not.toHaveBeenCalled();
95
+ });
96
+
97
+ it('should trigger polling when checkArchiveExist is call', async () => {
98
+ const { result, rerender } = renderHook(useCheckContractArchiveExists, {
99
+ wrapper: Wrapper,
100
+ });
101
+
102
+ mockCheckArchive.mockResolvedValue(false);
103
+ act(() => {
104
+ result.current.checkArchiveExists(faker.string.uuid());
105
+ });
106
+
107
+ await waitFor(() => {
108
+ expect(mockCheckArchive).toHaveBeenCalledTimes(1);
109
+ });
110
+
111
+ expect(result.current.isContractArchiveExists).toBe(false);
112
+
113
+ // isPolling it need's a rerender to be updated
114
+ rerender();
115
+ expect(result.current.isPolling).toBe(true);
116
+
117
+ mockCheckArchive.mockResolvedValue(true);
118
+ await waitFor(() => {
119
+ expect(mockCheckArchive).toHaveBeenCalledTimes(2);
120
+ });
121
+ expect(result.current.isPolling).toBe(false);
122
+ expect(result.current.isContractArchiveExists).toBe(true);
123
+ });
124
+ });
@@ -0,0 +1,73 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import useContractArchive from 'hooks/useContractArchive';
3
+ import { CONTRACT_DOWNLOAD_SETTINGS } from 'settings';
4
+ import { Nullable } from 'types/utils';
5
+ import { getStoredContractArchiveId } from '../useDownloadContractArchive/contractArchiveLocalStorage';
6
+
7
+ export interface UseCheckContractArchiveExistsProps {
8
+ enable: boolean;
9
+ }
10
+
11
+ const useCheckContractArchiveExist = (
12
+ { enable }: UseCheckContractArchiveExistsProps = { enable: true },
13
+ ) => {
14
+ // Contract's archive api interface
15
+ const {
16
+ methods: { check: checkArchiveExist },
17
+ } = useContractArchive();
18
+
19
+ // Store if the contract's archive exists or not on the server
20
+ // stay null until fetched
21
+ const [isContractArchiveExists, setIsContractArchiveExists] = useState<Nullable<boolean>>(null);
22
+
23
+ const timeoutRef = useRef<NodeJS.Timeout>();
24
+
25
+ // This method will check if the archive exists on the server
26
+ // option.polling === true will recursivly poll archive existence
27
+ const checkArchiveExists = async (
28
+ archiveId: string,
29
+ options: { polling: boolean } = { polling: true },
30
+ ) => {
31
+ clearTimeout(timeoutRef.current);
32
+ timeoutRef.current = undefined;
33
+
34
+ const isExists = await checkArchiveExist(archiveId);
35
+ setIsContractArchiveExists(isExists);
36
+
37
+ if (!options.polling) {
38
+ return;
39
+ }
40
+
41
+ if (!isExists) {
42
+ timeoutRef.current = setTimeout(
43
+ () => checkArchiveExists(archiveId),
44
+ CONTRACT_DOWNLOAD_SETTINGS.pollInterval,
45
+ );
46
+ }
47
+ };
48
+
49
+ // This effect will initialize isContractArchiveExists value
50
+ useEffect(() => {
51
+ const storedContractArchiveId = getStoredContractArchiveId();
52
+ if (enable && storedContractArchiveId) {
53
+ checkArchiveExists(storedContractArchiveId, { polling: false });
54
+ } else {
55
+ setIsContractArchiveExists(false);
56
+ }
57
+ }, [enable]);
58
+
59
+ // Be sure to clear any timeout before unmouting the hook.
60
+ useEffect(() => {
61
+ return () => {
62
+ clearTimeout(timeoutRef.current);
63
+ };
64
+ }, []);
65
+
66
+ return {
67
+ isPolling: !!timeoutRef.current,
68
+ isContractArchiveExists,
69
+ checkArchiveExists,
70
+ };
71
+ };
72
+
73
+ export default useCheckContractArchiveExist;
@@ -0,0 +1,85 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { CONTRACT_DOWNLOAD_SETTINGS } from 'settings';
3
+ import {
4
+ getStoredContractArchiveId,
5
+ isStoredContractArchiveIdExpired,
6
+ storeContractArchiveId,
7
+ unstoreContractArchiveId,
8
+ } from './contractArchiveLocalStorage';
9
+
10
+ describe('contractArchiveLocalStorage', () => {
11
+ afterEach(() => {
12
+ unstoreContractArchiveId();
13
+ });
14
+
15
+ it('should store and unstore contractArchiveId and creation date in localStorage', () => {
16
+ expect(
17
+ localStorage.getItem(CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalStorageKey),
18
+ ).toBeNull();
19
+
20
+ const contractArchiveId = faker.string.uuid();
21
+ storeContractArchiveId(contractArchiveId);
22
+ expect(localStorage.getItem(CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalStorageKey)).toMatch(
23
+ new RegExp(`[0-9]+::${contractArchiveId}`),
24
+ );
25
+
26
+ unstoreContractArchiveId();
27
+ expect(
28
+ localStorage.getItem(CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalStorageKey),
29
+ ).toBeNull();
30
+ });
31
+
32
+ it('should retrieve contractArchiveId from localStorage', () => {
33
+ const contractArchiveId = faker.string.uuid();
34
+ storeContractArchiveId(contractArchiveId);
35
+
36
+ const retrievedcontractArchiveId = getStoredContractArchiveId();
37
+ expect(retrievedcontractArchiveId).toBe(contractArchiveId);
38
+ });
39
+
40
+ it.each([
41
+ {
42
+ label: 'outdated creation date in the past',
43
+ now: Date.now(),
44
+ storageCreationTime:
45
+ Date.now() - CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalVaklidityDurationMs * 2,
46
+ },
47
+ {
48
+ label: 'outdated creation date in the future',
49
+ now: Date.now(),
50
+ storageCreationTime:
51
+ Date.now() + CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalVaklidityDurationMs * 2,
52
+ },
53
+ ])(
54
+ 'isStoredContractArchiveIdExpired should be true for $label',
55
+ ({ now, storageCreationTime }) => {
56
+ jest.useFakeTimers();
57
+ jest.setSystemTime(new Date(storageCreationTime));
58
+ const contractArchiveId = faker.string.uuid();
59
+ storeContractArchiveId(contractArchiveId);
60
+
61
+ jest.setSystemTime(new Date(now));
62
+ expect(isStoredContractArchiveIdExpired()).toBe(true);
63
+
64
+ jest.runOnlyPendingTimers();
65
+ jest.useRealTimers();
66
+ },
67
+ );
68
+
69
+ it("isStoredContractArchiveIdExpired should be true false storage isn't expired", () => {
70
+ const now = Date.now();
71
+ const validCreationTime =
72
+ now - CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalVaklidityDurationMs / 2;
73
+
74
+ jest.useFakeTimers();
75
+ jest.setSystemTime(new Date(validCreationTime));
76
+ const contractArchiveId = faker.string.uuid();
77
+ storeContractArchiveId(contractArchiveId);
78
+
79
+ jest.setSystemTime(new Date(now));
80
+ expect(isStoredContractArchiveIdExpired()).toBe(false);
81
+
82
+ jest.runOnlyPendingTimers();
83
+ jest.useRealTimers();
84
+ });
85
+ });
@@ -0,0 +1,50 @@
1
+ import { CONTRACT_DOWNLOAD_SETTINGS } from 'settings';
2
+
3
+ const generateLocalStorageId = (contractArchiveId: string) => {
4
+ return `${Date.now()}::${contractArchiveId}`;
5
+ };
6
+
7
+ const storeContractArchiveId = (contractArchiveId: string) => {
8
+ localStorage.setItem(
9
+ CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalStorageKey,
10
+ generateLocalStorageId(contractArchiveId),
11
+ );
12
+ };
13
+
14
+ const unstoreContractArchiveId = () => {
15
+ localStorage.removeItem(CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalStorageKey);
16
+ };
17
+
18
+ const getStoredContractArchiveId = () => {
19
+ const value = localStorage.getItem(CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalStorageKey);
20
+ if (value === null) {
21
+ return value;
22
+ }
23
+
24
+ const [, contractArchiveId] = value.split('::');
25
+ return contractArchiveId;
26
+ };
27
+
28
+ const isStoredContractArchiveIdExpired = () => {
29
+ const value = localStorage.getItem(CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalStorageKey);
30
+ if (value === null) {
31
+ return false;
32
+ }
33
+ const [creationTimestamp] = value.split('::');
34
+
35
+ const bounds: number[] = [Date.now(), parseInt(creationTimestamp, 10)];
36
+ // reverse bounds when computer time change.
37
+ if (bounds[0] > bounds[1]) {
38
+ bounds.reverse();
39
+ }
40
+
41
+ const [begin, end] = bounds;
42
+ return end - begin > CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalVaklidityDurationMs;
43
+ };
44
+
45
+ export {
46
+ storeContractArchiveId,
47
+ unstoreContractArchiveId,
48
+ getStoredContractArchiveId,
49
+ isStoredContractArchiveIdExpired,
50
+ };