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
|
@@ -9,10 +9,7 @@ const PaymentInterface = ({ onError, onSuccess }: PaymentInterfaceProps) => (
|
|
|
9
9
|
>
|
|
10
10
|
Simulate payment failure
|
|
11
11
|
</button>
|
|
12
|
-
<button
|
|
13
|
-
data-testid="payment-abort"
|
|
14
|
-
onClick={() => onError(PaymentErrorMessageId.ERROR_ABORTING)}
|
|
15
|
-
>
|
|
12
|
+
<button data-testid="payment-abort" onClick={() => onError(PaymentErrorMessageId.ERROR_ABORT)}>
|
|
16
13
|
Simulate payment abort
|
|
17
14
|
</button>
|
|
18
15
|
<button data-testid="payment-success" onClick={onSuccess}>
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export enum PaymentErrorMessageId {
|
|
2
2
|
ERROR_ABORT = 'errorAbort',
|
|
3
|
-
|
|
3
|
+
ERROR_DEFAULT = 'errorDefault',
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export enum SubscriptionErrorMessageId {
|
|
7
|
+
ERROR_ABORT = 'errorAbort',
|
|
4
8
|
ERROR_ADDRESS = 'errorAddress',
|
|
5
9
|
ERROR_DEFAULT = 'errorDefault',
|
|
6
10
|
ERROR_FULL_PRODUCT = 'errorFullProduct',
|
|
7
|
-
ERROR_TERMS = 'errorTerms',
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
export enum PaymentProviders {
|
|
@@ -4,6 +4,7 @@ import { IntlProvider } from 'react-intl';
|
|
|
4
4
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
5
5
|
import userEvent from '@testing-library/user-event';
|
|
6
6
|
import { CunninghamProvider } from '@openfun/cunningham-react';
|
|
7
|
+
import queryString from 'query-string';
|
|
7
8
|
import {
|
|
8
9
|
CourseStateFactory,
|
|
9
10
|
PacedCourseFactory,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
} from 'utils/test/factories/joanie';
|
|
18
19
|
import { SessionProvider } from 'contexts/SessionContext';
|
|
19
20
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
20
|
-
import { ProductType } from 'types/Joanie';
|
|
21
|
+
import { NOT_CANCELED_ORDER_STATES, ProductType } from 'types/Joanie';
|
|
21
22
|
import { Priority } from 'types';
|
|
22
23
|
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
23
24
|
import { User } from 'types/User';
|
|
@@ -102,11 +103,14 @@ describe('PurchaseButton', () => {
|
|
|
102
103
|
it('shows cta to open sale tunnel when user is authenticated', async () => {
|
|
103
104
|
const courseCode = '00000';
|
|
104
105
|
const product = ProductFactory().one();
|
|
106
|
+
const orderQueryParameters = {
|
|
107
|
+
course_code: courseCode,
|
|
108
|
+
product_id: product.id,
|
|
109
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
110
|
+
};
|
|
111
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
|
|
105
112
|
fetchMock
|
|
106
|
-
.get(
|
|
107
|
-
`https://joanie.endpoint/api/v1.0/orders/?course_code=${courseCode}&product_id=${product.id}&state=pending&state=validated&state=submitted`,
|
|
108
|
-
{},
|
|
109
|
-
)
|
|
113
|
+
.get(url, {})
|
|
110
114
|
.get(
|
|
111
115
|
`https://joanie.endpoint/api/v1.0/courses/${courseCode}/products/${product.id}/payment-schedule/`,
|
|
112
116
|
[],
|
|
@@ -142,11 +146,14 @@ describe('PurchaseButton', () => {
|
|
|
142
146
|
const product = ProductFactory({ remaining_order_count: null }).one();
|
|
143
147
|
fetchMock.get(`https://demo.endpoint/api/user/v1/accounts/${user.username}`, {});
|
|
144
148
|
fetchMock.get(`https://demo.endpoint/api/user/v1/preferences/${user.username}`, {});
|
|
149
|
+
const orderQueryParameters = {
|
|
150
|
+
course_code: courseCode,
|
|
151
|
+
product_id: product.id,
|
|
152
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
153
|
+
};
|
|
154
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
|
|
145
155
|
fetchMock
|
|
146
|
-
.get(
|
|
147
|
-
`https://joanie.endpoint/api/v1.0/orders/?course_code=${courseCode}&product_id=${product.id}&state=pending&state=validated&state=submitted`,
|
|
148
|
-
{},
|
|
149
|
-
)
|
|
156
|
+
.get(url, {})
|
|
150
157
|
.get(
|
|
151
158
|
`https://joanie.endpoint/api/v1.0/courses/${courseCode}/products/${product.id}/payment-schedule/`,
|
|
152
159
|
[],
|
|
@@ -180,11 +187,14 @@ describe('PurchaseButton', () => {
|
|
|
180
187
|
it('shows cta to open sale tunnel when remaining orders is undefined', async () => {
|
|
181
188
|
const courseCode = '00000';
|
|
182
189
|
const product = ProductFactory().one();
|
|
190
|
+
const orderQueryParameters = {
|
|
191
|
+
course_code: courseCode,
|
|
192
|
+
product_id: product.id,
|
|
193
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
194
|
+
};
|
|
195
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
|
|
183
196
|
fetchMock
|
|
184
|
-
.get(
|
|
185
|
-
`https://joanie.endpoint/api/v1.0/orders/?course_code=${courseCode}&product_id=${product.id}&state=pending&state=validated&state=submitted`,
|
|
186
|
-
{},
|
|
187
|
-
)
|
|
197
|
+
.get(url, {})
|
|
188
198
|
.get(
|
|
189
199
|
`https://joanie.endpoint/api/v1.0/courses/${courseCode}/products/${product.id}/payment-schedule/`,
|
|
190
200
|
[],
|
|
@@ -220,10 +230,13 @@ describe('PurchaseButton', () => {
|
|
|
220
230
|
it('renders a disabled CTA if the product have no remaining orders', async () => {
|
|
221
231
|
const courseCode = '00000';
|
|
222
232
|
const product = ProductFactory({ remaining_order_count: 0 }).one();
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
233
|
+
const orderQueryParameters = {
|
|
234
|
+
course_code: courseCode,
|
|
235
|
+
product_id: product.id,
|
|
236
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
237
|
+
};
|
|
238
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
|
|
239
|
+
fetchMock.get(url, {});
|
|
227
240
|
render(
|
|
228
241
|
<Wrapper client={createTestQueryClient({ user: richieUser })}>
|
|
229
242
|
<PurchaseButton
|
|
@@ -256,10 +269,14 @@ describe('PurchaseButton', () => {
|
|
|
256
269
|
const courseCode = '00000';
|
|
257
270
|
const product = ProductFactory({ ...productData, type: ProductType.CREDENTIAL }).one();
|
|
258
271
|
product.target_courses[0].course_runs = [];
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
272
|
+
|
|
273
|
+
const orderQueryParameters = {
|
|
274
|
+
course_code: courseCode,
|
|
275
|
+
product_id: product.id,
|
|
276
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
277
|
+
};
|
|
278
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
|
|
279
|
+
fetchMock.get(url, {});
|
|
263
280
|
|
|
264
281
|
render(
|
|
265
282
|
<Wrapper client={createTestQueryClient({ user: richieUser })}>
|
|
@@ -305,10 +322,14 @@ describe('PurchaseButton', () => {
|
|
|
305
322
|
const product = CertificateProductFactory().one();
|
|
306
323
|
const enrollment = EnrollmentFactory().one();
|
|
307
324
|
enrollment.course_run.state = CourseStateFactory(courseRunStateData).one();
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
325
|
+
|
|
326
|
+
const orderQueryParameters = {
|
|
327
|
+
enrollment_id: enrollment.id,
|
|
328
|
+
product_id: product.id,
|
|
329
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
330
|
+
};
|
|
331
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
|
|
332
|
+
fetchMock.get(url, {});
|
|
312
333
|
|
|
313
334
|
render(
|
|
314
335
|
<Wrapper client={createTestQueryClient({ user: true })}>
|
|
@@ -359,10 +380,13 @@ describe('PurchaseButton', () => {
|
|
|
359
380
|
const enrollment = EnrollmentFactory().one();
|
|
360
381
|
enrollment.course_run.state = CourseStateFactory(courseRunStateData).one();
|
|
361
382
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
383
|
+
const orderQueryParameters = {
|
|
384
|
+
enrollment_id: enrollment.id,
|
|
385
|
+
product_id: product.id,
|
|
386
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
387
|
+
};
|
|
388
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
|
|
389
|
+
fetchMock.get(url, {});
|
|
366
390
|
|
|
367
391
|
render(
|
|
368
392
|
<Wrapper client={createTestQueryClient({ user: true })}>
|
|
@@ -388,10 +412,14 @@ describe('PurchaseButton', () => {
|
|
|
388
412
|
it('renders a disabled CTA if product has no target courses', async () => {
|
|
389
413
|
const courseCode = '00000';
|
|
390
414
|
const product = ProductFactory().one();
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
415
|
+
|
|
416
|
+
const orderQueryParameters = {
|
|
417
|
+
course_code: courseCode,
|
|
418
|
+
product_id: product.id,
|
|
419
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
420
|
+
};
|
|
421
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
|
|
422
|
+
fetchMock.get(url, {});
|
|
395
423
|
product.target_courses = [];
|
|
396
424
|
|
|
397
425
|
render(
|
|
@@ -419,10 +447,14 @@ describe('PurchaseButton', () => {
|
|
|
419
447
|
it('does not render CTA if disabled property is false', async () => {
|
|
420
448
|
const courseCode = '00000';
|
|
421
449
|
const product = ProductFactory().one();
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
450
|
+
|
|
451
|
+
const orderQueryParameters = {
|
|
452
|
+
course_code: courseCode,
|
|
453
|
+
product_id: product.id,
|
|
454
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
455
|
+
};
|
|
456
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
|
|
457
|
+
fetchMock.get(url, {});
|
|
426
458
|
|
|
427
459
|
render(
|
|
428
460
|
<Wrapper client={createTestQueryClient({ user: true })}>
|
|
@@ -59,10 +59,11 @@ describe('AddressSelector', () => {
|
|
|
59
59
|
setBillingAddress,
|
|
60
60
|
setCreditCard: jest.fn(),
|
|
61
61
|
onPaymentSuccess: jest.fn(),
|
|
62
|
-
step: SaleTunnelStep.
|
|
62
|
+
step: SaleTunnelStep.IDLE,
|
|
63
63
|
registerSubmitCallback: jest.fn(),
|
|
64
64
|
unregisterSubmitCallback: jest.fn(),
|
|
65
65
|
runSubmitCallbacks: jest.fn(),
|
|
66
|
+
nextStep: jest.fn(),
|
|
66
67
|
}),
|
|
67
68
|
[billingAddress],
|
|
68
69
|
);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { SaleTunnelProps } from 'components/SaleTunnel/index';
|
|
2
2
|
import { CertificateProduct } from 'types/Joanie';
|
|
3
3
|
import { GenericSaleTunnel } from 'components/SaleTunnel/GenericSaleTunnel';
|
|
4
|
-
import { GenericPaymentButton } from 'components/SaleTunnel/GenericPaymentButton';
|
|
5
4
|
import { CertificateProductPath } from 'components/SaleTunnel/CertificateSaleTunnel/CertificateProductPath';
|
|
5
|
+
import SubscriptionButton from 'components/SaleTunnel/SubscriptionButton';
|
|
6
6
|
|
|
7
7
|
interface CertificateSaleTunnelProps extends Omit<SaleTunnelProps, 'product'> {
|
|
8
8
|
product: CertificateProduct;
|
|
@@ -23,7 +23,7 @@ const CertificatePaymentButton = ({
|
|
|
23
23
|
enrollment,
|
|
24
24
|
}: Pick<CertificateSaleTunnelProps, 'enrollment'>) => {
|
|
25
25
|
return (
|
|
26
|
-
<
|
|
26
|
+
<SubscriptionButton
|
|
27
27
|
buildOrderPayload={(payload) => {
|
|
28
28
|
return {
|
|
29
29
|
...payload,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { CredentialProduct } from 'types/Joanie';
|
|
2
2
|
import { GenericSaleTunnel } from 'components/SaleTunnel/GenericSaleTunnel';
|
|
3
3
|
import { SaleTunnelProps } from 'components/SaleTunnel/index';
|
|
4
|
-
import { GenericPaymentButton } from 'components/SaleTunnel/GenericPaymentButton';
|
|
5
4
|
import { CredentialProductPath } from 'components/SaleTunnel/CredentialSaleTunnel/CredentialProductPath';
|
|
5
|
+
import SubscriptionButton from 'components/SaleTunnel/SubscriptionButton';
|
|
6
6
|
|
|
7
7
|
interface CredentialSaleTunnelProps extends Omit<SaleTunnelProps, 'product'> {
|
|
8
8
|
product: CredentialProduct;
|
|
@@ -21,17 +21,13 @@ export const CredentialSaleTunnel = (props: CredentialSaleTunnelProps) => {
|
|
|
21
21
|
|
|
22
22
|
const CredentialPaymentButton = ({
|
|
23
23
|
course,
|
|
24
|
-
orderGroup,
|
|
25
24
|
}: Pick<CredentialSaleTunnelProps, 'course' | 'orderGroup'>) => {
|
|
26
25
|
return (
|
|
27
|
-
<
|
|
28
|
-
buildOrderPayload={(payload) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
...(orderGroup ? { order_group_id: orderGroup.id } : {}),
|
|
33
|
-
};
|
|
34
|
-
}}
|
|
26
|
+
<SubscriptionButton
|
|
27
|
+
buildOrderPayload={(payload) => ({
|
|
28
|
+
...payload,
|
|
29
|
+
course_code: course!.code,
|
|
30
|
+
})}
|
|
35
31
|
/>
|
|
36
32
|
);
|
|
37
33
|
};
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { Modal, ModalSize } from '@openfun/cunningham-react';
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
ReactNode,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
useCallback,
|
|
10
|
+
} from 'react';
|
|
4
11
|
import { SaleTunnelSponsors } from 'components/SaleTunnel/Sponsors/SaleTunnelSponsors';
|
|
5
12
|
import { SaleTunnelProps } from 'components/SaleTunnel/index';
|
|
6
|
-
import { Address, CreditCard, Order, Product } from 'types/Joanie';
|
|
13
|
+
import { Address, CreditCard, Order, OrderState, Product } from 'types/Joanie';
|
|
7
14
|
import useProductOrder from 'hooks/useProductOrder';
|
|
8
15
|
import { SaleTunnelSuccess } from 'components/SaleTunnel/SaleTunnelSuccess';
|
|
9
16
|
import WebAnalyticsAPIHandler from 'api/web-analytics';
|
|
@@ -11,16 +18,8 @@ import { CourseProductEvent } from 'types/web-analytics';
|
|
|
11
18
|
import { useOmniscientOrders, useOrders } from 'hooks/useOrders';
|
|
12
19
|
import { SaleTunnelInformation } from 'components/SaleTunnel/SaleTunnelInformation';
|
|
13
20
|
import { useEnrollments } from 'hooks/useEnrollments';
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
const messages = defineMessages({
|
|
17
|
-
walkthrough: {
|
|
18
|
-
id: 'components.SaleTunnel.GenericSaleTunnel.walkthrough',
|
|
19
|
-
defaultMessage:
|
|
20
|
-
'To enroll in the training, you will first be invited to sign the training agreement and then to define a payment method. You will not be charged during this step; the first payment will take place on the date mentioned in the payment schedule above.',
|
|
21
|
-
description: 'Message explaining the credential sale tunnel process',
|
|
22
|
-
},
|
|
23
|
-
});
|
|
21
|
+
import SaleTunnelSavePaymentMethod from 'components/SaleTunnel/SaleTunnelSavePaymentMethod';
|
|
22
|
+
import { LearnerContractFrame } from 'components/ContractFrame';
|
|
24
23
|
|
|
25
24
|
export interface SaleTunnelContextType {
|
|
26
25
|
props: SaleTunnelProps;
|
|
@@ -29,7 +28,7 @@ export interface SaleTunnelContextType {
|
|
|
29
28
|
webAnalyticsEventKey: string;
|
|
30
29
|
|
|
31
30
|
// internal
|
|
32
|
-
onPaymentSuccess: (
|
|
31
|
+
onPaymentSuccess: () => void;
|
|
33
32
|
step: SaleTunnelStep;
|
|
34
33
|
|
|
35
34
|
// meta
|
|
@@ -40,6 +39,7 @@ export interface SaleTunnelContextType {
|
|
|
40
39
|
registerSubmitCallback: (key: string, callback: () => Promise<void>) => void;
|
|
41
40
|
unregisterSubmitCallback: (key: string) => void;
|
|
42
41
|
runSubmitCallbacks: () => Promise<void>;
|
|
42
|
+
nextStep: () => void;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export const SaleTunnelContext = createContext<SaleTunnelContextType>({} as any);
|
|
@@ -55,9 +55,10 @@ export const useSaleTunnelContext = () => {
|
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
export enum SaleTunnelStep {
|
|
58
|
-
|
|
58
|
+
IDLE,
|
|
59
|
+
SIGN,
|
|
60
|
+
SAVE_PAYMENT,
|
|
59
61
|
SUCCESS,
|
|
60
|
-
NOT_VALIDATED,
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
interface GenericSaleTunnelProps extends SaleTunnelProps {
|
|
@@ -86,11 +87,35 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
|
|
|
86
87
|
} = useEnrollments(undefined, { enabled: false });
|
|
87
88
|
const [billingAddress, setBillingAddress] = useState<Address>();
|
|
88
89
|
const [creditCard, setCreditCard] = useState<CreditCard>();
|
|
89
|
-
const [step, setStep] = useState<SaleTunnelStep>(SaleTunnelStep.
|
|
90
|
+
const [step, setStep] = useState<SaleTunnelStep>(SaleTunnelStep.IDLE);
|
|
90
91
|
const [submitCallbacks, setSubmitCallbacks] = useState<Map<string, () => Promise<void>>>(
|
|
91
92
|
new Map(),
|
|
92
93
|
);
|
|
93
94
|
|
|
95
|
+
const nextStep = useCallback(() => {
|
|
96
|
+
if (order)
|
|
97
|
+
switch (step) {
|
|
98
|
+
case SaleTunnelStep.IDLE:
|
|
99
|
+
if ([OrderState.TO_SIGN, OrderState.SIGNING].includes(order.state)) {
|
|
100
|
+
setStep(SaleTunnelStep.SIGN);
|
|
101
|
+
} else if (order.state === OrderState.TO_SAVE_PAYMENT_METHOD) {
|
|
102
|
+
setStep(SaleTunnelStep.SAVE_PAYMENT);
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
case SaleTunnelStep.SIGN:
|
|
106
|
+
if (order.state === OrderState.TO_SAVE_PAYMENT_METHOD) {
|
|
107
|
+
setStep(SaleTunnelStep.SAVE_PAYMENT);
|
|
108
|
+
}
|
|
109
|
+
if (order.state === OrderState.COMPLETED) {
|
|
110
|
+
setStep(SaleTunnelStep.SUCCESS);
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
case SaleTunnelStep.SAVE_PAYMENT:
|
|
114
|
+
setStep(SaleTunnelStep.SUCCESS);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}, [order, step]);
|
|
118
|
+
|
|
94
119
|
const context: SaleTunnelContextType = useMemo(
|
|
95
120
|
() => ({
|
|
96
121
|
webAnalyticsEventKey: props.eventKey,
|
|
@@ -101,12 +126,9 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
|
|
|
101
126
|
setBillingAddress,
|
|
102
127
|
creditCard,
|
|
103
128
|
setCreditCard,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
} else {
|
|
108
|
-
setStep(SaleTunnelStep.NOT_VALIDATED);
|
|
109
|
-
}
|
|
129
|
+
nextStep,
|
|
130
|
+
onPaymentSuccess: () => {
|
|
131
|
+
nextStep();
|
|
110
132
|
WebAnalyticsAPIHandler()?.sendCourseProductEvent(
|
|
111
133
|
CourseProductEvent.PAYMENT_SUCCEED,
|
|
112
134
|
props.eventKey,
|
|
@@ -154,13 +176,16 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
|
|
|
154
176
|
|
|
155
177
|
export const GenericSaleTunnelInner = (props: GenericSaleTunnelProps) => {
|
|
156
178
|
const { step } = useSaleTunnelContext();
|
|
179
|
+
|
|
157
180
|
switch (step) {
|
|
158
|
-
case SaleTunnelStep.
|
|
159
|
-
return <
|
|
181
|
+
case SaleTunnelStep.IDLE:
|
|
182
|
+
return <GenericSaleTunnelInitialStep {...props} />;
|
|
183
|
+
case SaleTunnelStep.SIGN:
|
|
184
|
+
return <GenericSaleTunnelSignStep {...props} />;
|
|
185
|
+
case SaleTunnelStep.SAVE_PAYMENT:
|
|
186
|
+
return <GenericSaleTunnelSavePaymentMethodStep {...props} />;
|
|
160
187
|
case SaleTunnelStep.SUCCESS:
|
|
161
188
|
return <GenericSaleTunnelSuccessStep {...props} />;
|
|
162
|
-
case SaleTunnelStep.NOT_VALIDATED:
|
|
163
|
-
return <GenericSaleTunnelNotValidatedStep {...props} />;
|
|
164
189
|
}
|
|
165
190
|
throw new Error('Invalid step: ' + step);
|
|
166
191
|
};
|
|
@@ -169,7 +194,7 @@ export const GenericSaleTunnelInner = (props: GenericSaleTunnelProps) => {
|
|
|
169
194
|
* Steps.
|
|
170
195
|
*/
|
|
171
196
|
|
|
172
|
-
export const
|
|
197
|
+
export const GenericSaleTunnelInitialStep = (props: GenericSaleTunnelProps) => {
|
|
173
198
|
const { webAnalyticsEventKey } = useSaleTunnelContext();
|
|
174
199
|
|
|
175
200
|
useEffect(() => {
|
|
@@ -184,7 +209,7 @@ export const GenericSaleTunnelPaymentStep = (props: GenericSaleTunnelProps) => {
|
|
|
184
209
|
}, []);
|
|
185
210
|
|
|
186
211
|
return (
|
|
187
|
-
<Modal {...props} size={ModalSize.EXTRA_LARGE} title={props.product.title}
|
|
212
|
+
<Modal {...props} size={ModalSize.EXTRA_LARGE} title={props.product.title}>
|
|
188
213
|
<div className="sale-tunnel" data-testid="generic-sale-tunnel-payment-step">
|
|
189
214
|
<div className="sale-tunnel__main">
|
|
190
215
|
<div className="sale-tunnel__main__column sale-tunnel__main__left ">
|
|
@@ -198,14 +223,7 @@ export const GenericSaleTunnelPaymentStep = (props: GenericSaleTunnelProps) => {
|
|
|
198
223
|
<SaleTunnelInformation />
|
|
199
224
|
</div>
|
|
200
225
|
</div>
|
|
201
|
-
<div className="sale-tunnel__footer">
|
|
202
|
-
<div style={{ maxWidth: '680px' }} className="mb-s" data-testid="walkthrough-banner">
|
|
203
|
-
<Alert type={VariantType.INFO}>
|
|
204
|
-
<FormattedMessage {...messages.walkthrough} />
|
|
205
|
-
</Alert>
|
|
206
|
-
</div>
|
|
207
|
-
{props.paymentNode}
|
|
208
|
-
</div>
|
|
226
|
+
<div className="sale-tunnel__footer">{props.paymentNode}</div>
|
|
209
227
|
</div>
|
|
210
228
|
</Modal>
|
|
211
229
|
);
|
|
@@ -219,10 +237,26 @@ export const GenericSaleTunnelSuccessStep = (props: SaleTunnelProps) => {
|
|
|
219
237
|
);
|
|
220
238
|
};
|
|
221
239
|
|
|
222
|
-
export const
|
|
240
|
+
export const GenericSaleTunnelSavePaymentMethodStep = (props: SaleTunnelProps) => {
|
|
223
241
|
return (
|
|
224
|
-
<Modal {...props} size={ModalSize.
|
|
225
|
-
<
|
|
242
|
+
<Modal {...props} size={ModalSize.LARGE}>
|
|
243
|
+
<SaleTunnelSavePaymentMethod />
|
|
226
244
|
</Modal>
|
|
227
245
|
);
|
|
228
246
|
};
|
|
247
|
+
|
|
248
|
+
export const GenericSaleTunnelSignStep = ({ isOpen, onClose }: SaleTunnelProps) => {
|
|
249
|
+
const { order, nextStep } = useSaleTunnelContext();
|
|
250
|
+
|
|
251
|
+
const handleDone = useCallback(nextStep, [order]);
|
|
252
|
+
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
if (![OrderState.TO_SIGN, OrderState.SIGNING].includes(order!.state)) {
|
|
255
|
+
nextStep();
|
|
256
|
+
}
|
|
257
|
+
}, [order]);
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<LearnerContractFrame order={order!} isOpen={isOpen} onClose={onClose} onDone={handleDone} />
|
|
261
|
+
);
|
|
262
|
+
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { defineMessages, FormattedMessage, FormattedNumber } from 'react-intl';
|
|
2
2
|
import { AddressSelector } from 'components/SaleTunnel/AddressSelector';
|
|
3
|
-
import { CreditCardSelector } from 'components/CreditCardSelector';
|
|
4
3
|
import { PaymentScheduleGrid } from 'components/PaymentScheduleGrid';
|
|
5
4
|
import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
|
|
6
5
|
import OpenEdxFullNameForm from 'components/OpenEdxFullNameForm';
|
|
@@ -25,16 +24,6 @@ const messages = defineMessages({
|
|
|
25
24
|
description: 'Label for the full name input',
|
|
26
25
|
defaultMessage: 'Full name',
|
|
27
26
|
},
|
|
28
|
-
paymentMethodTitle: {
|
|
29
|
-
id: 'components.SaleTunnel.CreditCardSelector.title',
|
|
30
|
-
description: 'Title for the credit card section',
|
|
31
|
-
defaultMessage: 'Payment method',
|
|
32
|
-
},
|
|
33
|
-
paymentMethodDescription: {
|
|
34
|
-
id: 'components.SaleTunnel.CreditCardSelector.description',
|
|
35
|
-
description: 'Description for the credit card section',
|
|
36
|
-
defaultMessage: 'Choose your payment method or add a new one during the payment.',
|
|
37
|
-
},
|
|
38
27
|
totalInfo: {
|
|
39
28
|
id: 'components.SaleTunnel.Information.total.info',
|
|
40
29
|
description: 'Information about the total amount',
|
|
@@ -74,9 +63,6 @@ export const SaleTunnelInformation = () => {
|
|
|
74
63
|
<Email />
|
|
75
64
|
</div>
|
|
76
65
|
</div>
|
|
77
|
-
<div>
|
|
78
|
-
<CreditCardSelectorWrapper />
|
|
79
|
-
</div>
|
|
80
66
|
<div>
|
|
81
67
|
<PaymentScheduleBlock />
|
|
82
68
|
<Total />
|
|
@@ -84,22 +70,6 @@ export const SaleTunnelInformation = () => {
|
|
|
84
70
|
</div>
|
|
85
71
|
);
|
|
86
72
|
};
|
|
87
|
-
|
|
88
|
-
const CreditCardSelectorWrapper = () => {
|
|
89
|
-
const { creditCard, setCreditCard } = useSaleTunnelContext();
|
|
90
|
-
return (
|
|
91
|
-
<>
|
|
92
|
-
<h4 className="block-title mb-t">
|
|
93
|
-
<FormattedMessage {...messages.paymentMethodTitle} />
|
|
94
|
-
</h4>
|
|
95
|
-
<div className="description mb-s">
|
|
96
|
-
<FormattedMessage {...messages.paymentMethodDescription} />
|
|
97
|
-
</div>
|
|
98
|
-
<CreditCardSelector creditCard={creditCard} setCreditCard={setCreditCard} />
|
|
99
|
-
</>
|
|
100
|
-
);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
73
|
const Email = () => {
|
|
104
74
|
const { user } = useSession();
|
|
105
75
|
const { data: openEdxProfileData } = useOpenEdxProfile({
|