richie-education 3.3.2-dev6 → 3.4.1-dev13
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/.eslintrc.json +0 -3
- package/.storybook/main.js +11 -12
- package/.storybook/preview.tsx +49 -24
- package/cunningham.cjs +31 -0
- package/i18n/compile-translations.js +12 -10
- package/i18n/locales/ar-SA.json +20 -0
- package/i18n/locales/es-ES.json +20 -0
- package/i18n/locales/fa-IR.json +20 -0
- package/i18n/locales/fr-CA.json +20 -0
- package/i18n/locales/fr-FR.json +21 -1
- package/i18n/locales/ko-KR.json +20 -0
- package/i18n/locales/pt-PT.json +42 -22
- package/i18n/locales/ru-RU.json +20 -0
- package/i18n/locales/vi-VN.json +20 -0
- package/jest.config.js +5 -0
- package/js/api/joanie.ts +20 -0
- package/js/api/utils.ts +4 -3
- package/js/components/AddressesManagement/AddressForm/index.stories.tsx +1 -1
- package/js/components/AddressesManagement/AddressForm/index.tsx +4 -3
- package/js/components/AddressesManagement/index.stories.tsx +1 -1
- package/js/components/AddressesManagement/index.tsx +5 -3
- package/js/components/Badge/index.stories.tsx +1 -1
- package/js/components/Badge/index.tsx +1 -1
- package/js/components/Banner/index.stories.tsx +1 -1
- package/js/components/CourseGlimpse/index.stories.tsx +1 -1
- package/js/components/CourseGlimpseList/index.stories.tsx +1 -1
- package/js/components/CreditCardSelector/_styles.scss +2 -2
- package/js/components/CreditCardSelector/index.tsx +11 -3
- package/js/components/DownloadAgreementButton/index.tsx +51 -0
- package/js/components/DownloadBatchOrderSeatsButton/index.spec.tsx +46 -0
- package/js/components/DownloadBatchOrderSeatsButton/index.tsx +80 -0
- package/js/components/DownloadCertificateButton/index.tsx +2 -1
- package/js/components/DownloadContractButton/index.tsx +7 -1
- package/js/components/Form/Form/index.tsx +4 -2
- package/js/components/Icon/index.stories.tsx +2 -1
- package/js/components/Modal/index.stories.tsx +1 -1
- package/js/components/Modal/index.tsx +2 -1
- package/js/components/OpenEdxFullNameForm/index.stories.tsx +1 -1
- package/js/components/OpenEdxFullNameForm/index.tsx +2 -2
- package/js/components/PaymentScheduleGrid/_styles.scss +2 -2
- package/js/components/PurchaseButton/index.stories.tsx +1 -1
- package/js/components/RegisteredAddress/index.stories.tsx +1 -1
- package/js/components/RegisteredAddress/index.tsx +4 -2
- package/js/components/SaleTunnel/AddressSelector/CreateAddressFormModal.tsx +1 -1
- package/js/components/SaleTunnel/AddressSelector/EditAddressFormModal.tsx +1 -1
- package/js/components/SaleTunnel/AddressSelector/index.tsx +4 -2
- package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationGroup.tsx +5 -4
- package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationSingular.tsx +27 -5
- package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +1 -1
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +9 -6
- package/js/components/SaleTunnel/_styles.scss +9 -8
- package/js/components/SaleTunnel/index.credential.spec.tsx +50 -1
- package/js/components/SaleTunnel/index.full-process-b2b.spec.tsx +5 -5
- package/js/components/SaleTunnel/index.full-process-b2c.spec.tsx +1 -1
- package/js/components/SaleTunnel/index.stories.tsx +1 -1
- package/js/components/Spinner/index.stories.tsx +1 -1
- package/js/components/Tabs/index.stories.tsx +1 -1
- package/js/components/Tabs/index.tsx +2 -1
- package/js/components/TeacherDashboardCourseList/index.tsx +2 -1
- package/js/hooks/useAddressesManagement.tsx +4 -2
- package/js/hooks/useBatchOrder/index.tsx +21 -1
- package/js/hooks/useCreditCards/index.ts +6 -4
- package/js/hooks/useDashboardAddressForm.tsx +3 -3
- package/js/hooks/useDownloadAgreement/index.spec.tsx +136 -0
- package/js/hooks/useDownloadAgreement/index.tsx +25 -0
- package/js/hooks/useDownloadBatchOrderSeats/index.spec.tsx +132 -0
- package/js/hooks/useDownloadBatchOrderSeats/index.tsx +24 -0
- package/js/hooks/useMatchMedia.ts +1 -1
- package/js/hooks/useResources/useResourcesRoot.ts +1 -1
- package/js/hooks/useUnionResource/index.ts +6 -2
- package/js/hooks/useUnionResource/utils/fetchEntity.ts +1 -0
- package/js/pages/DashboardAddressesManagement/DashboardAddressBox.tsx +3 -3
- package/js/pages/DashboardAddressesManagement/DashboardCreateAddress.stories.tsx +1 -1
- package/js/pages/DashboardAddressesManagement/DashboardCreateAddress.tsx +1 -1
- package/js/pages/DashboardAddressesManagement/DashboardEditAddress.stories.tsx +1 -1
- package/js/pages/DashboardAddressesManagement/DashboardEditAddress.tsx +3 -2
- package/js/pages/DashboardAddressesManagement/index.stories.tsx +1 -1
- package/js/pages/DashboardAddressesManagement/index.tsx +1 -1
- package/js/pages/DashboardBatchOrderLayout/index.spec.tsx +19 -2
- package/js/pages/DashboardCourses/index.tsx +2 -1
- package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.stories.tsx +1 -1
- package/js/pages/DashboardCreditCardsManagement/DashboardCreditCardBox.tsx +3 -3
- package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.stories.tsx +1 -1
- package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.tsx +3 -2
- package/js/pages/DashboardCreditCardsManagement/index.stories.tsx +1 -1
- package/js/pages/DashboardOpenEdxProfile/index.stories.tsx +1 -1
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -2
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +2 -1
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +8 -9
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +14 -3
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +6 -1
- package/js/pages/TeacherDashboardCourseLoader/CourseRunList/utils.tsx +2 -1
- package/js/pages/TeacherDashboardOrganizationAgreements/BulkAgreementContractButton.tsx +4 -2
- package/js/pages/TeacherDashboardOrganizationAgreements/SignOrganizationAgreementButton.tsx +2 -1
- package/js/pages/TeacherDashboardOrganizationQuotes/BatchOrderSeatInfoQuote.tsx +112 -0
- package/js/pages/TeacherDashboardOrganizationQuotes/_styles.scss +17 -0
- package/js/pages/TeacherDashboardOrganizationQuotes/index.full-process.spec.tsx +7 -4
- package/js/pages/TeacherDashboardOrganizationQuotes/index.spec.tsx +8 -4
- package/js/pages/TeacherDashboardOrganizationQuotes/index.tsx +39 -26
- package/js/translations/ar-SA.json +1 -1
- package/js/translations/es-ES.json +1 -1
- package/js/translations/fa-IR.json +1 -1
- package/js/translations/fr-CA.json +1 -1
- package/js/translations/fr-FR.json +1 -1
- package/js/translations/ko-KR.json +1 -1
- package/js/translations/pt-PT.json +1 -1
- package/js/translations/ru-RU.json +1 -1
- package/js/translations/vi-VN.json +1 -1
- package/js/types/Joanie.ts +22 -2
- package/js/utils/ProductHelper/index.spec.ts +1 -1
- package/js/utils/StorybookHelper/index.tsx +3 -6
- package/js/utils/cunningham-tokens.ts +1111 -142
- package/js/utils/download.ts +3 -1
- package/js/utils/errors/handle.spec.ts +3 -3
- package/js/utils/react-query/useSessionMutation/index.ts +8 -3
- package/js/utils/test/factories/joanie.ts +16 -2
- package/js/widgets/Dashboard/components/DashboardAvatar/index.stories.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardBox/index.stories.tsx +13 -5
- package/js/widgets/Dashboard/components/DashboardBreadcrumbs/index.stories.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardBreadcrumbs/index.tsx +3 -2
- package/js/widgets/Dashboard/components/DashboardCard/index.spec.tsx +13 -2
- package/js/widgets/Dashboard/components/DashboardCard/index.stories.tsx +12 -4
- package/js/widgets/Dashboard/components/DashboardCard/index.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderAgreementInfo.tsx +72 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderPaymentModal/BatchOrderPaymentManager.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderPaymentModal/index.tsx +2 -1
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderSeatInfo.spec.tsx +114 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderSeatInfo.tsx +133 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/DashboardBatchOrderSubItems.tsx +17 -1
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/batchOrderSeatInfoMessages.ts +24 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/index.tsx +16 -3
- package/js/widgets/Dashboard/components/DashboardItem/Certificate/index.stories.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Contract/index.stories.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.stories.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -4
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.stories.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderReadonly.stories.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderWritable.stories.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/_styles.scss +2 -2
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +2 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrganizationBlock/index.tsx +6 -3
- package/js/widgets/Dashboard/components/DashboardItem/_styles.scss +6 -2
- package/js/widgets/Dashboard/components/DashboardItem/index.stories.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/index.tsx +2 -1
- package/js/widgets/Dashboard/components/DashboardListAvatar/index.stories.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardSidebar/index.stories.tsx +1 -1
- package/js/widgets/Dashboard/components/LearnerDashboardSidebar/index.stories.tsx +1 -1
- package/js/widgets/Dashboard/components/ProtectedOutlet/AuthenticatedOutlet.spec.tsx +1 -1
- package/js/widgets/Dashboard/components/ProtectedOutlet/ProtectedOutlet.spec.tsx +1 -1
- package/js/widgets/Dashboard/components/SearchBar/index.tsx +2 -1
- package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.stories.tsx +1 -1
- package/js/widgets/Dashboard/components/TeacherDashboardProfileSidebar/index.stories.tsx +1 -1
- package/js/widgets/Dashboard/hooks/useRouteInfo/index.spec.tsx +2 -2
- package/js/widgets/Dashboard/index.spec.tsx +1 -1
- package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +2 -2
- package/js/widgets/Search/components/SearchFilterValueParent/index.stories.tsx +1 -1
- package/js/widgets/Search/components/SearchFiltersPane/index.tsx +2 -1
- package/js/widgets/Slider/index.stories.tsx +1 -1
- package/js/widgets/Slider/index.tsx +7 -6
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCertificateItem/index.stories.tsx +1 -1
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.stories.tsx +1 -1
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +1 -1
- package/js/widgets/SyllabusCourseRunsList/components/CourseWishButton/index.tsx +4 -2
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.stories.tsx +1 -1
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.stories.tsx +41 -0
- package/js/widgets/UserLogin/index.stories.tsx +1 -1
- package/package.json +76 -81
- package/scss/components/_subheader.scss +1 -1
- package/scss/components/templates/richie/slider/_slider.scss +1 -1
- package/scss/objects/_course_glimpses.scss +1 -0
- package/scss/objects/_dashboard.scss +77 -0
- package/scss/trumps/_bootstrap.scss +1 -0
- package/scss/vendors/css/cunningham-tokens.css +1259 -154
- package/scss/vendors/cunningham-tokens.scss +1479 -150
- package/tsconfig.json +1 -1
- package/webpack.config.js +8 -0
|
@@ -75,7 +75,8 @@ export const AddressSelector = () => {
|
|
|
75
75
|
<Button
|
|
76
76
|
size="small"
|
|
77
77
|
icon={<span className="material-icons">edit</span>}
|
|
78
|
-
color="
|
|
78
|
+
color="brand"
|
|
79
|
+
variant="tertiary"
|
|
79
80
|
onClick={editFormModal.open}
|
|
80
81
|
fullWidth={isMobile}
|
|
81
82
|
>
|
|
@@ -85,7 +86,8 @@ export const AddressSelector = () => {
|
|
|
85
86
|
<Button
|
|
86
87
|
size="small"
|
|
87
88
|
icon={<span className="material-icons">add</span>}
|
|
88
|
-
color="
|
|
89
|
+
color="brand"
|
|
90
|
+
variant="primary"
|
|
89
91
|
onClick={createFormModal.open}
|
|
90
92
|
fullWidth={isMobile}
|
|
91
93
|
>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|
3
|
-
import { FormProvider, useForm } from 'react-hook-form';
|
|
3
|
+
import { FormProvider, Resolver, useForm } from 'react-hook-form';
|
|
4
4
|
import { yupResolver } from '@hookform/resolvers/yup';
|
|
5
5
|
import * as Yup from 'yup';
|
|
6
6
|
import { Step, StepLabel, Stepper } from '@mui/material';
|
|
@@ -96,7 +96,7 @@ export const SaleTunnelInformationGroup = () => {
|
|
|
96
96
|
);
|
|
97
97
|
};
|
|
98
98
|
|
|
99
|
-
const EMAIL_REGEX = /^[\w
|
|
99
|
+
const EMAIL_REGEX = /^[\w.+\-]+@([\w-]+\.)+[\w-]{2,4}$/;
|
|
100
100
|
|
|
101
101
|
export const validationSchema = Yup.object().shape({
|
|
102
102
|
offering_id: Yup.string().required(),
|
|
@@ -197,7 +197,7 @@ const BatchOrderForm = () => {
|
|
|
197
197
|
const form = useForm<BatchOrder>({
|
|
198
198
|
defaultValues: batchOrder || defaultValues,
|
|
199
199
|
mode: 'onBlur',
|
|
200
|
-
resolver: yupResolver(validationSchema)
|
|
200
|
+
resolver: yupResolver(validationSchema) as Resolver<BatchOrder>,
|
|
201
201
|
});
|
|
202
202
|
const { watch } = form;
|
|
203
203
|
const values = watch();
|
|
@@ -268,7 +268,8 @@ const BatchOrderForm = () => {
|
|
|
268
268
|
}
|
|
269
269
|
}}
|
|
270
270
|
hidden={activeStep === 0}
|
|
271
|
-
color="
|
|
271
|
+
color="brand"
|
|
272
|
+
variant="tertiary"
|
|
272
273
|
>
|
|
273
274
|
<FormattedMessage {...messages.previousButton} />
|
|
274
275
|
</Button>
|
|
@@ -143,7 +143,7 @@ const messages = defineMessages({
|
|
|
143
143
|
id: 'components.SaleTunnel.Information.cpf.description',
|
|
144
144
|
description: 'Explanatory text for the CPF payment option',
|
|
145
145
|
defaultMessage:
|
|
146
|
-
'
|
|
146
|
+
'Purchase your training course by using your Personal Training Account (CPF) on Mon Compte Formation.',
|
|
147
147
|
},
|
|
148
148
|
cpfButtonLabel: {
|
|
149
149
|
id: 'components.SaleTunnel.Information.cpf.buttonLabel',
|
|
@@ -236,7 +236,12 @@ export const SaleTunnelInformationSingular = () => {
|
|
|
236
236
|
</div>
|
|
237
237
|
)}
|
|
238
238
|
{paymentMode === PaymentMode.CPF ? (
|
|
239
|
-
<CpfPayment
|
|
239
|
+
<CpfPayment
|
|
240
|
+
deepLink={deepLink!}
|
|
241
|
+
discount={discount}
|
|
242
|
+
voucherError={voucherError}
|
|
243
|
+
setVoucherError={setVoucherError}
|
|
244
|
+
/>
|
|
240
245
|
) : (
|
|
241
246
|
<>
|
|
242
247
|
{needsPayment && (
|
|
@@ -443,7 +448,13 @@ const Voucher = ({
|
|
|
443
448
|
label={intl.formatMessage(messages.voucherTitle)}
|
|
444
449
|
disabled={!!voucherCode}
|
|
445
450
|
/>
|
|
446
|
-
<Button
|
|
451
|
+
<Button
|
|
452
|
+
size="small"
|
|
453
|
+
color="brand"
|
|
454
|
+
variant="primary"
|
|
455
|
+
onClick={submitVoucher}
|
|
456
|
+
disabled={!!voucherCode}
|
|
457
|
+
>
|
|
447
458
|
<FormattedMessage {...messages.voucherValidate} />
|
|
448
459
|
</Button>
|
|
449
460
|
</div>
|
|
@@ -493,14 +504,24 @@ const PaymentScheduleBlock = ({ schedule }: { schedule: PaymentSchedule }) => {
|
|
|
493
504
|
);
|
|
494
505
|
};
|
|
495
506
|
|
|
496
|
-
const CpfPayment = ({
|
|
507
|
+
const CpfPayment = ({
|
|
508
|
+
deepLink,
|
|
509
|
+
discount,
|
|
510
|
+
voucherError,
|
|
511
|
+
setVoucherError,
|
|
512
|
+
}: {
|
|
513
|
+
deepLink: string;
|
|
514
|
+
discount?: string;
|
|
515
|
+
voucherError: HttpError | null;
|
|
516
|
+
setVoucherError: (value: HttpError | null) => void;
|
|
517
|
+
}) => {
|
|
497
518
|
return (
|
|
498
519
|
<div className="sale-tunnel__cpf">
|
|
499
520
|
<p className="description mb-s">
|
|
500
521
|
<FormattedMessage {...messages.cpfDescription} />
|
|
501
522
|
</p>
|
|
502
523
|
<Button
|
|
503
|
-
color="
|
|
524
|
+
color="brand"
|
|
504
525
|
fullWidth={true}
|
|
505
526
|
href={deepLink}
|
|
506
527
|
target="_blank"
|
|
@@ -508,6 +529,7 @@ const CpfPayment = ({ deepLink }: { deepLink: string }) => {
|
|
|
508
529
|
>
|
|
509
530
|
<FormattedMessage {...messages.cpfButtonLabel} />
|
|
510
531
|
</Button>
|
|
532
|
+
<Voucher discount={discount} voucherError={voucherError} setVoucherError={setVoucherError} />
|
|
511
533
|
</div>
|
|
512
534
|
);
|
|
513
535
|
};
|
|
@@ -20,7 +20,7 @@ const messages = defineMessages({
|
|
|
20
20
|
},
|
|
21
21
|
successDetailMessage: {
|
|
22
22
|
defaultMessage:
|
|
23
|
-
'You
|
|
23
|
+
'You’ll be able to start your training once the first installment has been paid, or once access opens if no payment is required.',
|
|
24
24
|
description: 'Text to explain when the user will be able to start its training.',
|
|
25
25
|
id: 'components.SaleTunnelSuccess.successDetailMessage',
|
|
26
26
|
},
|
|
@@ -110,9 +110,6 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
|
|
|
110
110
|
paymentMode,
|
|
111
111
|
} = useSaleTunnelContext();
|
|
112
112
|
|
|
113
|
-
if (paymentMode === PaymentMode.CPF) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
113
|
const { methods: orderMethods } = useOrders(undefined, { enabled: false });
|
|
117
114
|
const { methods: batchOrderMethods } = useBatchOrder();
|
|
118
115
|
const [state, setState] = useState<ComponentStates>(ComponentStates.IDLE);
|
|
@@ -138,12 +135,17 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
|
|
|
138
135
|
return;
|
|
139
136
|
}
|
|
140
137
|
|
|
141
|
-
if (!billingAddress && needsPayment) {
|
|
138
|
+
if (!billingAddress && needsPayment && paymentMode !== PaymentMode.CPF) {
|
|
142
139
|
handleError(SubscriptionErrorMessageId.ERROR_ADDRESS);
|
|
143
140
|
return;
|
|
144
141
|
}
|
|
145
142
|
|
|
146
|
-
if (
|
|
143
|
+
if (
|
|
144
|
+
!saleTunnelProps.isWithdrawable &&
|
|
145
|
+
!hasWaivedWithdrawalRight &&
|
|
146
|
+
needsPayment &&
|
|
147
|
+
paymentMode !== PaymentMode.CPF
|
|
148
|
+
) {
|
|
147
149
|
handleError(SubscriptionErrorMessageId.ERROR_WITHDRAWAL_RIGHT);
|
|
148
150
|
return;
|
|
149
151
|
}
|
|
@@ -201,6 +203,7 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
|
|
|
201
203
|
};
|
|
202
204
|
|
|
203
205
|
const walkthroughMessages = useMemo(() => {
|
|
206
|
+
if (batchOrder) return;
|
|
204
207
|
if (product.contract_definition && product.price > 0) {
|
|
205
208
|
return messages.walkthroughToSignAndSavePayment;
|
|
206
209
|
} else if (product.contract_definition && product.price === 0) {
|
|
@@ -208,7 +211,7 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
|
|
|
208
211
|
} else if (!product.contract_definition && product.price > 0 && needsPayment) {
|
|
209
212
|
return messages.walkthroughToSavePayment;
|
|
210
213
|
}
|
|
211
|
-
}, [product, creditCard, needsPayment]);
|
|
214
|
+
}, [product, creditCard, needsPayment, batchOrder]);
|
|
212
215
|
|
|
213
216
|
useEffect(() => {
|
|
214
217
|
if (order) nextStep();
|
|
@@ -32,12 +32,13 @@
|
|
|
32
32
|
&__right {
|
|
33
33
|
flex: 1;
|
|
34
34
|
overflow: hidden;
|
|
35
|
+
position: relative;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
&__column {
|
|
38
39
|
display: flex;
|
|
39
40
|
flex-direction: column;
|
|
40
|
-
gap: var(--c--
|
|
41
|
+
gap: var(--c--globals--spacings--b);
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -135,7 +136,7 @@
|
|
|
135
136
|
margin-top: 1rem;
|
|
136
137
|
display: flex;
|
|
137
138
|
justify-content: space-between;
|
|
138
|
-
font-size: var(--c--
|
|
139
|
+
font-size: var(--c--globals--font--sizes--md);
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
|
|
@@ -159,8 +160,8 @@
|
|
|
159
160
|
gap: 0.5rem;
|
|
160
161
|
|
|
161
162
|
.title {
|
|
162
|
-
font-weight: var(--c--
|
|
163
|
-
font-size: var(--c--
|
|
163
|
+
font-weight: var(--c--globals--font--weights--bold);
|
|
164
|
+
font-size: var(--c--globals--font--sizes--md);
|
|
164
165
|
margin: 0 0 0.5rem;
|
|
165
166
|
}
|
|
166
167
|
|
|
@@ -209,17 +210,17 @@
|
|
|
209
210
|
}
|
|
210
211
|
|
|
211
212
|
.block-title {
|
|
212
|
-
font-size: var(--c--
|
|
213
|
+
font-size: var(--c--globals--font--sizes--md);
|
|
213
214
|
color: r-theme-val(sale-tunnel, title-color);
|
|
214
|
-
font-weight: var(--c--
|
|
215
|
+
font-weight: var(--c--globals--font--weights--extrabold);
|
|
215
216
|
text-align: left;
|
|
216
|
-
font-family: var(--c--
|
|
217
|
+
font-family: var(--c--globals--font--families--accent);
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
.sub-block-title {
|
|
220
221
|
font-size: 12px;
|
|
221
222
|
color: r-theme-val(sale-tunnel, title-color);
|
|
222
|
-
font-weight: var(--c--
|
|
223
|
+
font-weight: var(--c--globals--font--weights--bold);
|
|
223
224
|
text-align: left;
|
|
224
225
|
}
|
|
225
226
|
|
|
@@ -166,7 +166,9 @@ describe('SaleTunnel / Credential', () => {
|
|
|
166
166
|
|
|
167
167
|
// - CPF description and redirect button should be visible.
|
|
168
168
|
expect(
|
|
169
|
-
screen.getByText(
|
|
169
|
+
screen.getByText(
|
|
170
|
+
/purchase your training course by using your Personal Training Account \(CPF\) on Mon Compte Formation/i,
|
|
171
|
+
),
|
|
170
172
|
).toBeInTheDocument();
|
|
171
173
|
const cpfButton = screen.getByRole('link', { name: /go to mon compte formation/i });
|
|
172
174
|
|
|
@@ -222,4 +224,51 @@ describe('SaleTunnel / Credential', () => {
|
|
|
222
224
|
// - Classic billing information section should be displayed.
|
|
223
225
|
expect(screen.getByText(/this information will be used for billing/i)).toBeInTheDocument();
|
|
224
226
|
});
|
|
227
|
+
|
|
228
|
+
it('should display voucher input and subscribe button in CPF mode', async () => {
|
|
229
|
+
const course = PacedCourseFactory().one();
|
|
230
|
+
const product = CredentialProductFactory().one();
|
|
231
|
+
const billingAddress: Joanie.Address = AddressFactory({ is_main: true }).one();
|
|
232
|
+
const deepLink = 'https://placeholder.com/course/1';
|
|
233
|
+
const orderQueryParameters = {
|
|
234
|
+
course_code: course.code,
|
|
235
|
+
product_id: product.id,
|
|
236
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
fetchMock
|
|
240
|
+
.get(
|
|
241
|
+
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|
|
242
|
+
[],
|
|
243
|
+
)
|
|
244
|
+
.get(
|
|
245
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
246
|
+
[],
|
|
247
|
+
)
|
|
248
|
+
.get(
|
|
249
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
250
|
+
{ deep_link: deepLink },
|
|
251
|
+
)
|
|
252
|
+
.get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
|
|
253
|
+
overwriteRoutes: true,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const user = userEvent.setup({ delay: null });
|
|
257
|
+
|
|
258
|
+
render(<Wrapper product={product} course={course} />, {
|
|
259
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await screen.findByRole('heading', { level: 3, name: /payment method/i });
|
|
263
|
+
|
|
264
|
+
// Switch to CPF mode
|
|
265
|
+
await user.click(screen.getByRole('radio', { name: /my training account \(cpf\)/i }));
|
|
266
|
+
|
|
267
|
+
// - Voucher input should be visible in CPF mode.
|
|
268
|
+
expect(screen.getByLabelText('Voucher code')).toBeInTheDocument();
|
|
269
|
+
expect(screen.getByRole('button', { name: 'Validate' })).toBeInTheDocument();
|
|
270
|
+
|
|
271
|
+
// - Subscribe button should be visible in CPF mode.
|
|
272
|
+
expect(screen.getByRole('button', { name: 'Subscribe' })).toBeInTheDocument();
|
|
273
|
+
});
|
|
225
274
|
});
|
|
@@ -210,11 +210,11 @@ describe('SaleTunnel', () => {
|
|
|
210
210
|
await user.type($lastName, 'Doe');
|
|
211
211
|
await user.type($firstName, 'John');
|
|
212
212
|
await user.type($role, 'HR');
|
|
213
|
-
await user.type($email, 'john.doe@fun-mooc.com');
|
|
213
|
+
await user.type($email, 'john.doe+admin@fun-mooc.com');
|
|
214
214
|
await user.type($phone, '+338203920103');
|
|
215
215
|
|
|
216
216
|
expect($lastName).toHaveValue('Doe');
|
|
217
|
-
expect($email).toHaveValue('john.doe@fun-mooc.com');
|
|
217
|
+
expect($email).toHaveValue('john.doe+admin@fun-mooc.com');
|
|
218
218
|
|
|
219
219
|
// Signatory step
|
|
220
220
|
await user.click(screen.getByRole('button', { name: 'Next' }));
|
|
@@ -227,7 +227,7 @@ describe('SaleTunnel', () => {
|
|
|
227
227
|
await user.type($signatoryLastName, 'Doe');
|
|
228
228
|
await user.type($signatoryFirstName, 'John');
|
|
229
229
|
await user.type($signatoryRole, 'CEO');
|
|
230
|
-
await user.type($signatoryEmail, 'john.doe@fun-mooc.com');
|
|
230
|
+
await user.type($signatoryEmail, 'john.doe+ceo@fun-mooc.com');
|
|
231
231
|
await user.type($signatoryPhone, '+338203920103');
|
|
232
232
|
|
|
233
233
|
// Participants step
|
|
@@ -298,12 +298,12 @@ describe('SaleTunnel', () => {
|
|
|
298
298
|
administrative_lastname: 'Doe',
|
|
299
299
|
administrative_firstname: 'John',
|
|
300
300
|
administrative_profession: 'HR',
|
|
301
|
-
administrative_email: 'john.doe@fun-mooc.com',
|
|
301
|
+
administrative_email: 'john.doe+admin@fun-mooc.com',
|
|
302
302
|
administrative_telephone: '+338203920103',
|
|
303
303
|
signatory_lastname: 'Doe',
|
|
304
304
|
signatory_firstname: 'John',
|
|
305
305
|
signatory_profession: 'CEO',
|
|
306
|
-
signatory_email: 'john.doe@fun-mooc.com',
|
|
306
|
+
signatory_email: 'john.doe+ceo@fun-mooc.com',
|
|
307
307
|
signatory_telephone: '+338203920103',
|
|
308
308
|
nb_seats: '13',
|
|
309
309
|
payment_method: PaymentMethod.PURCHASE_ORDER,
|
|
@@ -404,7 +404,7 @@ describe('SaleTunnel', () => {
|
|
|
404
404
|
await screen.findByTestId('generic-sale-tunnel-success-step');
|
|
405
405
|
screen.getByText('Subscription confirmed!');
|
|
406
406
|
screen.getByText(
|
|
407
|
-
/
|
|
407
|
+
/you’ll be able to start your training once the first installment has been paid, or once access opens if no payment is required\./i,
|
|
408
408
|
);
|
|
409
409
|
screen.getByRole('link', { name: 'Close' });
|
|
410
410
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { defineMessages, useIntl } from 'react-intl';
|
|
2
|
-
import { MutateOptions } from '@tanstack/react-query';
|
|
3
2
|
import { useAddresses } from 'hooks/useAddresses';
|
|
4
3
|
import * as Joanie from 'types/Joanie';
|
|
5
4
|
import { confirm } from 'utils/indirection/window';
|
|
@@ -58,7 +57,10 @@ export function useAddressesManagement() {
|
|
|
58
57
|
* @param {Joanie.Address} address
|
|
59
58
|
* @param {AddressesMutateOptions} options
|
|
60
59
|
*/
|
|
61
|
-
const remove = (
|
|
60
|
+
const remove = (
|
|
61
|
+
address: Joanie.Address,
|
|
62
|
+
options?: Parameters<typeof addresses.methods.delete>[1],
|
|
63
|
+
) => {
|
|
62
64
|
if (address.is_main) {
|
|
63
65
|
addresses.methods.setError(intl.formatMessage(messages.errorCannotRemoveMain));
|
|
64
66
|
return;
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { defineMessages } from 'react-intl';
|
|
2
2
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
3
3
|
import { ResourcesQuery, useResource, useResources, UseResourcesProps } from 'hooks/useResources';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
API,
|
|
6
|
+
BatchOrderQueryFilters,
|
|
7
|
+
BatchOrderRead,
|
|
8
|
+
BatchOrderSeat,
|
|
9
|
+
BatchOrderSeatsQueryFilters,
|
|
10
|
+
} from 'types/Joanie';
|
|
5
11
|
|
|
6
12
|
const messages = defineMessages({
|
|
7
13
|
errorCreate: {
|
|
@@ -34,3 +40,17 @@ export const useBatchOrdersActions = () => {
|
|
|
34
40
|
submitForPayment,
|
|
35
41
|
};
|
|
36
42
|
};
|
|
43
|
+
|
|
44
|
+
const seatsProps: UseResourcesProps<
|
|
45
|
+
BatchOrderSeat,
|
|
46
|
+
BatchOrderSeatsQueryFilters,
|
|
47
|
+
API['user']['batchOrders']['seats']
|
|
48
|
+
> = {
|
|
49
|
+
queryKey: ['batchOrders', 'seats'],
|
|
50
|
+
apiInterface: () => useJoanieApi().user.batchOrders.seats,
|
|
51
|
+
session: true,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const useBatchOrderSeats = useResources<BatchOrderSeat, BatchOrderSeatsQueryFilters>(
|
|
55
|
+
seatsProps,
|
|
56
|
+
);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { defineMessages, useIntl } from 'react-intl';
|
|
2
2
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
3
|
-
import { MutateOptions } from '@tanstack/query-core';
|
|
4
3
|
import { API, CreditCard } from 'types/Joanie';
|
|
5
4
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
6
5
|
import { useSessionMutation } from 'utils/react-query/useSessionMutation';
|
|
@@ -84,10 +83,13 @@ const useCreditCardResources =
|
|
|
84
83
|
* If the error is a 409, it means the credit card is used to pay at least one order
|
|
85
84
|
* and the user should be informed about that.
|
|
86
85
|
*/
|
|
87
|
-
const deleteMutateAsync = async (
|
|
86
|
+
const deleteMutateAsync = async (
|
|
87
|
+
creditCard: CreditCard,
|
|
88
|
+
options?: Parameters<typeof custom.methods.delete>[1],
|
|
89
|
+
) => {
|
|
88
90
|
return custom.methods.delete(creditCard.id, {
|
|
89
91
|
...options,
|
|
90
|
-
onError: (error: HttpError, variables, context) => {
|
|
92
|
+
onError: (error: HttpError, variables, context, mutationContext) => {
|
|
91
93
|
if (error.code === HttpStatusCode.CONFLICT) {
|
|
92
94
|
custom.methods.setError(
|
|
93
95
|
intl.formatMessage(messages.errorCannotDelete, {
|
|
@@ -97,7 +99,7 @@ const useCreditCardResources =
|
|
|
97
99
|
} else {
|
|
98
100
|
custom.methods.setError(intl.formatMessage(messages.errorDelete));
|
|
99
101
|
}
|
|
100
|
-
options?.onError?.(error, variables, context);
|
|
102
|
+
options?.onError?.(error, variables, context, mutationContext);
|
|
101
103
|
},
|
|
102
104
|
});
|
|
103
105
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { yupResolver } from '@hookform/resolvers/yup
|
|
2
|
-
import { FormProvider, useForm } from 'react-hook-form';
|
|
1
|
+
import { yupResolver } from '@hookform/resolvers/yup';
|
|
2
|
+
import { FormProvider, Resolver, useForm } from 'react-hook-form';
|
|
3
3
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
|
4
4
|
import * as Yup from 'yup';
|
|
5
5
|
import countries from 'i18n-iso-countries';
|
|
@@ -60,7 +60,7 @@ export const useDashboardAddressForm = (address?: Address) => {
|
|
|
60
60
|
defaultValues: address || defaultValues,
|
|
61
61
|
mode: 'onBlur',
|
|
62
62
|
reValidateMode: 'onChange',
|
|
63
|
-
resolver: yupResolver(validationSchema)
|
|
63
|
+
resolver: yupResolver(validationSchema) as Resolver<AddressFormValues>,
|
|
64
64
|
});
|
|
65
65
|
const { register, handleSubmit, formState } = form;
|
|
66
66
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import fetchMock from 'fetch-mock';
|
|
3
|
+
import { PropsWithChildren } from 'react';
|
|
4
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
5
|
+
import { IntlProvider } from 'react-intl';
|
|
6
|
+
import { act, fireEvent, renderHook, waitFor } from '@testing-library/react';
|
|
7
|
+
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
8
|
+
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
9
|
+
import { useDownloadAgreement } from 'hooks/useDownloadAgreement/index';
|
|
10
|
+
import { handle } from 'utils/errors/handle';
|
|
11
|
+
import { SessionProvider } from 'contexts/SessionContext';
|
|
12
|
+
import { Deferred } from 'utils/test/deferred';
|
|
13
|
+
import { OrganizationFactory } from 'utils/test/factories/joanie';
|
|
14
|
+
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
15
|
+
|
|
16
|
+
jest.mock('utils/errors/handle');
|
|
17
|
+
jest.mock('utils/context', () => ({
|
|
18
|
+
__esModule: true,
|
|
19
|
+
default: mockRichieContextFactory({
|
|
20
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
|
|
21
|
+
joanie_backend: { endpoint: 'https://joanie.test' },
|
|
22
|
+
}).one(),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const mockHandle = handle as jest.MockedFn<typeof handle>;
|
|
26
|
+
|
|
27
|
+
describe('useDownloadAgreement', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
fetchMock.get('https://joanie.test/api/v1.0/orders/', []);
|
|
30
|
+
fetchMock.get('https://joanie.test/api/v1.0/addresses/', []);
|
|
31
|
+
fetchMock.get('https://joanie.test/api/v1.0/credit-cards/', []);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
beforeAll(() => {
|
|
35
|
+
// eslint-disable-next-line compat/compat
|
|
36
|
+
URL.createObjectURL = jest.fn();
|
|
37
|
+
// eslint-disable-next-line compat/compat
|
|
38
|
+
URL.revokeObjectURL = jest.fn();
|
|
39
|
+
HTMLAnchorElement.prototype.click = jest.fn();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
jest.clearAllMocks();
|
|
44
|
+
fetchMock.restore();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const Wrapper = ({ children }: PropsWithChildren) => {
|
|
48
|
+
return (
|
|
49
|
+
<QueryClientProvider client={createTestQueryClient({ user: true })}>
|
|
50
|
+
<IntlProvider locale="en">
|
|
51
|
+
<SessionProvider>{children}</SessionProvider>
|
|
52
|
+
</IntlProvider>
|
|
53
|
+
</QueryClientProvider>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
it('downloads the agreement PDF', async () => {
|
|
58
|
+
const organization = OrganizationFactory().one();
|
|
59
|
+
const agreementId = faker.string.uuid();
|
|
60
|
+
const DOWNLOAD_URL = `https://joanie.test/api/v1.0/organizations/${organization.id}/agreements/${agreementId}/download/`;
|
|
61
|
+
const deferred = new Deferred();
|
|
62
|
+
fetchMock.get(DOWNLOAD_URL, deferred.promise);
|
|
63
|
+
|
|
64
|
+
const { result } = renderHook(() => useDownloadAgreement(), {
|
|
65
|
+
wrapper: Wrapper,
|
|
66
|
+
});
|
|
67
|
+
await waitFor(() => expect(result.current).not.toBeNull());
|
|
68
|
+
|
|
69
|
+
expect(fetchMock.called(DOWNLOAD_URL)).toBe(false);
|
|
70
|
+
// eslint-disable-next-line compat/compat
|
|
71
|
+
expect(URL.createObjectURL).toHaveBeenCalledTimes(0);
|
|
72
|
+
// eslint-disable-next-line compat/compat
|
|
73
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
|
|
74
|
+
expect(result.current.loading).toBe(false);
|
|
75
|
+
|
|
76
|
+
act(() => {
|
|
77
|
+
result.current.download(organization.id, agreementId);
|
|
78
|
+
});
|
|
79
|
+
expect(result.current.loading).toBe(true);
|
|
80
|
+
|
|
81
|
+
deferred.resolve({
|
|
82
|
+
status: HttpStatusCode.OK,
|
|
83
|
+
body: new Blob(['%PDF-1.4']),
|
|
84
|
+
headers: {
|
|
85
|
+
'Content-Disposition': 'attachment; filename="Convention_de_formation.pdf";',
|
|
86
|
+
'Content-Type': 'application/pdf',
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await waitFor(() => {
|
|
91
|
+
expect(fetchMock.called(DOWNLOAD_URL)).toBe(true);
|
|
92
|
+
// eslint-disable-next-line compat/compat
|
|
93
|
+
expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
|
|
94
|
+
// eslint-disable-next-line compat/compat
|
|
95
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
|
|
96
|
+
expect(result.current.loading).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
fireEvent.blur(window);
|
|
100
|
+
// eslint-disable-next-line compat/compat
|
|
101
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('handles an error if agreement download request fails', async () => {
|
|
105
|
+
const organization = OrganizationFactory().one();
|
|
106
|
+
const agreementId = faker.string.uuid();
|
|
107
|
+
const DOWNLOAD_URL = `https://joanie.test/api/v1.0/organizations/${organization.id}/agreements/${agreementId}/download/`;
|
|
108
|
+
fetchMock.get(DOWNLOAD_URL, HttpStatusCode.UNAUTHORIZED);
|
|
109
|
+
|
|
110
|
+
const { result } = renderHook(() => useDownloadAgreement(), {
|
|
111
|
+
wrapper: Wrapper,
|
|
112
|
+
});
|
|
113
|
+
await waitFor(() => expect(result.current).not.toBeNull());
|
|
114
|
+
|
|
115
|
+
expect(fetchMock.called(DOWNLOAD_URL)).toBe(false);
|
|
116
|
+
expect(mockHandle).not.toHaveBeenCalled();
|
|
117
|
+
// eslint-disable-next-line compat/compat
|
|
118
|
+
expect(URL.createObjectURL).toHaveBeenCalledTimes(0);
|
|
119
|
+
// eslint-disable-next-line compat/compat
|
|
120
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
|
|
121
|
+
|
|
122
|
+
act(() => {
|
|
123
|
+
result.current.download(organization.id, agreementId);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await waitFor(() => {
|
|
127
|
+
expect(fetchMock.called(DOWNLOAD_URL)).toBe(true);
|
|
128
|
+
expect(mockHandle).toHaveBeenNthCalledWith(1, new Error('Unauthorized'));
|
|
129
|
+
// eslint-disable-next-line compat/compat
|
|
130
|
+
expect(URL.createObjectURL).toHaveBeenCalledTimes(0);
|
|
131
|
+
// eslint-disable-next-line compat/compat
|
|
132
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
|
|
133
|
+
expect(result.current.loading).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
3
|
+
import { browserDownloadFromBlob } from 'utils/download';
|
|
4
|
+
|
|
5
|
+
export const useDownloadAgreement = () => {
|
|
6
|
+
const [loading, setLoading] = useState(false);
|
|
7
|
+
const API = useJoanieApi();
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
download: async (organizationId: string, agreementId: string) => {
|
|
11
|
+
setLoading(true);
|
|
12
|
+
try {
|
|
13
|
+
await browserDownloadFromBlob(() =>
|
|
14
|
+
API.organizations.agreements.download({
|
|
15
|
+
organization_id: organizationId,
|
|
16
|
+
id: agreementId,
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
} finally {
|
|
20
|
+
setLoading(false);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
loading,
|
|
24
|
+
};
|
|
25
|
+
};
|