richie-education 2.25.0-b2.dev32 → 2.25.0-b2.dev35

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 (27) hide show
  1. package/js/api/joanie.ts +26 -3
  2. package/js/api/lms/dummy.spec.ts +9 -1
  3. package/js/api/lms/dummy.ts +12 -2
  4. package/js/contexts/SessionContext/BaseSessionProvider.tsx +4 -12
  5. package/js/contexts/SessionContext/JoanieSessionProvider.tsx +4 -11
  6. package/js/contexts/SessionContext/index.spec.tsx +2 -4
  7. package/js/hooks/useContractArchive/index.download.spec.tsx +119 -0
  8. package/js/hooks/useContractArchive/index.spec.tsx +91 -0
  9. package/js/hooks/useContractArchive/index.ts +83 -0
  10. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +136 -0
  11. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +144 -0
  12. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +73 -0
  13. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +166 -0
  14. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +23 -8
  15. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +74 -0
  16. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +124 -0
  17. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +73 -0
  18. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +85 -0
  19. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +50 -0
  20. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +266 -0
  21. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +153 -0
  22. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.spec.tsx +100 -0
  23. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +23 -0
  24. package/js/settings.ts +7 -0
  25. package/js/types/Joanie.ts +5 -0
  26. package/js/utils/errors/HttpError.ts +1 -0
  27. package/package.json +1 -1
