richie-education 2.25.0-b2.dev68 → 2.25.0-b2.dev69
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.
- package/js/api/joanie.ts +19 -5
- package/js/components/DownloadContractButton/index.spec.tsx +17 -4
- package/js/components/DownloadContractButton/index.tsx +6 -11
- package/js/hooks/useContractArchive/index.download.spec.tsx +8 -1
- package/js/hooks/useContractArchive/index.ts +8 -27
- package/js/hooks/useDownloadCertificate/index.spec.tsx +17 -5
- package/js/hooks/useDownloadCertificate/index.tsx +2 -20
- package/js/utils/download.ts +43 -0
- package/js/widgets/Dashboard/components/DashboardItem/Certificate/index.spec.tsx +12 -4
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCertificateItem/index.spec.tsx +8 -1
- package/package.json +1 -1
package/js/api/joanie.ts
CHANGED
|
@@ -23,13 +23,21 @@ interface CheckStatusOptions {
|
|
|
23
23
|
ignoredErrorStatus: (number | HttpStatusCode)[];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export async function getFileFromResponse(response: Response): Promise<File> {
|
|
27
|
+
const filenameRegex = /filename="(.*)";/;
|
|
28
|
+
const dispositionHeader = response.headers.get('Content-Disposition');
|
|
29
|
+
const matches = dispositionHeader?.match(filenameRegex);
|
|
30
|
+
|
|
31
|
+
return new File([await response.blob()], matches ? matches[1] : '');
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
export function getResponseBody(response: Response) {
|
|
27
35
|
if (response.headers.get('Content-Type') === 'application/json') {
|
|
28
36
|
return response.json();
|
|
29
37
|
}
|
|
30
38
|
const fileType = ['application/pdf', 'application/zip'];
|
|
31
39
|
if (fileType.includes(response.headers.get('Content-Type') || '')) {
|
|
32
|
-
return
|
|
40
|
+
return new Promise((resolve) => resolve(response));
|
|
33
41
|
}
|
|
34
42
|
return response.text();
|
|
35
43
|
}
|
|
@@ -321,7 +329,7 @@ const API = (): Joanie.API => {
|
|
|
321
329
|
let url = ROUTES.user.orders.invoice.download.replace(':id', order_id);
|
|
322
330
|
url += `?${queryString.stringify({ reference: invoice_reference })}`;
|
|
323
331
|
|
|
324
|
-
return fetchWithJWT(url).then(checkStatus);
|
|
332
|
+
return fetchWithJWT(url).then(checkStatus).then(getFileFromResponse);
|
|
325
333
|
},
|
|
326
334
|
},
|
|
327
335
|
submit_for_signature: async (id) =>
|
|
@@ -346,7 +354,9 @@ const API = (): Joanie.API => {
|
|
|
346
354
|
},
|
|
347
355
|
certificates: {
|
|
348
356
|
download: async (id: string): Promise<File> =>
|
|
349
|
-
fetchWithJWT(ROUTES.user.certificates.download.replace(':id', id))
|
|
357
|
+
fetchWithJWT(ROUTES.user.certificates.download.replace(':id', id))
|
|
358
|
+
.then(checkStatus)
|
|
359
|
+
.then(getFileFromResponse),
|
|
350
360
|
get: async (filters) => {
|
|
351
361
|
return fetchWithJWT(buildApiUrl(ROUTES.user.certificates.get, filters)).then(checkStatus);
|
|
352
362
|
},
|
|
@@ -382,7 +392,9 @@ const API = (): Joanie.API => {
|
|
|
382
392
|
download(id: string): Promise<any> {
|
|
383
393
|
return fetchWithJWT(ROUTES.user.contracts.download.replace(':id', id), {
|
|
384
394
|
method: 'GET',
|
|
385
|
-
})
|
|
395
|
+
})
|
|
396
|
+
.then(checkStatus)
|
|
397
|
+
.then(getFileFromResponse);
|
|
386
398
|
},
|
|
387
399
|
zip_archive: {
|
|
388
400
|
check: async (id) => {
|
|
@@ -399,7 +411,9 @@ const API = (): Joanie.API => {
|
|
|
399
411
|
get: async (id) => {
|
|
400
412
|
return fetchWithJWT(ROUTES.user.contracts.zip_archive.get.replace(':id', id), {
|
|
401
413
|
method: 'GET',
|
|
402
|
-
})
|
|
414
|
+
})
|
|
415
|
+
.then(checkStatus)
|
|
416
|
+
.then(getFileFromResponse);
|
|
403
417
|
},
|
|
404
418
|
},
|
|
405
419
|
},
|
|
@@ -10,6 +10,7 @@ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/fac
|
|
|
10
10
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
11
11
|
import JoanieApiProvider from 'contexts/JoanieApiContext';
|
|
12
12
|
import { alert } from 'utils/indirection/window';
|
|
13
|
+
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
13
14
|
import DownloadContractButton from '.';
|
|
14
15
|
|
|
15
16
|
jest.mock('utils/context', () => ({
|
|
@@ -65,7 +66,16 @@ describe('<DownloadContractButton/>', () => {
|
|
|
65
66
|
const DOWNLOAD_URL = `https://joanie.endpoint/api/v1.0/contracts/${
|
|
66
67
|
order.contract!.id
|
|
67
68
|
}/download/`;
|
|
68
|
-
|
|
69
|
+
|
|
70
|
+
fetchMock.get(DOWNLOAD_URL, () => ({
|
|
71
|
+
status: HttpStatusCode.OK,
|
|
72
|
+
body: new Blob(['contract content']),
|
|
73
|
+
headers: {
|
|
74
|
+
'Content-Disposition': 'attachment; filename="test.pdf";',
|
|
75
|
+
'Content-Type': 'application/pdf',
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
const expectedFile = new File(['contract content'], 'test.pdf');
|
|
69
79
|
|
|
70
80
|
render(
|
|
71
81
|
<Wrapper>
|
|
@@ -88,9 +98,9 @@ describe('<DownloadContractButton/>', () => {
|
|
|
88
98
|
// eslint-disable-next-line compat/compat
|
|
89
99
|
expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
|
|
90
100
|
// eslint-disable-next-line compat/compat
|
|
91
|
-
expect(URL.createObjectURL).toHaveBeenCalledWith(
|
|
101
|
+
expect(URL.createObjectURL).toHaveBeenCalledWith(expectedFile);
|
|
92
102
|
expect(window.open).toHaveBeenCalledTimes(1);
|
|
93
|
-
expect(window.open).toHaveBeenCalledWith(
|
|
103
|
+
expect(window.open).toHaveBeenCalledWith(expectedFile);
|
|
94
104
|
});
|
|
95
105
|
|
|
96
106
|
it('fails downloading the contract and shows an error', async () => {
|
|
@@ -109,7 +119,10 @@ describe('<DownloadContractButton/>', () => {
|
|
|
109
119
|
const DOWNLOAD_URL = `https://joanie.endpoint/api/v1.0/contracts/${
|
|
110
120
|
order.contract!.id
|
|
111
121
|
}/download/`;
|
|
112
|
-
fetchMock.get(DOWNLOAD_URL,
|
|
122
|
+
fetchMock.get(DOWNLOAD_URL, {
|
|
123
|
+
status: HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
124
|
+
body: 'Bad request',
|
|
125
|
+
});
|
|
113
126
|
|
|
114
127
|
render(
|
|
115
128
|
<Wrapper>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Button } from '@openfun/cunningham-react';
|
|
2
2
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
|
3
3
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
4
|
-
import { handle } from 'utils/errors/handle';
|
|
5
4
|
import { Contract } from 'types/Joanie';
|
|
6
5
|
import { alert } from 'utils/indirection/window';
|
|
6
|
+
import { browserDownloadFromBlob } from 'utils/download';
|
|
7
7
|
|
|
8
8
|
const messages = defineMessages({
|
|
9
9
|
contractDownloadActionLabel: {
|
|
@@ -29,16 +29,11 @@ const DownloadContractButton = ({ contract, className }: DownloadContractButtonP
|
|
|
29
29
|
const intl = useIntl();
|
|
30
30
|
|
|
31
31
|
const downloadContract = async () => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// eslint-disable-next-line compat/compat
|
|
39
|
-
URL.revokeObjectURL(file);
|
|
40
|
-
} catch (e) {
|
|
41
|
-
handle(e);
|
|
32
|
+
const success = await browserDownloadFromBlob(
|
|
33
|
+
() => api.user.contracts.download(contract!.id),
|
|
34
|
+
true,
|
|
35
|
+
);
|
|
36
|
+
if (!success) {
|
|
42
37
|
alert(intl.formatMessage(messages.contractDownloadError));
|
|
43
38
|
}
|
|
44
39
|
};
|
|
@@ -71,7 +71,14 @@ describe('useContractArchive', () => {
|
|
|
71
71
|
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
|
|
72
72
|
|
|
73
73
|
result.current.methods.get(archiveId);
|
|
74
|
-
deferred.resolve(
|
|
74
|
+
deferred.resolve({
|
|
75
|
+
status: HttpStatusCode.OK,
|
|
76
|
+
body: new Blob(['test']),
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Disposition': 'attachment; filename="test.pdf";',
|
|
79
|
+
'Content-Type': 'application/pdf',
|
|
80
|
+
},
|
|
81
|
+
});
|
|
75
82
|
|
|
76
83
|
await waitFor(() => {
|
|
77
84
|
expect(fetchMock.called(DOWNLOAD_URL)).toBe(true);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
2
|
+
import { browserDownloadFromBlob } from 'utils/download';
|
|
2
3
|
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
3
4
|
import { handle } from 'utils/errors/handle';
|
|
4
5
|
|
|
@@ -17,25 +18,6 @@ const extractArchiveId = (url: string): string => {
|
|
|
17
18
|
return match[0];
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
// TODO: should be factorized with useDownloadCertificate
|
|
21
|
-
// and maybe DownloadContractButton
|
|
22
|
-
const buildArchiveFromBlob = (fileName: string, blob: Blob) => {
|
|
23
|
-
// eslint-disable-next-line compat/compat
|
|
24
|
-
const url = URL.createObjectURL(blob);
|
|
25
|
-
const $link = document.createElement('a');
|
|
26
|
-
$link.href = url;
|
|
27
|
-
$link.download = fileName;
|
|
28
|
-
|
|
29
|
-
const revokeObject = () => {
|
|
30
|
-
// eslint-disable-next-line compat/compat
|
|
31
|
-
URL.revokeObjectURL(url);
|
|
32
|
-
window.removeEventListener('blur', revokeObject);
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
window.addEventListener('blur', revokeObject);
|
|
36
|
-
$link.click();
|
|
37
|
-
};
|
|
38
|
-
|
|
39
21
|
const useContractArchive = () => {
|
|
40
22
|
const api = useJoanieApi();
|
|
41
23
|
return {
|
|
@@ -57,17 +39,16 @@ const useContractArchive = () => {
|
|
|
57
39
|
return false;
|
|
58
40
|
},
|
|
59
41
|
get: async (archiveId: string): Promise<true | void> => {
|
|
60
|
-
try {
|
|
61
|
-
const response = api.user.contracts.zip_archive.get(archiveId);
|
|
62
|
-
buildArchiveFromBlob('contracts.zip', await response);
|
|
63
|
-
return true;
|
|
64
|
-
} catch (error) {
|
|
65
|
-
handle(error);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
42
|
// FIXME: two thing could happen here with HttpStatusCode.NOT_FOUND response:
|
|
69
43
|
// * nothing found because the zip is generating.
|
|
70
44
|
// * nothing found because the zip doesn't and will never exist.
|
|
45
|
+
const success = await browserDownloadFromBlob(() =>
|
|
46
|
+
api.user.contracts.zip_archive.get(archiveId),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (success) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
71
52
|
},
|
|
72
53
|
create: async (organizationId: string): Promise<string> => {
|
|
73
54
|
const response = await api.user.contracts.zip_archive.create({
|
|
@@ -2,7 +2,7 @@ import fetchMock from 'fetch-mock';
|
|
|
2
2
|
import { PropsWithChildren } from 'react';
|
|
3
3
|
import { QueryClientProvider } from '@tanstack/react-query';
|
|
4
4
|
import { IntlProvider } from 'react-intl';
|
|
5
|
-
import { fireEvent, renderHook, waitFor } from '@testing-library/react';
|
|
5
|
+
import { act, fireEvent, renderHook, waitFor } from '@testing-library/react';
|
|
6
6
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
7
7
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
8
8
|
import { useDownloadCertificate } from 'hooks/useDownloadCertificate/index';
|
|
@@ -72,9 +72,19 @@ describe('useDownloadCertificate', () => {
|
|
|
72
72
|
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
|
|
73
73
|
expect(result.current.loading).toBe(false);
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
act(() => {
|
|
76
|
+
result.current.download(certificate.id);
|
|
77
|
+
});
|
|
78
|
+
expect(result.current.loading).toBe(true);
|
|
79
|
+
|
|
80
|
+
deferred.resolve({
|
|
81
|
+
status: HttpStatusCode.OK,
|
|
82
|
+
body: new Blob(['test']),
|
|
83
|
+
headers: {
|
|
84
|
+
'Content-Disposition': 'attachment; filename="test.pdf";',
|
|
85
|
+
'Content-Type': 'application/pdf',
|
|
86
|
+
},
|
|
87
|
+
});
|
|
78
88
|
|
|
79
89
|
await waitFor(() => {
|
|
80
90
|
expect(fetchMock.called(DOWNLOAD_URL)).toBe(true);
|
|
@@ -109,7 +119,9 @@ describe('useDownloadCertificate', () => {
|
|
|
109
119
|
// eslint-disable-next-line compat/compat
|
|
110
120
|
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
|
|
111
121
|
|
|
112
|
-
|
|
122
|
+
act(() => {
|
|
123
|
+
result.current.download(certificate.id);
|
|
124
|
+
});
|
|
113
125
|
|
|
114
126
|
await waitFor(() => {
|
|
115
127
|
expect(fetchMock.called(DOWNLOAD_URL)).toBe(true);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import { handle } from 'utils/errors/handle';
|
|
3
2
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
3
|
+
import { browserDownloadFromBlob } from 'utils/download';
|
|
4
4
|
|
|
5
5
|
export const useDownloadCertificate = () => {
|
|
6
6
|
const [loading, setLoading] = useState(false);
|
|
@@ -9,25 +9,7 @@ export const useDownloadCertificate = () => {
|
|
|
9
9
|
return {
|
|
10
10
|
download: async (certificateId: string) => {
|
|
11
11
|
setLoading(true);
|
|
12
|
-
|
|
13
|
-
const $link = document.createElement('a');
|
|
14
|
-
const file = await API.user.certificates.download(certificateId);
|
|
15
|
-
// eslint-disable-next-line compat/compat
|
|
16
|
-
const url = URL.createObjectURL(file);
|
|
17
|
-
$link.href = url;
|
|
18
|
-
$link.download = '';
|
|
19
|
-
|
|
20
|
-
const revokeObject = () => {
|
|
21
|
-
// eslint-disable-next-line compat/compat
|
|
22
|
-
URL.revokeObjectURL(url);
|
|
23
|
-
window.removeEventListener('blur', revokeObject);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
window.addEventListener('blur', revokeObject);
|
|
27
|
-
$link.click();
|
|
28
|
-
} catch (error) {
|
|
29
|
-
handle(error);
|
|
30
|
-
}
|
|
12
|
+
await browserDownloadFromBlob(() => API.user.certificates.download(certificateId));
|
|
31
13
|
setLoading(false);
|
|
32
14
|
},
|
|
33
15
|
loading,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { handle } from './errors/handle';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* browserDownloadFromBlob handle direct download of a file api response.
|
|
5
|
+
*
|
|
6
|
+
* @param downloadFunction, an api promise that return a File
|
|
7
|
+
* @param newWindow, does it open in a new window or not
|
|
8
|
+
* @returns boolean, true for success
|
|
9
|
+
*/
|
|
10
|
+
export const browserDownloadFromBlob = async (
|
|
11
|
+
downloadFunction: () => Promise<File>,
|
|
12
|
+
newWindow: boolean = false,
|
|
13
|
+
) => {
|
|
14
|
+
try {
|
|
15
|
+
const file = await downloadFunction();
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line compat/compat
|
|
18
|
+
const url = URL.createObjectURL(file);
|
|
19
|
+
|
|
20
|
+
if (newWindow) {
|
|
21
|
+
window.open(url);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const $link = document.createElement('a');
|
|
26
|
+
$link.href = url;
|
|
27
|
+
$link.download = file.name;
|
|
28
|
+
|
|
29
|
+
const revokeObject = () => {
|
|
30
|
+
// eslint-disable-next-line compat/compat
|
|
31
|
+
URL.revokeObjectURL(url);
|
|
32
|
+
window.removeEventListener('blur', revokeObject);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
window.addEventListener('blur', revokeObject);
|
|
36
|
+
$link.click();
|
|
37
|
+
return true;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
handle(error);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return false;
|
|
43
|
+
};
|
|
@@ -48,9 +48,13 @@ describe.each([
|
|
|
48
48
|
beforeAll(() => {
|
|
49
49
|
// eslint-disable-next-line compat/compat
|
|
50
50
|
URL.createObjectURL = jest.fn();
|
|
51
|
+
// eslint-disable-next-line compat/compat
|
|
52
|
+
URL.revokeObjectURL = jest.fn();
|
|
53
|
+
HTMLAnchorElement.prototype.click = jest.fn();
|
|
51
54
|
});
|
|
52
55
|
|
|
53
56
|
beforeEach(() => {
|
|
57
|
+
// SessionProvider queries
|
|
54
58
|
fetchMock.get('https://joanie.test/api/v1.0/orders/', []);
|
|
55
59
|
fetchMock.get('https://joanie.test/api/v1.0/addresses/', []);
|
|
56
60
|
fetchMock.get('https://joanie.test/api/v1.0/credit-cards/', []);
|
|
@@ -88,10 +92,14 @@ describe.each([
|
|
|
88
92
|
order: OrderFactory().one(),
|
|
89
93
|
}).one();
|
|
90
94
|
|
|
91
|
-
fetchMock.get(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
fetchMock.get(`https://joanie.test/api/v1.0/certificates/${certificate.id}/download/`, () => ({
|
|
96
|
+
status: HttpStatusCode.OK,
|
|
97
|
+
body: new Blob(['test']),
|
|
98
|
+
headers: {
|
|
99
|
+
'Content-Disposition': 'attachment; filename="test.pdf";',
|
|
100
|
+
'Content-Type': 'application/pdf',
|
|
101
|
+
},
|
|
102
|
+
}));
|
|
95
103
|
|
|
96
104
|
render(
|
|
97
105
|
<DashboardItemCertificate certificate={certificate} productType={ProductType.CREDENTIAL} />,
|
|
@@ -91,7 +91,14 @@ describe('CourseProductCertificateItem', () => {
|
|
|
91
91
|
// When user clicks on "Download" button, the certificate should be downloaded
|
|
92
92
|
fetchMock.get(
|
|
93
93
|
`https://joanie.test/api/v1.0/certificates/${order.certificate_id}/download/`,
|
|
94
|
-
|
|
94
|
+
() => ({
|
|
95
|
+
status: HttpStatusCode.OK,
|
|
96
|
+
body: new Blob(['test']),
|
|
97
|
+
headers: {
|
|
98
|
+
'Content-Disposition': 'attachment; filename="test.pdf";',
|
|
99
|
+
'Content-Type': 'application/pdf',
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
95
102
|
);
|
|
96
103
|
|
|
97
104
|
fireEvent.click($button);
|