richie-education 2.28.2-dev39 → 2.28.2-dev53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/js/api/joanie.ts +12 -16
- package/js/api/lms/dummy.ts +1 -12
- package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
- package/js/components/ContractFrame/AbstractContractFrame.tsx +28 -23
- 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 +201 -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/types/Joanie.ts +49 -34
- package/js/utils/OrderHelper/index.ts +38 -42
- 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 +2 -5
- 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 +1 -1
- 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,229 @@
|
|
|
1
|
+
import { PropsWithChildren } from 'react';
|
|
2
|
+
import { act, screen } from '@testing-library/react';
|
|
3
|
+
import fetchMock from 'fetch-mock';
|
|
4
|
+
import { userEvent } from '@testing-library/user-event';
|
|
5
|
+
import {
|
|
6
|
+
RichieContextFactory as mockRichieContextFactory,
|
|
7
|
+
UserFactory,
|
|
8
|
+
} from 'utils/test/factories/richie';
|
|
9
|
+
import { render } from 'utils/test/render';
|
|
10
|
+
import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
|
|
11
|
+
import OpenEdxFullNameForm from 'components/OpenEdxFullNameForm/index';
|
|
12
|
+
import { SaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
|
|
13
|
+
import { SaleTunnelContextFactory } from 'utils/test/factories/joanie';
|
|
14
|
+
import { OpenEdxApiProfileFactory } from 'utils/test/factories/openEdx';
|
|
15
|
+
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
16
|
+
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
17
|
+
import { AppWrapperProps } from 'utils/test/wrappers/types';
|
|
18
|
+
import { expectBannerError } from 'utils/test/expectBanner';
|
|
19
|
+
|
|
20
|
+
jest.mock('utils/context', () => ({
|
|
21
|
+
__esModule: true,
|
|
22
|
+
default: mockRichieContextFactory({
|
|
23
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
|
|
24
|
+
joanie_backend: { endpoint: 'https://joanie.endpoint' },
|
|
25
|
+
}).one(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
jest.mock('utils/errors/handle', () => ({
|
|
29
|
+
handle: jest.fn(),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
describe('OpenEdxFullNameForm', () => {
|
|
33
|
+
let submitCallbacks: Record<PropertyKey, () => Promise<void>> = {};
|
|
34
|
+
const Wrapper = ({ children, ...props }: PropsWithChildren<AppWrapperProps>) => (
|
|
35
|
+
<BaseJoanieAppWrapper {...props}>
|
|
36
|
+
<SaleTunnelContext.Provider
|
|
37
|
+
value={SaleTunnelContextFactory({
|
|
38
|
+
registerSubmitCallback: (key: string, callback: () => Promise<void>) => {
|
|
39
|
+
submitCallbacks[key] = callback;
|
|
40
|
+
},
|
|
41
|
+
}).one()}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
</SaleTunnelContext.Provider>
|
|
45
|
+
,
|
|
46
|
+
</BaseJoanieAppWrapper>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
jest.clearAllMocks();
|
|
51
|
+
fetchMock.restore();
|
|
52
|
+
submitCallbacks = {};
|
|
53
|
+
});
|
|
54
|
+
setupJoanieSession();
|
|
55
|
+
|
|
56
|
+
it('should not populate form with username', async () => {
|
|
57
|
+
const user = UserFactory({ full_name: '' }).one();
|
|
58
|
+
const queryClient = createTestQueryClient({ user });
|
|
59
|
+
const { 'pref-lang': prefLang, ...profile } = OpenEdxApiProfileFactory({
|
|
60
|
+
name: user.full_name,
|
|
61
|
+
username: user.username,
|
|
62
|
+
email: user.email,
|
|
63
|
+
}).one();
|
|
64
|
+
fetchMock.get(`https://auth.test/api/user/v1/accounts/${user.username}`, profile);
|
|
65
|
+
fetchMock.get(`https://auth.test/api/user/v1/preferences/${user.username}`, {
|
|
66
|
+
'pref-lang': prefLang,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
render(<OpenEdxFullNameForm />, {
|
|
70
|
+
queryOptions: { client: queryClient },
|
|
71
|
+
wrapper: Wrapper,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const $input = await screen.findByRole('textbox', { name: 'Full name' });
|
|
75
|
+
expect($input).toHaveValue('');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should populate form with existing full name', async () => {
|
|
79
|
+
const user = UserFactory().one();
|
|
80
|
+
const queryClient = createTestQueryClient({ user });
|
|
81
|
+
const { 'pref-lang': prefLang, ...profile } = OpenEdxApiProfileFactory({
|
|
82
|
+
name: user.full_name,
|
|
83
|
+
username: user.username,
|
|
84
|
+
email: user.email,
|
|
85
|
+
}).one();
|
|
86
|
+
fetchMock.get(`https://auth.test/api/user/v1/accounts/${user.username}`, profile);
|
|
87
|
+
fetchMock.get(`https://auth.test/api/user/v1/preferences/${user.username}`, {
|
|
88
|
+
'pref-lang': prefLang,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
render(<OpenEdxFullNameForm />, {
|
|
92
|
+
queryOptions: { client: queryClient },
|
|
93
|
+
wrapper: Wrapper,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const $input = await screen.findByRole('textbox', { name: 'Full name' });
|
|
97
|
+
expect($input).toHaveValue(user.full_name);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should require a value to submit the form', async () => {
|
|
101
|
+
const user = UserFactory({ full_name: '' }).one();
|
|
102
|
+
const queryClient = createTestQueryClient({ user });
|
|
103
|
+
const { 'pref-lang': prefLang, ...profile } = OpenEdxApiProfileFactory({
|
|
104
|
+
name: user.full_name,
|
|
105
|
+
username: user.username,
|
|
106
|
+
email: user.email,
|
|
107
|
+
}).one();
|
|
108
|
+
fetchMock.get(`https://auth.test/api/user/v1/accounts/${user.username}`, profile);
|
|
109
|
+
fetchMock.get(`https://auth.test/api/user/v1/preferences/${user.username}`, {
|
|
110
|
+
'pref-lang': prefLang,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(submitCallbacks).toEqual({});
|
|
114
|
+
|
|
115
|
+
render(<OpenEdxFullNameForm />, {
|
|
116
|
+
queryOptions: { client: queryClient },
|
|
117
|
+
wrapper: Wrapper,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(submitCallbacks.hasOwnProperty('openEdxFullNameForm')).toBe(true);
|
|
121
|
+
|
|
122
|
+
const $input = await screen.findByRole('textbox', { name: 'Full name' });
|
|
123
|
+
expect($input).toHaveValue('');
|
|
124
|
+
|
|
125
|
+
// Submit the form
|
|
126
|
+
await act(async () => {
|
|
127
|
+
await expect(submitCallbacks.openEdxFullNameForm()).rejects.not.toBeUndefined();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
screen.getByText('This field is required.');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should require a value with at least 3 chars to submit the form', async () => {
|
|
134
|
+
const user = UserFactory({ full_name: '' }).one();
|
|
135
|
+
const queryClient = createTestQueryClient({ user });
|
|
136
|
+
const { 'pref-lang': prefLang, ...profile } = OpenEdxApiProfileFactory({
|
|
137
|
+
name: user.full_name,
|
|
138
|
+
username: user.username,
|
|
139
|
+
email: user.email,
|
|
140
|
+
}).one();
|
|
141
|
+
fetchMock.get(`https://auth.test/api/user/v1/accounts/${user.username}`, profile);
|
|
142
|
+
fetchMock.get(`https://auth.test/api/user/v1/preferences/${user.username}`, {
|
|
143
|
+
'pref-lang': prefLang,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(submitCallbacks).toEqual({});
|
|
147
|
+
|
|
148
|
+
render(<OpenEdxFullNameForm />, {
|
|
149
|
+
queryOptions: { client: queryClient },
|
|
150
|
+
wrapper: Wrapper,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(submitCallbacks.hasOwnProperty('openEdxFullNameForm')).toBe(true);
|
|
154
|
+
|
|
155
|
+
const $input = await screen.findByRole('textbox', { name: 'Full name' });
|
|
156
|
+
expect($input).toHaveValue('');
|
|
157
|
+
|
|
158
|
+
const eventHandler = userEvent.setup();
|
|
159
|
+
await eventHandler.type($input, 'Jo');
|
|
160
|
+
|
|
161
|
+
// Submit the form
|
|
162
|
+
await act(async () => {
|
|
163
|
+
await expect(submitCallbacks.openEdxFullNameForm()).rejects.not.toBeUndefined();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
screen.getByText('The minimum length is 3 chars.');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should submit the form', async () => {
|
|
170
|
+
const user = UserFactory({ full_name: '' }).one();
|
|
171
|
+
const queryClient = createTestQueryClient({ user });
|
|
172
|
+
const { 'pref-lang': prefLang, ...profile } = OpenEdxApiProfileFactory({
|
|
173
|
+
name: user.full_name,
|
|
174
|
+
username: user.username,
|
|
175
|
+
email: user.email,
|
|
176
|
+
}).one();
|
|
177
|
+
fetchMock
|
|
178
|
+
.get(`https://auth.test/api/user/v1/accounts/${user.username}`, profile)
|
|
179
|
+
.get(`https://auth.test/api/user/v1/preferences/${user.username}`, {
|
|
180
|
+
'pref-lang': prefLang,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(submitCallbacks).toEqual({});
|
|
184
|
+
|
|
185
|
+
render(<OpenEdxFullNameForm />, {
|
|
186
|
+
queryOptions: { client: queryClient },
|
|
187
|
+
wrapper: Wrapper,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(submitCallbacks.hasOwnProperty('openEdxFullNameForm')).toBe(true);
|
|
191
|
+
|
|
192
|
+
const $input = await screen.findByRole('textbox', { name: 'Full name' });
|
|
193
|
+
expect($input).toHaveValue('');
|
|
194
|
+
|
|
195
|
+
const eventHandler = userEvent.setup();
|
|
196
|
+
await eventHandler.type($input, 'John Doe');
|
|
197
|
+
|
|
198
|
+
// Submit the form
|
|
199
|
+
fetchMock
|
|
200
|
+
.patch(`https://auth.test/api/user/v1/accounts/${user.username}`, 200)
|
|
201
|
+
.patch(`https://auth.test/api/user/v1/preferences/${user.username}`, 200)
|
|
202
|
+
.get('https://auth.test/api/v1.0/user/me', { ...user, full_name: 'John Doe' })
|
|
203
|
+
.get(
|
|
204
|
+
`https://auth.test/api/user/v1/accounts/JohnDoe`,
|
|
205
|
+
{ ...profile, name: 'John Doe' },
|
|
206
|
+
{ overwriteRoutes: true },
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
await act(async () => {
|
|
210
|
+
await submitCallbacks.openEdxFullNameForm();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect($input).toHaveValue('John Doe');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should display error if request to retrieve account fails', async () => {
|
|
217
|
+
const user = UserFactory().one();
|
|
218
|
+
const queryClient = createTestQueryClient({ user });
|
|
219
|
+
fetchMock.get(`https://auth.test/api/user/v1/accounts/${user.username}`, 500);
|
|
220
|
+
fetchMock.get(`https://auth.test/api/user/v1/preferences/${user.username}`, 500);
|
|
221
|
+
|
|
222
|
+
render(<OpenEdxFullNameForm />, {
|
|
223
|
+
queryOptions: { client: queryClient },
|
|
224
|
+
wrapper: Wrapper,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await expectBannerError('An error occurred while fetching your profile. Please retry later.');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -9,7 +9,6 @@ import useOpenEdxProfile from 'hooks/useOpenEdxProfile';
|
|
|
9
9
|
import Form, { getLocalizedCunninghamErrorProp } from 'components/Form';
|
|
10
10
|
import { Spinner } from 'components/Spinner';
|
|
11
11
|
import Banner, { BannerType } from 'components/Banner';
|
|
12
|
-
import { UserHelper } from 'utils/UserHelper';
|
|
13
12
|
import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
|
|
14
13
|
|
|
15
14
|
const messages = defineMessages({
|
|
@@ -51,7 +50,7 @@ export interface OpenEdxFullNameFormValues {
|
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
const validationSchema = Yup.object().shape({
|
|
54
|
-
name: Yup.string().required(),
|
|
53
|
+
name: Yup.string().required().min(3),
|
|
55
54
|
});
|
|
56
55
|
|
|
57
56
|
const OpenEdxFullNameForm = () => {
|
|
@@ -70,7 +69,7 @@ const OpenEdxFullNameForm = () => {
|
|
|
70
69
|
|
|
71
70
|
const defaultValues = useMemo(
|
|
72
71
|
() => ({
|
|
73
|
-
name: (openEdxProfileData
|
|
72
|
+
name: (openEdxProfileData?.name || '')?.trim(),
|
|
74
73
|
}),
|
|
75
74
|
[openEdxProfileData],
|
|
76
75
|
);
|
|
@@ -82,19 +81,20 @@ const OpenEdxFullNameForm = () => {
|
|
|
82
81
|
resolver: yupResolver(validationSchema),
|
|
83
82
|
});
|
|
84
83
|
|
|
85
|
-
const { register, handleSubmit, reset, formState } = form;
|
|
84
|
+
const { getValues, register, handleSubmit, reset, formState } = form;
|
|
86
85
|
|
|
87
86
|
useEffect(() => {
|
|
88
87
|
if (openEdxProfileData) {
|
|
89
|
-
reset({ name: (openEdxProfileData
|
|
88
|
+
reset({ name: (openEdxProfileData?.name || '')?.trim() });
|
|
90
89
|
}
|
|
91
90
|
}, [openEdxProfileData]);
|
|
92
91
|
|
|
93
92
|
useEffect(() => {
|
|
94
93
|
registerSubmitCallback('openEdxFullNameForm', async () => {
|
|
95
94
|
return new Promise<void>((resolve, reject) => {
|
|
95
|
+
const { name } = getValues();
|
|
96
96
|
// Don't save if the form has not been modified.
|
|
97
|
-
if (!formState.isDirty) {
|
|
97
|
+
if (name && !formState.isDirty) {
|
|
98
98
|
resolve();
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
@@ -110,7 +110,7 @@ const OpenEdxFullNameForm = () => {
|
|
|
110
110
|
},
|
|
111
111
|
onError: (e) => reject(e),
|
|
112
112
|
});
|
|
113
|
-
})();
|
|
113
|
+
}, reject)();
|
|
114
114
|
});
|
|
115
115
|
});
|
|
116
116
|
return () => {
|
|
@@ -29,7 +29,7 @@ const LyraPopIn = ({
|
|
|
29
29
|
if (error && typeof error === 'string') handle(`[LyraPopIn] - ${error}`);
|
|
30
30
|
|
|
31
31
|
if (shouldAbort.current) {
|
|
32
|
-
onError(PaymentErrorMessageId.
|
|
32
|
+
onError(PaymentErrorMessageId.ERROR_ABORT);
|
|
33
33
|
} else if (typeof error === 'string') {
|
|
34
34
|
onError(error);
|
|
35
35
|
} else {
|
|
@@ -109,7 +109,7 @@ const LyraPopIn = ({
|
|
|
109
109
|
|
|
110
110
|
const handleClosePopIn = () => {
|
|
111
111
|
if (shouldAbort.current === true) {
|
|
112
|
-
onError(PaymentErrorMessageId.
|
|
112
|
+
onError(PaymentErrorMessageId.ERROR_ABORT);
|
|
113
113
|
}
|
|
114
114
|
};
|
|
115
115
|
|
|
@@ -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
|
};
|