@@ -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
+ };
@@ -0,0 +1,266 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { act, renderHook, waitFor } from '@testing-library/react';
3
+ import { IntlProvider } from 'react-intl';
4
+ import { PropsWithChildren } from 'react';
5
+ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
6
+ import JoanieApiProvider from 'contexts/JoanieApiContext';
7
+ import { CONTRACT_DOWNLOAD_SETTINGS } from 'settings';
8
+ import {
9
+ getStoredContractArchiveId,
10
+ storeContractArchiveId,
11
+ unstoreContractArchiveId,
12
+ } from './contractArchiveLocalStorage';
13
+ import useDownloadContractArchive, { ContractDownloadStatus } from '.';
14
+
15
+ jest.mock('settings', () => ({
16
+ ...jest.requireActual('settings'),
17
+ CONTRACT_DOWNLOAD_SETTINGS: {
18
+ ...jest.requireActual('settings').CONTRACT_DOWNLOAD_SETTINGS,
19
+ pollInterval: 100,
20
+ },
21
+ }));
22
+
23
+ jest.mock('utils/context', () => ({
24
+ __esModule: true,
25
+ default: mockRichieContextFactory({
26
+ authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
27
+ joanie_backend: { endpoint: 'https://joanie.test' },
28
+ }).one(),
29
+ }));
30
+
31
+ const mockCheckArchive = jest.fn();
32
+ const mockCreateArchive = jest.fn();
33
+ const mockGetArchive = jest.fn();
34
+ jest.mock('hooks/useContractArchive', () => ({
35
+ __esModule: true,
36
+ default: () => ({
37
+ methods: { get: mockGetArchive, create: mockCreateArchive, check: mockCheckArchive },
38
+ }),
39
+ }));
40
+
41
+ let mockHasContractToDownload: boolean;
42
+ jest.mock('pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx', () => ({
43
+ __esModule: true,
44
+ default: () => mockHasContractToDownload,
45
+ }));
46
+
47
+ describe('useDownloadContractArchive', () => {
48
+ let organizationId: string;
49
+ const Wrapper = ({ children }: PropsWithChildren) => {
50
+ return (
51
+ <IntlProvider locale="en">
52
+ <JoanieApiProvider>{children}</JoanieApiProvider>
53
+ </IntlProvider>
54
+ );
55
+ };
56
+
57
+ afterEach(() => {
58
+ jest.resetAllMocks();
59
+ unstoreContractArchiveId();
60
+ });
61
+
62
+ describe('with no contract available for download', () => {
63
+ beforeEach(() => {
64
+ organizationId = faker.string.uuid();
65
+ mockHasContractToDownload = false;
66
+ });
67
+
68
+ it('should return IDLE status when no contractArchiveId is stored', async () => {
69
+ const { result } = renderHook(() => useDownloadContractArchive({ organizationId }), {
70
+ wrapper: Wrapper,
71
+ });
72
+ expect(result.current.status).toBe(ContractDownloadStatus.IDLE);
73
+
74
+ // no api call should have been done.
75
+ expect(mockCheckArchive).not.toHaveBeenCalled();
76
+ expect(mockGetArchive).not.toHaveBeenCalled();
77
+ expect(mockCreateArchive).not.toHaveBeenCalled();
78
+ });
79
+
80
+ it('should return IDLE status when contractArchiveId is stored', async () => {
81
+ const contractArchiveId = faker.string.uuid();
82
+ storeContractArchiveId(contractArchiveId);
83
+
84
+ const { result } = renderHook(() => useDownloadContractArchive({ organizationId }), {
85
+ wrapper: Wrapper,
86
+ });
87
+
88
+ expect(result.current.status).toBe(ContractDownloadStatus.IDLE);
89
+
90
+ // no api call should have been done.
91
+ expect(mockCheckArchive).not.toHaveBeenCalled();
92
+ expect(mockGetArchive).not.toHaveBeenCalled();
93
+ expect(mockCreateArchive).not.toHaveBeenCalled();
94
+ });
95
+ });
96
+
97
+ describe('with contracts available for download', () => {
98
+ beforeEach(() => {
99
+ organizationId = faker.string.uuid();
100
+ mockHasContractToDownload = true;
101
+ });
102
+
103
+ it('should return IDLE status when no contractArchiveId is stored', async () => {
104
+ const { result } = renderHook(() => useDownloadContractArchive({ organizationId }), {
105
+ wrapper: Wrapper,
106
+ });
107
+
108
+ // ensure that initial effects are done
109
+ expect(result.current.status).toBe(ContractDownloadStatus.IDLE);
110
+
111
+ // no api call should have been done.
112
+ expect(mockCheckArchive).not.toHaveBeenCalled();
113
+ expect(mockGetArchive).not.toHaveBeenCalled();
114
+ expect(mockCreateArchive).not.toHaveBeenCalled();
115
+ });
116
+
117
+ it('should return PENDING status when a contractArchiveId is stored', async () => {
118
+ const contractArchiveId = faker.string.uuid();
119
+ storeContractArchiveId(contractArchiveId);
120
+ mockCheckArchive.mockResolvedValue(false);
121
+
122
+ const { result } = renderHook(() => useDownloadContractArchive({ organizationId }), {
123
+ wrapper: Wrapper,
124
+ });
125
+
126
+ await waitFor(() => {
127
+ expect(result.current.status).toBe(ContractDownloadStatus.PENDING);
128
+ });
129
+
130
+ expect(mockCheckArchive).toHaveBeenCalledTimes(1);
131
+ expect(mockGetArchive).not.toHaveBeenCalled();
132
+ expect(mockCreateArchive).not.toHaveBeenCalled();
133
+ });
134
+
135
+ it('should return READY status when a contractArchiveId is stored and it exists on the server', async () => {
136
+ const contractArchiveId = faker.string.uuid();
137
+ storeContractArchiveId(contractArchiveId);
138
+ mockCheckArchive.mockResolvedValue(true);
139
+
140
+ const { result } = renderHook(() => useDownloadContractArchive({ organizationId }), {
141
+ wrapper: Wrapper,
142
+ });
143
+
144
+ // ensure that initial effects are done
145
+ await waitFor(() => {
146
+ expect(result.current.status).toBe(ContractDownloadStatus.READY);
147
+ });
148
+
149
+ expect(mockCheckArchive).toHaveBeenCalledTimes(1);
150
+ expect(mockGetArchive).not.toHaveBeenCalled();
151
+ expect(mockCreateArchive).not.toHaveBeenCalled();
152
+ });
153
+
154
+ it("doDownloadArchive should call 'getArchive' if the archive is ready for download", async () => {
155
+ const contractArchiveId = faker.string.uuid();
156
+ storeContractArchiveId(contractArchiveId);
157
+ mockCheckArchive.mockResolvedValue(true);
158
+
159
+ const { result } = renderHook(() => useDownloadContractArchive({ organizationId }), {
160
+ wrapper: Wrapper,
161
+ });
162
+
163
+ // ensure that initial effects are done
164
+ await waitFor(() => {
165
+ expect(result.current.status).toBe(ContractDownloadStatus.READY);
166
+ });
167
+ expect(mockCheckArchive).toHaveBeenCalledTimes(1);
168
+
169
+ act(() => {
170
+ result.current.downloadContractArchive();
171
+ });
172
+
173
+ await waitFor(() => {
174
+ expect(result.current.status).toBe(ContractDownloadStatus.IDLE);
175
+ });
176
+
177
+ expect(mockCheckArchive).toHaveBeenCalledTimes(1);
178
+ // backend is called to download the archive
179
+ expect(mockGetArchive).toHaveBeenCalledTimes(1);
180
+
181
+ // but not to generate the archive
182
+ expect(mockCreateArchive).not.toHaveBeenCalled();
183
+ });
184
+
185
+ it("doDownloadArchive should call 'createArchive' if no contractArchiveId is stored", async () => {
186
+ const { result } = renderHook(() => useDownloadContractArchive({ organizationId }), {
187
+ wrapper: Wrapper,
188
+ });
189
+
190
+ mockCheckArchive.mockResolvedValue(true);
191
+ mockCreateArchive.mockResolvedValue(faker.string.uuid());
192
+ act(() => {
193
+ result.current.downloadContractArchive();
194
+ });
195
+
196
+ await waitFor(() => {
197
+ expect(result.current.status).toBe(ContractDownloadStatus.IDLE);
198
+ });
199
+
200
+ expect(mockCreateArchive).toHaveBeenCalledTimes(1);
201
+ expect(mockCheckArchive).toHaveBeenCalledTimes(1);
202
+ expect(mockGetArchive).toHaveBeenCalledTimes(1);
203
+ });
204
+ });
205
+
206
+ describe('with contracts available for download and expired contractArchiveId', () => {
207
+ let contractArchiveId: string;
208
+ beforeEach(() => {
209
+ organizationId = faker.string.uuid();
210
+ mockHasContractToDownload = true;
211
+
212
+ const now = Date.now();
213
+ const unvalidCreationTime =
214
+ now - CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalVaklidityDurationMs * 2;
215
+
216
+ jest.useFakeTimers();
217
+ jest.setSystemTime(new Date(unvalidCreationTime));
218
+ contractArchiveId = faker.string.uuid();
219
+ storeContractArchiveId(contractArchiveId);
220
+ jest.setSystemTime(new Date(now));
221
+ });
222
+
223
+ afterEach(() => {
224
+ jest.runOnlyPendingTimers();
225
+ jest.useRealTimers();
226
+ });
227
+
228
+ it('should return READY status when archive exists on the server', async () => {
229
+ mockCheckArchive.mockResolvedValue(true);
230
+
231
+ const { result } = renderHook(() => useDownloadContractArchive({ organizationId }), {
232
+ wrapper: Wrapper,
233
+ });
234
+
235
+ // ensure that initial effects are done
236
+ await waitFor(() => {
237
+ expect(result.current.status).toBe(ContractDownloadStatus.READY);
238
+ });
239
+
240
+ expect(getStoredContractArchiveId()).toBe(contractArchiveId);
241
+
242
+ expect(mockCheckArchive).toHaveBeenCalledTimes(1);
243
+ expect(mockGetArchive).not.toHaveBeenCalled();
244
+ expect(mockCreateArchive).not.toHaveBeenCalled();
245
+ });
246
+
247
+ it("should return IDLE status and clear stored id when archive doesn't exists on the server", async () => {
248
+ mockCheckArchive.mockResolvedValue(false);
249
+
250
+ const { result } = renderHook(() => useDownloadContractArchive({ organizationId }), {
251
+ wrapper: Wrapper,
252
+ });
253
+
254
+ // ensure that initial effects are done
255
+ await waitFor(() => {
256
+ expect(result.current.status).toBe(ContractDownloadStatus.IDLE);
257
+ });
258
+
259
+ expect(getStoredContractArchiveId()).toBe(null);
260
+
261
+ expect(mockCheckArchive).toHaveBeenCalledTimes(1);
262
+ expect(mockGetArchive).not.toHaveBeenCalled();
263
+ expect(mockCreateArchive).not.toHaveBeenCalled();
264
+ });
265
+ });
266
+ });
@@ -0,0 +1,153 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import useContractArchive from 'hooks/useContractArchive';
3
+ import useCheckContractArchiveExists from '../useCheckContractArchiveExists';
4
+ import useHasContractToDownload from '../useHasContractToDownload';
5
+ import {
6
+ storeContractArchiveId,
7
+ unstoreContractArchiveId,
8
+ getStoredContractArchiveId,
9
+ isStoredContractArchiveIdExpired,
10
+ } from './contractArchiveLocalStorage';
11
+
12
+ export enum ContractDownloadStatus {
13
+ INITIALIZING = 'initializing',
14
+ IDLE = 'idle',
15
+ PENDING = 'pending',
16
+ READY = 'ready',
17
+ }
18
+
19
+ interface UseTeacherContractsBulkDownloadProps {
20
+ organizationId: string;
21
+ }
22
+
23
+ const useDownloadContractArchive = ({ organizationId }: UseTeacherContractsBulkDownloadProps) => {
24
+ // Contract's archive api interface
25
+ const {
26
+ methods: { get: getArchive, create: createArchive },
27
+ } = useContractArchive();
28
+
29
+ // Simple hook that verifiy if current user have some fully signed contracts to download.
30
+ const hasContractToDownload = useHasContractToDownload(organizationId);
31
+
32
+ // Component state of the localstorage contract's archive id
33
+ const [contractArchiveId, setContractArchiveId] = useState<string | null>(
34
+ getStoredContractArchiveId(),
35
+ );
36
+
37
+ // Hook that handle contract archive existence check and recursive polling of it.
38
+ const { isPolling, isContractArchiveExists, checkArchiveExists } = useCheckContractArchiveExists({
39
+ enable: !!hasContractToDownload,
40
+ });
41
+
42
+ // Stored contract's archive id have to be cleanup at some point.
43
+ // this expired logic is handle by ./contractArchiveLocalStorage and
44
+ // this isContractArchiveIdExpired track it at a component level.
45
+ const [isContractArchiveIdExpired, setIsContractArchiveIdExpired] = useState<boolean | null>(
46
+ null,
47
+ );
48
+
49
+ // This state track if the user have request the download or not.
50
+ // if he have click the download button, it will be true.
51
+ // if he just load the page, it will be false.
52
+ const [isDownloadRequest, setIsDownloadRequest] = useState(false);
53
+
54
+ // Here we compute the download status.
55
+ // It stay as INITIALIZING until all needed data get fetched.
56
+ // Then it become either:
57
+ // * READY: the archive is ready to be download from the server
58
+ // * PENDING: the archive is generating on the server
59
+ // * IDLE: A fresh new download process
60
+ const contractDownloadStatus = useMemo(() => {
61
+ if (
62
+ [hasContractToDownload, isContractArchiveExists, isContractArchiveIdExpired].includes(null)
63
+ ) {
64
+ return ContractDownloadStatus.INITIALIZING;
65
+ }
66
+
67
+ if (hasContractToDownload && contractArchiveId) {
68
+ if (isContractArchiveExists) {
69
+ return ContractDownloadStatus.READY;
70
+ }
71
+
72
+ return ContractDownloadStatus.PENDING;
73
+ }
74
+
75
+ return ContractDownloadStatus.IDLE;
76
+ }, [
77
+ hasContractToDownload,
78
+ contractArchiveId,
79
+ isContractArchiveExists,
80
+ isContractArchiveIdExpired,
81
+ ]);
82
+
83
+ const clearContractArchive = () => {
84
+ setIsDownloadRequest(false);
85
+ setContractArchiveId(null);
86
+ unstoreContractArchiveId();
87
+ };
88
+
89
+ // Either download the archive
90
+ // or flag the download as requested and request the archive's creation
91
+ const downloadContractArchive = async () => {
92
+ if (isContractArchiveExists && contractArchiveId !== null) {
93
+ await getArchive(contractArchiveId);
94
+ clearContractArchive();
95
+ } else {
96
+ // When archive is ready and download is requested
97
+ // a useEffect will trigger the download.
98
+ setIsDownloadRequest(true);
99
+ createContractArchive();
100
+ }
101
+ };
102
+
103
+ // Request the archive's creation if needed
104
+ // then can start archive's polling (if option.polling === true)
105
+ const createContractArchive = async () => {
106
+ let newContractArchiveId;
107
+ if (contractArchiveId === null) {
108
+ newContractArchiveId = await createArchive(organizationId);
109
+ setContractArchiveId(newContractArchiveId);
110
+ storeContractArchiveId(newContractArchiveId);
111
+ } else {
112
+ newContractArchiveId = contractArchiveId;
113
+ }
114
+
115
+ if (!isPolling) {
116
+ checkArchiveExists(newContractArchiveId, { polling: true });
117
+ }
118
+ };
119
+
120
+ // Here we check the validity of stored id timestamp
121
+ // if an id is expired, we'll either:
122
+ // * when it exists on the server: switch expired state but nothing else
123
+ // * when didn't exists on the server: switch expired state and clear download data.
124
+ useEffect(() => {
125
+ if (isContractArchiveExists === null) {
126
+ return;
127
+ }
128
+
129
+ if (isStoredContractArchiveIdExpired() && isContractArchiveExists === false) {
130
+ setIsContractArchiveIdExpired(true);
131
+ clearContractArchive();
132
+ } else {
133
+ setIsContractArchiveIdExpired(false);
134
+ }
135
+ }, [contractDownloadStatus, isContractArchiveExists]);
136
+
137
+ // When the archive become ready for download
138
+ // this effect will trigger the download
139
+ // if it have been previously requested by the user
140
+ useEffect(() => {
141
+ if (isDownloadRequest && isContractArchiveExists) {
142
+ downloadContractArchive();
143
+ }
144
+ }, [isDownloadRequest, isContractArchiveExists]);
145
+
146
+ return {
147
+ status: contractDownloadStatus,
148
+ downloadContractArchive,
149
+ createContractArchive,
150
+ };
151
+ };
152
+
153
+ export default useDownloadContractArchive;