richie-education 2.30.1-dev12 → 2.30.1-dev13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/js/components/PaymentInterfaces/types.ts +1 -0
- package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -0
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +6 -1
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +2 -0
- package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +8 -0
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +17 -2
- package/js/components/SaleTunnel/WithdrawRightCheckbox/index.tsx +59 -0
- package/js/components/SaleTunnel/index.full-process.spec.tsx +16 -2
- package/js/components/SaleTunnel/index.spec.tsx +40 -0
- package/js/types/Joanie.ts +2 -0
- package/js/utils/test/factories/joanie.ts +3 -0
- package/package.json +1 -1
|
@@ -35,6 +35,8 @@ export interface SaleTunnelContextType {
|
|
|
35
35
|
setBillingAddress: (address?: Address) => void;
|
|
36
36
|
creditCard?: CreditCard;
|
|
37
37
|
setCreditCard: (creditCard?: CreditCard) => void;
|
|
38
|
+
hasWaivedWithdrawalRight: boolean;
|
|
39
|
+
setHasWaivedWithdrawalRight: (hasWaivedWithdrawalRight: boolean) => void;
|
|
38
40
|
registerSubmitCallback: (key: string, callback: () => Promise<void>) => void;
|
|
39
41
|
unregisterSubmitCallback: (key: string) => void;
|
|
40
42
|
runSubmitCallbacks: () => Promise<void>;
|
|
@@ -76,6 +78,7 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
|
|
|
76
78
|
});
|
|
77
79
|
const [billingAddress, setBillingAddress] = useState<Address>();
|
|
78
80
|
const [creditCard, setCreditCard] = useState<CreditCard>();
|
|
81
|
+
const [hasWaivedWithdrawalRight, setHasWaivedWithdrawalRight] = useState(false);
|
|
79
82
|
const [step, setStep] = useState<SaleTunnelStep>(SaleTunnelStep.IDLE);
|
|
80
83
|
const [submitCallbacks, setSubmitCallbacks] = useState<Map<string, () => Promise<void>>>(
|
|
81
84
|
new Map(),
|
|
@@ -115,6 +118,8 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
|
|
|
115
118
|
setBillingAddress,
|
|
116
119
|
creditCard,
|
|
117
120
|
setCreditCard,
|
|
121
|
+
hasWaivedWithdrawalRight,
|
|
122
|
+
setHasWaivedWithdrawalRight,
|
|
118
123
|
nextStep,
|
|
119
124
|
step,
|
|
120
125
|
registerSubmitCallback: (key, callback) => {
|
|
@@ -131,7 +136,7 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
|
|
|
131
136
|
await Promise.all(Array.from(submitCallbacks.values()).map((cb) => cb()));
|
|
132
137
|
},
|
|
133
138
|
}),
|
|
134
|
-
[props, order, billingAddress, creditCard, step, submitCallbacks],
|
|
139
|
+
[props, order, billingAddress, creditCard, step, submitCallbacks, hasWaivedWithdrawalRight],
|
|
135
140
|
);
|
|
136
141
|
|
|
137
142
|
return (
|
|
@@ -7,6 +7,7 @@ import { useSession } from 'contexts/SessionContext';
|
|
|
7
7
|
import useOpenEdxProfile from 'hooks/useOpenEdxProfile';
|
|
8
8
|
import { usePaymentSchedule } from 'hooks/usePaymentSchedule';
|
|
9
9
|
import { Spinner } from 'components/Spinner';
|
|
10
|
+
import WithdrawRightCheckbox from 'components/SaleTunnel/WithdrawRightCheckbox';
|
|
10
11
|
|
|
11
12
|
const messages = defineMessages({
|
|
12
13
|
title: {
|
|
@@ -71,6 +72,7 @@ export const SaleTunnelInformation = () => {
|
|
|
71
72
|
<div>
|
|
72
73
|
<PaymentScheduleBlock />
|
|
73
74
|
<Total />
|
|
75
|
+
<WithdrawRightCheckbox />
|
|
74
76
|
</div>
|
|
75
77
|
</div>
|
|
76
78
|
);
|
|
@@ -4,4 +4,12 @@
|
|
|
4
4
|
margin-top: 0.5rem;
|
|
5
5
|
margin-bottom: 0;
|
|
6
6
|
}
|
|
7
|
+
|
|
8
|
+
&__waiveCheckbox {
|
|
9
|
+
& > .waiveCheckbox__input {
|
|
10
|
+
/* Just add 1 px offset to prevent border input to be hidden
|
|
11
|
+
due to the overflow hidden applied to the parent block */
|
|
12
|
+
margin-left: 1px;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
7
15
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { Alert, Button, VariantType } from '@openfun/cunningham-react';
|
|
3
3
|
import { defineMessages, FormattedMessage } from 'react-intl';
|
|
4
4
|
import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
|
|
@@ -50,6 +50,11 @@ const messages = defineMessages({
|
|
|
50
50
|
description: "Error message shown when the user didn't select a billing address.",
|
|
51
51
|
id: 'components.SubscriptionButton.errorAddress',
|
|
52
52
|
},
|
|
53
|
+
errorWithdrawalRight: {
|
|
54
|
+
defaultMessage: 'You must waive your withdrawal right.',
|
|
55
|
+
description: "Error message shown when the user must waive its withdrawal right but doesn't.",
|
|
56
|
+
id: 'components.SubscriptionButton.errorWithdrawalRight',
|
|
57
|
+
},
|
|
53
58
|
orderCreationInProgress: {
|
|
54
59
|
defaultMessage: 'Order creation in progress',
|
|
55
60
|
description: 'Label for screen reader when an order creation is in progress.',
|
|
@@ -65,7 +70,10 @@ enum ComponentStates {
|
|
|
65
70
|
|
|
66
71
|
interface Props {
|
|
67
72
|
buildOrderPayload: (
|
|
68
|
-
payload: Pick<
|
|
73
|
+
payload: Pick<
|
|
74
|
+
OrderCreationPayload,
|
|
75
|
+
'product_id' | 'billing_address' | 'order_group_id' | 'has_waived_withdrawal_right'
|
|
76
|
+
>,
|
|
69
77
|
) => OrderCreationPayload;
|
|
70
78
|
}
|
|
71
79
|
|
|
@@ -74,6 +82,7 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
|
|
|
74
82
|
order,
|
|
75
83
|
creditCard,
|
|
76
84
|
billingAddress,
|
|
85
|
+
hasWaivedWithdrawalRight,
|
|
77
86
|
product,
|
|
78
87
|
nextStep,
|
|
79
88
|
runSubmitCallbacks,
|
|
@@ -107,10 +116,16 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
|
|
|
107
116
|
return;
|
|
108
117
|
}
|
|
109
118
|
|
|
119
|
+
if (!product.is_withdrawable && !hasWaivedWithdrawalRight) {
|
|
120
|
+
handleError(SubscriptionErrorMessageId.ERROR_WITHDRAWAL_RIGHT);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
110
124
|
const payload = buildOrderPayload({
|
|
111
125
|
product_id: product.id,
|
|
112
126
|
billing_address: billingAddress!,
|
|
113
127
|
order_group_id: saleTunnelProps.orderGroup?.id,
|
|
128
|
+
has_waived_withdrawal_right: hasWaivedWithdrawalRight,
|
|
114
129
|
});
|
|
115
130
|
|
|
116
131
|
orderMethods.create(payload, {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Alert, Checkbox, VariantType } from '@openfun/cunningham-react';
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { defineMessages, FormattedMessage } from 'react-intl/lib';
|
|
4
|
+
import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
|
|
5
|
+
|
|
6
|
+
const messages = defineMessages({
|
|
7
|
+
waiveCheckboxExplanation: {
|
|
8
|
+
defaultMessage:
|
|
9
|
+
'This training will start before the end of your withdrawal period. You must waive it to subscribe.',
|
|
10
|
+
description: 'Text to explain why the user has to waive to its withdrawal right.',
|
|
11
|
+
id: 'components.SaleTunnel.WithdrawRightCheckbox.waiverLabel',
|
|
12
|
+
},
|
|
13
|
+
waiveCheckboxLabel: {
|
|
14
|
+
defaultMessage: 'I waive my right of withdrawal',
|
|
15
|
+
description: 'Label of the checkbox to waive the withdrawal right.',
|
|
16
|
+
id: 'components.SaleTunnel.WithdrawRightCheckbox.waiveCheckboxLabel',
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const WithdrawRightCheckbox = () => {
|
|
21
|
+
const {
|
|
22
|
+
product,
|
|
23
|
+
registerSubmitCallback,
|
|
24
|
+
unregisterSubmitCallback,
|
|
25
|
+
hasWaivedWithdrawalRight,
|
|
26
|
+
setHasWaivedWithdrawalRight,
|
|
27
|
+
} = useSaleTunnelContext();
|
|
28
|
+
const [hasErrorState, setHasError] = useState(false);
|
|
29
|
+
const setError = useCallback(async () => {
|
|
30
|
+
setHasError(!product.is_withdrawable && !hasWaivedWithdrawalRight);
|
|
31
|
+
}, [hasWaivedWithdrawalRight, product.is_withdrawable]);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
registerSubmitCallback('withdrawalRight', setError);
|
|
35
|
+
return () => {
|
|
36
|
+
unregisterSubmitCallback('withdrawalRight');
|
|
37
|
+
};
|
|
38
|
+
}, [setError]);
|
|
39
|
+
|
|
40
|
+
if (product.is_withdrawable) return null;
|
|
41
|
+
return (
|
|
42
|
+
<section
|
|
43
|
+
className="mt-t subscription-button__waiveCheckbox"
|
|
44
|
+
data-testid="withdraw-right-checkbox"
|
|
45
|
+
>
|
|
46
|
+
<Alert type={hasErrorState ? VariantType.ERROR : VariantType.WARNING} className="mb-s">
|
|
47
|
+
<FormattedMessage {...messages.waiveCheckboxExplanation} />
|
|
48
|
+
</Alert>
|
|
49
|
+
<Checkbox
|
|
50
|
+
className="waiveCheckbox__input"
|
|
51
|
+
label={<FormattedMessage {...messages.waiveCheckboxLabel} />}
|
|
52
|
+
checked={hasWaivedWithdrawalRight}
|
|
53
|
+
onChange={(e) => setHasWaivedWithdrawalRight(e.target.checked)}
|
|
54
|
+
/>
|
|
55
|
+
</section>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default WithdrawRightCheckbox;
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
CreditCardFactory,
|
|
21
21
|
PaymentFactory,
|
|
22
22
|
PaymentInstallmentFactory,
|
|
23
|
+
ProductFactory,
|
|
23
24
|
} from 'utils/test/factories/joanie';
|
|
24
25
|
import { CourseRun, NOT_CANCELED_ORDER_STATES, OrderState } from 'types/Joanie';
|
|
25
26
|
import { Priority } from 'types';
|
|
@@ -97,9 +98,9 @@ describe('SaleTunnel', () => {
|
|
|
97
98
|
* Initialization.
|
|
98
99
|
*/
|
|
99
100
|
const course = PacedCourseFactory().one();
|
|
100
|
-
const
|
|
101
|
+
const product = ProductFactory({ is_withdrawable: false }).one();
|
|
102
|
+
const relation = CourseProductRelationFactory({ course, product }).one();
|
|
101
103
|
const paymentSchedule = PaymentInstallmentFactory().many(2);
|
|
102
|
-
const { product } = relation;
|
|
103
104
|
|
|
104
105
|
fetchMock.get(
|
|
105
106
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/`,
|
|
@@ -265,6 +266,11 @@ describe('SaleTunnel', () => {
|
|
|
265
266
|
priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
|
|
266
267
|
);
|
|
267
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Make sure the checkbox to waive withdrawal right is displayed
|
|
271
|
+
*/
|
|
272
|
+
const $waiveCheckbox = screen.getByLabelText('I waive my right of withdrawal');
|
|
273
|
+
|
|
268
274
|
/**
|
|
269
275
|
* Subscribe
|
|
270
276
|
*/
|
|
@@ -282,6 +288,14 @@ describe('SaleTunnel', () => {
|
|
|
282
288
|
}) as HTMLButtonElement;
|
|
283
289
|
await user.click($button);
|
|
284
290
|
|
|
291
|
+
/**
|
|
292
|
+
* An error should be displayed if the user has not waived its withdrawal right.
|
|
293
|
+
*/
|
|
294
|
+
screen.getByText('You must waive your withdrawal right.');
|
|
295
|
+
|
|
296
|
+
await user.click($waiveCheckbox);
|
|
297
|
+
await user.click($button);
|
|
298
|
+
|
|
285
299
|
order.state = OrderState.TO_SAVE_PAYMENT_METHOD;
|
|
286
300
|
order.contract = ContractFactory({ student_signed_on: new Date().toISOString() }).one();
|
|
287
301
|
|
|
@@ -453,4 +453,44 @@ describe.each([
|
|
|
453
453
|
|
|
454
454
|
screen.getByTestId('walkthrough-banner');
|
|
455
455
|
});
|
|
456
|
+
|
|
457
|
+
it('should show a checkbox to waive withdrawal right if the product is not withdrawable', async () => {
|
|
458
|
+
const product = ProductFactory({ is_withdrawable: false }).one();
|
|
459
|
+
const schedule = PaymentInstallmentFactory().many(2);
|
|
460
|
+
fetchMock
|
|
461
|
+
.get(
|
|
462
|
+
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
463
|
+
[],
|
|
464
|
+
)
|
|
465
|
+
.get(
|
|
466
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
467
|
+
schedule,
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
render(<Wrapper product={product} />, {
|
|
471
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
screen.getByTestId('withdraw-right-checkbox');
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('should not show a checkbox to waive withdrawal right if the product is withdrawable', async () => {
|
|
478
|
+
const product = ProductFactory({ is_withdrawable: true }).one();
|
|
479
|
+
const schedule = PaymentInstallmentFactory().many(2);
|
|
480
|
+
fetchMock
|
|
481
|
+
.get(
|
|
482
|
+
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
483
|
+
[],
|
|
484
|
+
)
|
|
485
|
+
.get(
|
|
486
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
487
|
+
schedule,
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
render(<Wrapper product={product} />, {
|
|
491
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
expect(screen.queryByTestId('withdraw-right-checkbox')).toBeNull();
|
|
495
|
+
});
|
|
456
496
|
});
|
package/js/types/Joanie.ts
CHANGED
|
@@ -149,6 +149,7 @@ export interface Product {
|
|
|
149
149
|
state: CourseState;
|
|
150
150
|
instructions: Nullable<string>;
|
|
151
151
|
contract_definition?: ContractDefinition;
|
|
152
|
+
is_withdrawable: boolean;
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
export interface CredentialProduct extends Product {
|
|
@@ -468,6 +469,7 @@ interface AbstractOrderProductCreationPayload {
|
|
|
468
469
|
product_id: Product['id'];
|
|
469
470
|
order_group_id?: OrderGroup['id'];
|
|
470
471
|
billing_address: Omit<Address, 'id' | 'is_main'>;
|
|
472
|
+
has_waived_withdrawal_right: boolean;
|
|
471
473
|
}
|
|
472
474
|
|
|
473
475
|
interface OrderCertificateCreationPayload extends AbstractOrderProductCreationPayload {
|
|
@@ -206,6 +206,7 @@ export const CredentialProductFactory = factory((): CredentialProduct => {
|
|
|
206
206
|
remaining_order_count: faker.number.int({ min: 1, max: 100 }),
|
|
207
207
|
state: CourseStateFactory().one(),
|
|
208
208
|
instructions: null,
|
|
209
|
+
is_withdrawable: true,
|
|
209
210
|
};
|
|
210
211
|
});
|
|
211
212
|
|
|
@@ -490,6 +491,8 @@ export const SaleTunnelContextFactory = factory(
|
|
|
490
491
|
billingAddress: undefined,
|
|
491
492
|
setBillingAddress: noop,
|
|
492
493
|
setCreditCard: noop,
|
|
494
|
+
setHasWaivedWithdrawalRight: noop,
|
|
495
|
+
hasWaivedWithdrawalRight: false,
|
|
493
496
|
step: SaleTunnelStep.IDLE,
|
|
494
497
|
registerSubmitCallback: noop,
|
|
495
498
|
unregisterSubmitCallback: noop,
|