richie-education 2.28.2-dev39 → 2.28.2-dev58
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 +11 -2
- package/i18n/locales/ar-SA.json +209 -125
- package/i18n/locales/es-ES.json +210 -126
- package/i18n/locales/fa-IR.json +209 -125
- package/i18n/locales/fr-CA.json +209 -125
- package/i18n/locales/fr-FR.json +209 -125
- package/i18n/locales/ko-KR.json +209 -125
- package/i18n/locales/pt-PT.json +212 -128
- package/i18n/locales/ru-RU.json +209 -125
- package/i18n/locales/vi-VN.json +209 -125
- package/js/api/joanie.ts +14 -17
- package/js/api/lms/dummy.ts +1 -12
- package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
- package/js/components/ContractFrame/AbstractContractFrame.tsx +32 -25
- package/js/components/ContractFrame/LearnerContractFrame.tsx +2 -2
- package/js/components/ContractFrame/_styles.scss +6 -14
- package/js/components/CreditCardSelector/index.spec.tsx +7 -7
- package/js/components/CreditCardSelector/index.tsx +2 -2
- package/js/components/DownloadContractButton/index.spec.tsx +1 -1
- package/js/components/OpenEdxFullNameForm/index.spec.tsx +229 -0
- package/js/components/OpenEdxFullNameForm/index.tsx +7 -7
- package/js/components/PaymentInterfaces/LyraPopIn.tsx +2 -2
- package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
- package/js/components/PaymentInterfaces/__mocks__/index.tsx +1 -4
- package/js/components/PaymentInterfaces/types.ts +5 -2
- package/js/components/PurchaseButton/index.spec.tsx +69 -37
- package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -1
- package/js/components/SaleTunnel/CertificateSaleTunnel/index.tsx +2 -2
- package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +6 -10
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +75 -41
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +0 -30
- package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/_styles.scss +12 -0
- package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +160 -0
- package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +15 -29
- package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.tsx +5 -0
- package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +202 -0
- package/js/components/SaleTunnel/_styles.scss +10 -1
- package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
- package/js/components/SaleTunnel/index.credential.spec.tsx +12 -21
- package/js/components/SaleTunnel/index.full-process.spec.tsx +110 -48
- package/js/components/SaleTunnel/index.spec.tsx +330 -779
- package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +16 -11
- package/js/components/SignContractButton/index.spec.tsx +16 -20
- package/js/components/SignContractButton/index.tsx +3 -1
- package/js/hooks/useCreditCards/index.spec.tsx +70 -6
- package/js/hooks/useCreditCards/index.ts +49 -11
- package/js/hooks/useOrders/index.spec.tsx +322 -0
- package/js/hooks/{useOrders.ts → useOrders/index.ts} +40 -14
- package/js/hooks/useProductOrder/index.spec.tsx +77 -60
- package/js/hooks/useProductOrder/index.tsx +2 -2
- package/js/hooks/useResources/useResourcesRoot.ts +1 -0
- package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.spec.tsx +1 -1
- package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.tsx +4 -2
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +8 -5
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +8 -9
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +1 -1
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +1 -6
- package/js/settings/settings.test.ts +11 -2
- 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 +49 -34
- package/js/utils/OrderHelper/index.ts +38 -42
- package/js/utils/search/getSuggestionsSection/index.spec.ts +3 -2
- package/js/utils/test/factories/joanie.ts +36 -51
- package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -18
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +26 -32
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +11 -6
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +7 -6
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +9 -10
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +3 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +6 -7
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +28 -8
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +4 -6
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +34 -35
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +27 -24
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +18 -73
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +32 -16
- package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +3 -11
- package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +25 -3
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -6
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +7 -14
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.spec.tsx +7 -5
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.tsx +5 -7
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +242 -332
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +12 -13
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +10 -21
- package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +2 -2
- package/package.json +27 -27
- package/scss/components/_index.scss +2 -1
- package/js/components/PaymentButton/_styles.scss +0 -27
- package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -333
- package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +0 -41
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { defineMessages, FormattedMessage } from 'react-intl';
|
|
2
|
+
import { Button } from '@openfun/cunningham-react';
|
|
3
|
+
import { useEffect, useState, useRef } from 'react';
|
|
4
|
+
import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
|
|
5
|
+
import { CreditCard, OrderState } from 'types/Joanie';
|
|
6
|
+
import { Icon, IconTypeEnum } from 'components/Icon';
|
|
7
|
+
import { Payment, PaymentErrorMessageId } from 'components/PaymentInterfaces/types';
|
|
8
|
+
import { useCreditCards } from 'hooks/useCreditCards';
|
|
9
|
+
import { Spinner } from 'components/Spinner';
|
|
10
|
+
import PaymentInterfaces from 'components/PaymentInterfaces';
|
|
11
|
+
import { useOrders } from 'hooks/useOrders';
|
|
12
|
+
import { CreditCardSelector } from 'components/CreditCardSelector';
|
|
13
|
+
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
14
|
+
|
|
15
|
+
const messages = defineMessages({
|
|
16
|
+
title: {
|
|
17
|
+
id: 'components.SaleTunnelSavePaymentMethod.title',
|
|
18
|
+
defaultMessage: 'Define a payment method',
|
|
19
|
+
description: 'Content title',
|
|
20
|
+
},
|
|
21
|
+
description: {
|
|
22
|
+
defaultMessage:
|
|
23
|
+
'This is the last step to validate your subscription, you must define a payment method. This one will be used to debit installments. You will not be charged during this step. Pick an existing payment method or add a new one.',
|
|
24
|
+
description: "Text to explain what the user has to do in the 'save payment method' step",
|
|
25
|
+
id: 'components.SaleTunnelSavePaymentMethod.description',
|
|
26
|
+
},
|
|
27
|
+
cta: {
|
|
28
|
+
defaultMessage: 'Define',
|
|
29
|
+
description: 'Label to the call to action to close sale tunnel',
|
|
30
|
+
id: 'components.SaleTunnelSavePaymentMethod.cta',
|
|
31
|
+
},
|
|
32
|
+
errorAbort: {
|
|
33
|
+
defaultMessage: 'You have aborted the payment.',
|
|
34
|
+
description: 'Error message shown when user aborts the payment.',
|
|
35
|
+
id: 'components.PaymentButton.errorAbort',
|
|
36
|
+
},
|
|
37
|
+
errorDefault: {
|
|
38
|
+
defaultMessage: 'An error occurred during payment. Please retry later.',
|
|
39
|
+
description: 'Error message shown when payment creation request failed.',
|
|
40
|
+
id: 'components.PaymentButton.errorDefault',
|
|
41
|
+
},
|
|
42
|
+
tokenizingPayment: {
|
|
43
|
+
defaultMessage: 'Payment method definition in progress.',
|
|
44
|
+
description: 'Label for screen reader when a credit card is being tokenized.',
|
|
45
|
+
id: 'components.PaymentButton.tokenizingPayment',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const SaleTunnelSavePaymentMethod = () => {
|
|
50
|
+
const initialCreditCards = useRef<CreditCard[]>();
|
|
51
|
+
const pollingTimeoutRef = useRef<NodeJS.Timeout>();
|
|
52
|
+
const JoanieApi = useJoanieApi();
|
|
53
|
+
const [payment, setPayment] = useState<Payment>();
|
|
54
|
+
const [error, setError] = useState<string>();
|
|
55
|
+
const creditCards = useCreditCards();
|
|
56
|
+
const orders = useOrders(undefined, { enabled: false });
|
|
57
|
+
const { order, nextStep, creditCard, setCreditCard } = useSaleTunnelContext();
|
|
58
|
+
|
|
59
|
+
const setPaymentMethod = async (creditCardId: string) => {
|
|
60
|
+
orders.methods.set_payment_method(
|
|
61
|
+
{ id: order!.id, credit_card_id: creditCardId },
|
|
62
|
+
{ onError: () => handleError() },
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const tokenizePaymentMethod = async () => {
|
|
67
|
+
const data = await creditCards.methods.tokenize();
|
|
68
|
+
setPayment(data);
|
|
69
|
+
setError(undefined);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const waitForNewCreditCard = async () => {
|
|
73
|
+
const { results } = await JoanieApi.user.creditCards.get();
|
|
74
|
+
const initialIds = initialCreditCards.current!.map((cc) => cc.id);
|
|
75
|
+
const newCard = results.find((cc) => !initialIds.includes(cc.id));
|
|
76
|
+
|
|
77
|
+
if (!newCard) {
|
|
78
|
+
pollingTimeoutRef.current = setTimeout(waitForNewCreditCard, 1000);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setCreditCard(newCard);
|
|
83
|
+
await setPaymentMethod(newCard.id);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleError = (message: string = PaymentErrorMessageId.ERROR_DEFAULT) => {
|
|
87
|
+
setError(message);
|
|
88
|
+
setPayment(undefined);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!payment) {
|
|
93
|
+
initialCreditCards.current = creditCards.items;
|
|
94
|
+
}
|
|
95
|
+
}, [creditCards]);
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (order?.state !== OrderState.TO_SAVE_PAYMENT_METHOD) {
|
|
99
|
+
nextStep();
|
|
100
|
+
}
|
|
101
|
+
}, [order]);
|
|
102
|
+
|
|
103
|
+
useEffect(
|
|
104
|
+
() => () => {
|
|
105
|
+
clearTimeout(pollingTimeoutRef.current);
|
|
106
|
+
},
|
|
107
|
+
[],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<section
|
|
112
|
+
className="sale-tunnel-step sale-tunnel-step--save-payment-method"
|
|
113
|
+
data-testid="generic-sale-tunnel-save-payment-method-step"
|
|
114
|
+
>
|
|
115
|
+
<header className="sale-tunnel-step__header">
|
|
116
|
+
<Icon name={IconTypeEnum.CREDIT_CARD} />
|
|
117
|
+
<h3 className="sale-tunnel-step__title">
|
|
118
|
+
<FormattedMessage {...messages.title} />
|
|
119
|
+
</h3>
|
|
120
|
+
</header>
|
|
121
|
+
<div className="sale-tunnel-step__content">
|
|
122
|
+
<p className="mb-s">
|
|
123
|
+
<FormattedMessage {...messages.description} />
|
|
124
|
+
</p>
|
|
125
|
+
<CreditCardSelector creditCard={creditCard} setCreditCard={setCreditCard} />
|
|
126
|
+
</div>
|
|
127
|
+
<footer className="sale-tunnel-step__footer">
|
|
128
|
+
<Button
|
|
129
|
+
size="medium"
|
|
130
|
+
disabled={!!payment || orders.states.settingPaymentMethod}
|
|
131
|
+
onClick={creditCard ? () => setPaymentMethod(creditCard.id) : tokenizePaymentMethod}
|
|
132
|
+
>
|
|
133
|
+
{payment || orders.states.settingPaymentMethod ? (
|
|
134
|
+
<Spinner size="small">
|
|
135
|
+
<span id="tokenizing-payment">
|
|
136
|
+
<FormattedMessage {...messages.tokenizingPayment} />
|
|
137
|
+
</span>
|
|
138
|
+
</Spinner>
|
|
139
|
+
) : (
|
|
140
|
+
<FormattedMessage {...messages.cta} />
|
|
141
|
+
)}
|
|
142
|
+
</Button>
|
|
143
|
+
{error && (
|
|
144
|
+
<p className="payment-button__error">
|
|
145
|
+
{messages.hasOwnProperty(error) ? (
|
|
146
|
+
<FormattedMessage {...messages[error as PaymentErrorMessageId]} />
|
|
147
|
+
) : (
|
|
148
|
+
error
|
|
149
|
+
)}
|
|
150
|
+
</p>
|
|
151
|
+
)}
|
|
152
|
+
{payment && (
|
|
153
|
+
<PaymentInterfaces {...payment} onSuccess={waitForNewCreditCard} onError={handleError} />
|
|
154
|
+
)}
|
|
155
|
+
</footer>
|
|
156
|
+
</section>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export default SaleTunnelSavePaymentMethod;
|
|
@@ -5,72 +5,58 @@ import { SuccessIcon } from 'components/SuccessIcon';
|
|
|
5
5
|
import { getDashboardBasename } from 'widgets/Dashboard/hooks/useDashboardRouter/getDashboardBasename';
|
|
6
6
|
import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
|
|
7
7
|
import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRoutesPaths';
|
|
8
|
+
import { ProductType } from 'types/Joanie';
|
|
8
9
|
|
|
9
10
|
const messages = defineMessages({
|
|
10
11
|
congratulations: {
|
|
11
|
-
defaultMessage: '
|
|
12
|
+
defaultMessage: 'Subscription confirmed!',
|
|
12
13
|
description: 'Text displayed to thank user for his order',
|
|
13
14
|
id: 'components.SaleTunnelSuccess.congratulations',
|
|
14
15
|
},
|
|
15
16
|
successMessage: {
|
|
16
|
-
defaultMessage: 'Your order has been successfully
|
|
17
|
+
defaultMessage: 'Your order has been successfully registered.',
|
|
17
18
|
description: 'Message to confirm that order has been created',
|
|
18
19
|
id: 'components.SaleTunnelSuccess.successMessage',
|
|
19
20
|
},
|
|
20
21
|
successDetailMessage: {
|
|
21
|
-
defaultMessage: 'You will receive your invoice by email in a few moments.',
|
|
22
|
-
description: "Text to remind that order's invoice will be send by email soon",
|
|
23
|
-
id: 'components.SaleTunnelSuccess.successDetailMessage',
|
|
24
|
-
},
|
|
25
|
-
successDetailSignatureMessage: {
|
|
26
22
|
defaultMessage:
|
|
27
|
-
'
|
|
28
|
-
description: 'Text to
|
|
29
|
-
id: 'components.SaleTunnelSuccess.
|
|
23
|
+
'You will be able to start your training once the first installment will be paid.',
|
|
24
|
+
description: 'Text to explain when the user will be able to start its training.',
|
|
25
|
+
id: 'components.SaleTunnelSuccess.successDetailMessage',
|
|
30
26
|
},
|
|
31
27
|
cta: {
|
|
32
|
-
defaultMessage: '
|
|
28
|
+
defaultMessage: 'Close',
|
|
33
29
|
description: 'Label to the call to action to close sale tunnel',
|
|
34
30
|
id: 'components.SaleTunnelSuccess.cta',
|
|
35
31
|
},
|
|
36
|
-
ctaSignature: {
|
|
37
|
-
defaultMessage: 'Sign the training contract',
|
|
38
|
-
description: 'Label to the call to action to close sale tunnel if there is a pending signature',
|
|
39
|
-
id: 'components.SaleTunnelSuccess.ctaSignature',
|
|
40
|
-
},
|
|
41
32
|
});
|
|
42
33
|
|
|
43
34
|
export const SaleTunnelSuccess = ({ closeModal }: { closeModal: () => void }) => {
|
|
44
35
|
const intl = useIntl();
|
|
45
36
|
const { order, product } = useSaleTunnelContext();
|
|
37
|
+
|
|
46
38
|
return (
|
|
47
|
-
<section className="sale-tunnel-
|
|
48
|
-
<header className="sale-tunnel-
|
|
39
|
+
<section className="sale-tunnel-step" data-testid="generic-sale-tunnel-success-step">
|
|
40
|
+
<header className="sale-tunnel-step__header">
|
|
49
41
|
<SuccessIcon />
|
|
50
|
-
<h3 className="sale-tunnel-
|
|
42
|
+
<h3 className="sale-tunnel-step__title">
|
|
51
43
|
<FormattedMessage {...messages.congratulations} />
|
|
52
44
|
</h3>
|
|
53
45
|
</header>
|
|
54
|
-
<p className="sale-tunnel-
|
|
46
|
+
<p className="sale-tunnel-step__content">
|
|
55
47
|
<FormattedMessage {...messages.successMessage} />
|
|
56
48
|
<br />
|
|
57
49
|
<FormattedMessage {...messages.successDetailMessage} />
|
|
58
|
-
{product.contract_definition && (
|
|
59
|
-
<>
|
|
60
|
-
<br />
|
|
61
|
-
<FormattedMessage {...messages.successDetailSignatureMessage} />
|
|
62
|
-
</>
|
|
63
|
-
)}
|
|
64
50
|
</p>
|
|
65
|
-
<footer className="sale-tunnel-
|
|
66
|
-
{product.
|
|
51
|
+
<footer className="sale-tunnel-step__footer">
|
|
52
|
+
{product.type === ProductType.CREDENTIAL ? (
|
|
67
53
|
<Button
|
|
68
54
|
href={
|
|
69
55
|
getDashboardBasename(intl.locale) +
|
|
70
56
|
generatePath(LearnerDashboardPaths.ORDER, { orderId: order!.id })
|
|
71
57
|
}
|
|
72
58
|
>
|
|
73
|
-
<FormattedMessage {...messages.
|
|
59
|
+
<FormattedMessage {...messages.cta} />
|
|
74
60
|
</Button>
|
|
75
61
|
) : (
|
|
76
62
|
<Button onClick={closeModal}>
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { useMemo, useEffect, useState } from 'react';
|
|
2
|
+
import { Alert, Button, VariantType } from '@openfun/cunningham-react';
|
|
3
|
+
import { defineMessages, FormattedMessage } from 'react-intl';
|
|
4
|
+
import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
|
|
5
|
+
import { useOrders } from 'hooks/useOrders';
|
|
6
|
+
import { OrderCreationPayload } from 'types/Joanie';
|
|
7
|
+
import { useMatchMediaLg } from 'hooks/useMatchMedia';
|
|
8
|
+
import { SubscriptionErrorMessageId } from 'components/PaymentInterfaces/types';
|
|
9
|
+
import { HttpError } from 'utils/errors/HttpError';
|
|
10
|
+
import { Spinner } from 'components/Spinner';
|
|
11
|
+
|
|
12
|
+
const messages = defineMessages({
|
|
13
|
+
subscribe: {
|
|
14
|
+
id: 'components.SaleTunnel.SubscriptionButton.subscribe',
|
|
15
|
+
defaultMessage: 'Subscribe',
|
|
16
|
+
description: 'Label of the button to subscribe to a product.',
|
|
17
|
+
},
|
|
18
|
+
walkthroughToSignAndSavePayment: {
|
|
19
|
+
id: 'components.SaleTunnel.SubscriptionButton.walkthroughToSignAndSavePayment',
|
|
20
|
+
defaultMessage:
|
|
21
|
+
'To enroll in the training, you will first be invited to sign the training agreement and then to define a payment method.',
|
|
22
|
+
description:
|
|
23
|
+
'Message explaining the subscription process with a training agreement to sign and a payment method to set.',
|
|
24
|
+
},
|
|
25
|
+
walkthroughToSign: {
|
|
26
|
+
id: 'components.SaleTunnel.SubscriptionButton.walkthroughToSign',
|
|
27
|
+
defaultMessage:
|
|
28
|
+
'To enroll in the training, you will be invited to sign the training agreement.',
|
|
29
|
+
description:
|
|
30
|
+
'Message explaining the subscription process with only a training agreement to sign.',
|
|
31
|
+
},
|
|
32
|
+
walkthroughToSavePayment: {
|
|
33
|
+
id: 'components.SaleTunnel.SubscriptionButton.walkthroughToSavePayment',
|
|
34
|
+
defaultMessage: 'To enroll in the training, you will be invited to define a payment method.',
|
|
35
|
+
description: 'Message explaining the subscription process with only a payment method to set.',
|
|
36
|
+
},
|
|
37
|
+
errorDefault: {
|
|
38
|
+
defaultMessage: 'An error occurred during order creation. Please retry later.',
|
|
39
|
+
description: 'Error message shown when order creation request failed.',
|
|
40
|
+
id: 'components.SubscriptionButton.errorDefault',
|
|
41
|
+
},
|
|
42
|
+
errorFullProduct: {
|
|
43
|
+
defaultMessage: 'There are no more places available for this product.',
|
|
44
|
+
description:
|
|
45
|
+
'Error message shown when order creation request failed because there is no remaining available seat for the product.',
|
|
46
|
+
id: 'components.SubscriptionButton.errorFullProduct',
|
|
47
|
+
},
|
|
48
|
+
errorAddress: {
|
|
49
|
+
defaultMessage: 'You must have a billing address.',
|
|
50
|
+
description: "Error message shown when the user didn't select a billing address.",
|
|
51
|
+
id: 'components.SubscriptionButton.errorAddress',
|
|
52
|
+
},
|
|
53
|
+
orderCreationInProgress: {
|
|
54
|
+
defaultMessage: 'Order creation in progress',
|
|
55
|
+
description: 'Label for screen reader when an order creation is in progress.',
|
|
56
|
+
id: 'components.SubscriptionButton.orderCreationInProgress',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
enum ComponentStates {
|
|
61
|
+
IDLE = 'idle',
|
|
62
|
+
LOADING = 'loading',
|
|
63
|
+
ERROR = 'error',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface Props {
|
|
67
|
+
buildOrderPayload: (
|
|
68
|
+
payload: Pick<OrderCreationPayload, 'product_id' | 'billing_address' | 'order_group_id'>,
|
|
69
|
+
) => OrderCreationPayload;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const SubscriptionButton = ({ buildOrderPayload }: Props) => {
|
|
73
|
+
const {
|
|
74
|
+
order,
|
|
75
|
+
creditCard,
|
|
76
|
+
billingAddress,
|
|
77
|
+
product,
|
|
78
|
+
nextStep,
|
|
79
|
+
runSubmitCallbacks,
|
|
80
|
+
props: saleTunnelProps,
|
|
81
|
+
} = useSaleTunnelContext();
|
|
82
|
+
const { methods: orderMethods } = useOrders(undefined, { enabled: false });
|
|
83
|
+
const [state, setState] = useState<ComponentStates>(ComponentStates.IDLE);
|
|
84
|
+
const [error, setError] = useState<SubscriptionErrorMessageId | string>();
|
|
85
|
+
const isMobile = useMatchMediaLg();
|
|
86
|
+
|
|
87
|
+
const handleError = (
|
|
88
|
+
messageId: SubscriptionErrorMessageId | string = SubscriptionErrorMessageId.ERROR_DEFAULT,
|
|
89
|
+
) => {
|
|
90
|
+
setState(ComponentStates.ERROR);
|
|
91
|
+
setError(messageId);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const createOrder = async () => {
|
|
95
|
+
setState(ComponentStates.LOADING);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await runSubmitCallbacks();
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
100
|
+
} catch (_error) {
|
|
101
|
+
setState(ComponentStates.IDLE);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!billingAddress) {
|
|
106
|
+
handleError(SubscriptionErrorMessageId.ERROR_ADDRESS);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const payload = buildOrderPayload({
|
|
111
|
+
product_id: product.id,
|
|
112
|
+
billing_address: billingAddress!,
|
|
113
|
+
order_group_id: saleTunnelProps.orderGroup?.id,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
orderMethods.create(payload, {
|
|
117
|
+
onError: async (createOrderError: HttpError) => {
|
|
118
|
+
if (createOrderError.responseBody) {
|
|
119
|
+
const responseErrors = await createOrderError.responseBody;
|
|
120
|
+
if ('max_validated_orders' in responseErrors) {
|
|
121
|
+
handleError(SubscriptionErrorMessageId.ERROR_FULL_PRODUCT);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
handleError();
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const walkthroughMessages = useMemo(() => {
|
|
130
|
+
if (product.contract_definition && product.price > 0) {
|
|
131
|
+
return messages.walkthroughToSignAndSavePayment;
|
|
132
|
+
} else if (product.contract_definition && product.price === 0) {
|
|
133
|
+
return messages.walkthroughToSign;
|
|
134
|
+
} else if (!product.contract_definition && product.price > 0) {
|
|
135
|
+
return messages.walkthroughToSavePayment;
|
|
136
|
+
}
|
|
137
|
+
}, [product, creditCard]);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (order) nextStep();
|
|
141
|
+
}, [order]);
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (error && [ComponentStates.IDLE, ComponentStates.LOADING].includes(state)) {
|
|
145
|
+
setError(undefined);
|
|
146
|
+
}
|
|
147
|
+
if (state === ComponentStates.ERROR) {
|
|
148
|
+
document.querySelector<HTMLElement>('#sale-tunnel-subscription-error')?.focus();
|
|
149
|
+
}
|
|
150
|
+
}, [state]);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<>
|
|
154
|
+
<div style={{ maxWidth: '680px' }} className="mb-s" data-testid="walkthrough-banner">
|
|
155
|
+
{walkthroughMessages && (
|
|
156
|
+
<Alert type={VariantType.INFO}>
|
|
157
|
+
<FormattedMessage
|
|
158
|
+
{...walkthroughMessages}
|
|
159
|
+
values={{ credictCarNumbers: creditCard?.last_numbers }}
|
|
160
|
+
/>
|
|
161
|
+
</Alert>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
<Button
|
|
165
|
+
onClick={createOrder}
|
|
166
|
+
fullWidth={isMobile}
|
|
167
|
+
disabled={state === ComponentStates.LOADING}
|
|
168
|
+
{...(state === ComponentStates.ERROR && {
|
|
169
|
+
'aria-describedby': 'sale-tunnel-payment-error',
|
|
170
|
+
})}
|
|
171
|
+
>
|
|
172
|
+
{state === ComponentStates.LOADING ? (
|
|
173
|
+
<Spinner theme="light" aria-labelledby="order-creation-in-progress">
|
|
174
|
+
<span id="order-creation-in-progress">
|
|
175
|
+
<FormattedMessage {...messages.orderCreationInProgress} />
|
|
176
|
+
</span>
|
|
177
|
+
</Spinner>
|
|
178
|
+
) : (
|
|
179
|
+
<FormattedMessage {...messages.subscribe} />
|
|
180
|
+
)}
|
|
181
|
+
</Button>
|
|
182
|
+
{state === ComponentStates.ERROR && (
|
|
183
|
+
<p className="subscription-button__error" id="sale-tunnel-subscription-error" tabIndex={-1}>
|
|
184
|
+
{!error || messages.hasOwnProperty(error) ? (
|
|
185
|
+
<FormattedMessage
|
|
186
|
+
{...messages[
|
|
187
|
+
(error as Exclude<
|
|
188
|
+
SubscriptionErrorMessageId,
|
|
189
|
+
SubscriptionErrorMessageId.ERROR_ABORT
|
|
190
|
+
>) || SubscriptionErrorMessageId.ERROR_DEFAULT
|
|
191
|
+
]}
|
|
192
|
+
/>
|
|
193
|
+
) : (
|
|
194
|
+
error
|
|
195
|
+
)}
|
|
196
|
+
</p>
|
|
197
|
+
)}
|
|
198
|
+
</>
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export default SubscriptionButton;
|
|
@@ -105,14 +105,23 @@
|
|
|
105
105
|
text-align: left;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
.sale-tunnel-
|
|
108
|
+
.sale-tunnel-step {
|
|
109
109
|
align-items: center;
|
|
110
110
|
display: flex;
|
|
111
111
|
flex-direction: column;
|
|
112
112
|
|
|
113
113
|
&__header {
|
|
114
|
+
align-items: center;
|
|
115
|
+
display: flex;
|
|
116
|
+
flex-direction: column;
|
|
114
117
|
margin-bottom: 0;
|
|
115
118
|
text-align: center;
|
|
119
|
+
|
|
120
|
+
& > .icon {
|
|
121
|
+
--size: 4.5rem;
|
|
122
|
+
height: var(--size);
|
|
123
|
+
width: var(--size);
|
|
124
|
+
}
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
&__title {
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { Checkbox } from '@openfun/cunningham-react';
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { defineMessages, useIntl } from 'react-intl';
|
|
4
|
-
import { Product } from 'types/Joanie';
|
|
5
|
-
import context from 'utils/context';
|
|
6
|
-
import { PaymentErrorMessageId } from 'components/PaymentInterfaces/types';
|
|
7
|
-
|
|
8
|
-
const messages = defineMessages({
|
|
9
|
-
termsMessage: {
|
|
10
|
-
defaultMessage: 'By checking this box, you accept the ',
|
|
11
|
-
description: 'Message next to the checkbox in order to accept the terms',
|
|
12
|
-
id: 'components.PaymentButton.termsMessage',
|
|
13
|
-
},
|
|
14
|
-
termsMessageLink: {
|
|
15
|
-
defaultMessage: 'General Terms of Sale',
|
|
16
|
-
description: 'Clickable link included in the terms message',
|
|
17
|
-
id: 'components.SaleTunnelStepPayment.termsMessageLink',
|
|
18
|
-
},
|
|
19
|
-
termsMessageLinkTitle: {
|
|
20
|
-
defaultMessage: 'Open a preview of the General Terms of Sale',
|
|
21
|
-
description: 'Title of clickable link included in the terms message',
|
|
22
|
-
id: 'components.SaleTunnelStepPayment.termsMessageLinkTitle',
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
export const useTerms = ({
|
|
27
|
-
product,
|
|
28
|
-
onError,
|
|
29
|
-
error,
|
|
30
|
-
}: {
|
|
31
|
-
product: Product;
|
|
32
|
-
onError: (error: PaymentErrorMessageId | string) => void;
|
|
33
|
-
error?: PaymentErrorMessageId | string;
|
|
34
|
-
}) => {
|
|
35
|
-
const intl = useIntl();
|
|
36
|
-
const [termsAccepted, setTermsAccepted] = useState(false);
|
|
37
|
-
const validateTerms = () => {
|
|
38
|
-
if (!product.contract_definition) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
if (!termsAccepted) {
|
|
42
|
-
onError(PaymentErrorMessageId.ERROR_TERMS);
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
termsAccepted: termsAccepted || !product.contract_definition,
|
|
48
|
-
validateTerms,
|
|
49
|
-
renderTermsCheckbox: () => {
|
|
50
|
-
if (!product.contract_definition) {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
return (
|
|
54
|
-
<section className="payment-button__terms">
|
|
55
|
-
<Checkbox
|
|
56
|
-
label={
|
|
57
|
-
<>
|
|
58
|
-
{intl.formatMessage(messages.termsMessage)}{' '}
|
|
59
|
-
<a
|
|
60
|
-
href={context.site_urls.terms_and_conditions ?? '#'}
|
|
61
|
-
target="_blank"
|
|
62
|
-
rel="noopener noreferrer"
|
|
63
|
-
title={intl.formatMessage(messages.termsMessageLinkTitle)}
|
|
64
|
-
>
|
|
65
|
-
{intl.formatMessage(messages.termsMessageLink)}
|
|
66
|
-
</a>
|
|
67
|
-
</>
|
|
68
|
-
}
|
|
69
|
-
onChange={(e) => setTermsAccepted(e.target.checked)}
|
|
70
|
-
checked={termsAccepted}
|
|
71
|
-
state={error === PaymentErrorMessageId.ERROR_TERMS ? 'error' : 'default'}
|
|
72
|
-
/>
|
|
73
|
-
</section>
|
|
74
|
-
);
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fetchMock from 'fetch-mock';
|
|
2
2
|
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import queryString from 'query-string';
|
|
3
4
|
import {
|
|
4
5
|
RichieContextFactory as mockRichieContextFactory,
|
|
5
6
|
PacedCourseFactory,
|
|
@@ -8,13 +9,13 @@ import {
|
|
|
8
9
|
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
9
10
|
import {
|
|
10
11
|
AddressFactory,
|
|
11
|
-
|
|
12
|
+
CredentialOrderFactory,
|
|
12
13
|
CredentialProductFactory,
|
|
13
14
|
OrderGroupFactory,
|
|
14
15
|
} from 'utils/test/factories/joanie';
|
|
15
16
|
import type * as Joanie from 'types/Joanie';
|
|
16
17
|
import { Maybe } from 'types/utils';
|
|
17
|
-
import { OrderCredentialCreationPayload } from 'types/Joanie';
|
|
18
|
+
import { NOT_CANCELED_ORDER_STATES, OrderCredentialCreationPayload } from 'types/Joanie';
|
|
18
19
|
import { SaleTunnel, SaleTunnelProps } from 'components/SaleTunnel/index';
|
|
19
20
|
import { render } from 'utils/test/render';
|
|
20
21
|
import { getAddressLabel } from 'components/SaleTunnel/AddressSelector';
|
|
@@ -86,26 +87,23 @@ describe('SaleTunnel / Credential', () => {
|
|
|
86
87
|
const billingAddress: Joanie.Address = AddressFactory({ is_main: true }).one();
|
|
87
88
|
|
|
88
89
|
let createOrderPayload: Maybe<OrderCredentialCreationPayload>;
|
|
89
|
-
const {
|
|
90
|
+
const order = CredentialOrderFactory({ order_group_id: orderGroup.id }).one();
|
|
91
|
+
const orderQueryParameters = {
|
|
92
|
+
course_code: course.code,
|
|
93
|
+
product_id: product.id,
|
|
94
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
95
|
+
};
|
|
96
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
|
|
90
97
|
fetchMock
|
|
91
|
-
.get(
|
|
92
|
-
`https://joanie.endpoint/api/v1.0/orders/?course_code=${course.code}&product_id=${product.id}&state=pending&state=validated&state=submitted`,
|
|
93
|
-
[],
|
|
94
|
-
)
|
|
98
|
+
.get(url, [])
|
|
95
99
|
.get(
|
|
96
100
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
97
101
|
[],
|
|
98
102
|
)
|
|
99
|
-
.post('https://joanie.endpoint/api/v1.0/orders/', (
|
|
103
|
+
.post('https://joanie.endpoint/api/v1.0/orders/', (_, { body }) => {
|
|
100
104
|
createOrderPayload = JSON.parse(body as any);
|
|
101
105
|
return order;
|
|
102
106
|
})
|
|
103
|
-
.patch(`https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`, {
|
|
104
|
-
paymentInfo,
|
|
105
|
-
})
|
|
106
|
-
.get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, {
|
|
107
|
-
...order,
|
|
108
|
-
})
|
|
109
107
|
.get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
|
|
110
108
|
overwriteRoutes: true,
|
|
111
109
|
});
|
|
@@ -121,13 +119,6 @@ describe('SaleTunnel / Credential', () => {
|
|
|
121
119
|
name: `Subscribe`,
|
|
122
120
|
}) as HTMLButtonElement;
|
|
123
121
|
|
|
124
|
-
const $terms = screen.getByLabelText(
|
|
125
|
-
'By checking this box, you accept the General Terms of Sale',
|
|
126
|
-
);
|
|
127
|
-
await act(async () => {
|
|
128
|
-
fireEvent.click($terms);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
122
|
// - Payment button should not be disabled.
|
|
132
123
|
expect($button.disabled).toBe(false);
|
|
133
124
|
|