richie-education 2.18.0 → 2.19.0
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/i18n/locales/ar-SA.json +16 -4
- package/i18n/locales/es-ES.json +16 -4
- package/i18n/locales/fa-IR.json +16 -4
- package/i18n/locales/fr-CA.json +17 -5
- package/i18n/locales/fr-FR.json +18 -6
- package/i18n/locales/ko-KR.json +16 -4
- package/i18n/locales/pt-PT.json +17 -5
- package/i18n/locales/ru-RU.json +16 -4
- package/js/components/CourseProductItem/PurchaseButton.spec.tsx +154 -0
- package/js/components/CourseProductItem/PurchaseButton.tsx +93 -0
- package/js/components/CourseProductItem/_styles.scss +14 -0
- package/js/components/CourseProductItem/index.spec.tsx +112 -30
- package/js/components/CourseProductItem/index.tsx +40 -30
- package/js/components/CourseRunEnrollment/_styles.scss +19 -10
- package/js/components/SaleTunnel/index.spec.tsx +24 -89
- package/js/components/SaleTunnel/index.tsx +33 -86
- package/js/hooks/useOrders.ts +15 -0
- package/js/hooks/useProduct.ts +2 -2
- package/js/hooks/useResources/useResourcesOmniscient.ts +1 -1
- 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/utils/api/joanie.ts +1 -3
- package/js/utils/test/factories.ts +1 -0
- package/package.json +29 -25
- package/scss/colors/_theme.scss +4 -1
- package/scss/generic/_icons.scss +1 -0
package/i18n/locales/ar-SA.json
CHANGED
|
@@ -175,6 +175,10 @@
|
|
|
175
175
|
"description": "Accessible text for the initial loading spinner displayed when product is fetching",
|
|
176
176
|
"message": "Loading product information..."
|
|
177
177
|
},
|
|
178
|
+
"components.CourseProductItem.pending": {
|
|
179
|
+
"description": "Message displayed when authenticated user has purchased the product but order is still pending",
|
|
180
|
+
"message": "Pending"
|
|
181
|
+
},
|
|
178
182
|
"components.CourseProductsList.end": {
|
|
179
183
|
"description": "End label displayed in the header of course run dates section",
|
|
180
184
|
"message": "End"
|
|
@@ -827,14 +831,22 @@
|
|
|
827
831
|
"description": "Error message shown to the user when enrollment update request fails.",
|
|
828
832
|
"message": "An error occurred while updating the enrollment. Please retry later."
|
|
829
833
|
},
|
|
830
|
-
"hooks.
|
|
831
|
-
"description": "Error message shown to the user when
|
|
832
|
-
"message": "
|
|
834
|
+
"hooks.useOrders.errorGet": {
|
|
835
|
+
"description": "Error message shown to the user when orders fetch request fails.",
|
|
836
|
+
"message": "An error occurred while fetching orders. Please retry later."
|
|
837
|
+
},
|
|
838
|
+
"hooks.useOrders.errorNotFound": {
|
|
839
|
+
"description": "Error message shown to the user when no orders matches.",
|
|
840
|
+
"message": "Cannot find the orders."
|
|
833
841
|
},
|
|
834
|
-
"hooks.useProduct.
|
|
842
|
+
"hooks.useProduct.errorGet": {
|
|
835
843
|
"description": "Error message shown to the user when product fetch request fails.",
|
|
836
844
|
"message": "An error occurred while fetching product. Please retry later."
|
|
837
845
|
},
|
|
846
|
+
"hooks.useProduct.errorNotFound": {
|
|
847
|
+
"description": "Error message shown to the user when no product matches.",
|
|
848
|
+
"message": "Cannot find the product."
|
|
849
|
+
},
|
|
838
850
|
"hooks.useResources.errorCreate": {
|
|
839
851
|
"description": "Error message shown to the user when resource creation request fails.",
|
|
840
852
|
"message": "An error occurred while creating a resource. Please retry later."
|
package/i18n/locales/es-ES.json
CHANGED
|
@@ -175,6 +175,10 @@
|
|
|
175
175
|
"description": "Accessible text for the initial loading spinner displayed when product is fetching",
|
|
176
176
|
"message": "Loading product information..."
|
|
177
177
|
},
|
|
178
|
+
"components.CourseProductItem.pending": {
|
|
179
|
+
"description": "Message displayed when authenticated user has purchased the product but order is still pending",
|
|
180
|
+
"message": "Pending"
|
|
181
|
+
},
|
|
178
182
|
"components.CourseProductsList.end": {
|
|
179
183
|
"description": "End label displayed in the header of course run dates section",
|
|
180
184
|
"message": "Fin"
|
|
@@ -827,14 +831,22 @@
|
|
|
827
831
|
"description": "Error message shown to the user when enrollment update request fails.",
|
|
828
832
|
"message": "An error occurred while updating the enrollment. Please retry later."
|
|
829
833
|
},
|
|
830
|
-
"hooks.
|
|
831
|
-
"description": "Error message shown to the user when
|
|
832
|
-
"message": "
|
|
834
|
+
"hooks.useOrders.errorGet": {
|
|
835
|
+
"description": "Error message shown to the user when orders fetch request fails.",
|
|
836
|
+
"message": "An error occurred while fetching orders. Please retry later."
|
|
837
|
+
},
|
|
838
|
+
"hooks.useOrders.errorNotFound": {
|
|
839
|
+
"description": "Error message shown to the user when no orders matches.",
|
|
840
|
+
"message": "Cannot find the orders."
|
|
833
841
|
},
|
|
834
|
-
"hooks.useProduct.
|
|
842
|
+
"hooks.useProduct.errorGet": {
|
|
835
843
|
"description": "Error message shown to the user when product fetch request fails.",
|
|
836
844
|
"message": "An error occurred while fetching product. Please retry later."
|
|
837
845
|
},
|
|
846
|
+
"hooks.useProduct.errorNotFound": {
|
|
847
|
+
"description": "Error message shown to the user when no product matches.",
|
|
848
|
+
"message": "Cannot find the product."
|
|
849
|
+
},
|
|
838
850
|
"hooks.useResources.errorCreate": {
|
|
839
851
|
"description": "Error message shown to the user when resource creation request fails.",
|
|
840
852
|
"message": "An error occurred while creating a resource. Please retry later."
|
package/i18n/locales/fa-IR.json
CHANGED
|
@@ -175,6 +175,10 @@
|
|
|
175
175
|
"description": "Accessible text for the initial loading spinner displayed when product is fetching",
|
|
176
176
|
"message": "Loading product information..."
|
|
177
177
|
},
|
|
178
|
+
"components.CourseProductItem.pending": {
|
|
179
|
+
"description": "Message displayed when authenticated user has purchased the product but order is still pending",
|
|
180
|
+
"message": "Pending"
|
|
181
|
+
},
|
|
178
182
|
"components.CourseProductsList.end": {
|
|
179
183
|
"description": "End label displayed in the header of course run dates section",
|
|
180
184
|
"message": "End"
|
|
@@ -827,14 +831,22 @@
|
|
|
827
831
|
"description": "Error message shown to the user when enrollment update request fails.",
|
|
828
832
|
"message": "An error occurred while updating the enrollment. Please retry later."
|
|
829
833
|
},
|
|
830
|
-
"hooks.
|
|
831
|
-
"description": "Error message shown to the user when
|
|
832
|
-
"message": "
|
|
834
|
+
"hooks.useOrders.errorGet": {
|
|
835
|
+
"description": "Error message shown to the user when orders fetch request fails.",
|
|
836
|
+
"message": "An error occurred while fetching orders. Please retry later."
|
|
837
|
+
},
|
|
838
|
+
"hooks.useOrders.errorNotFound": {
|
|
839
|
+
"description": "Error message shown to the user when no orders matches.",
|
|
840
|
+
"message": "Cannot find the orders."
|
|
833
841
|
},
|
|
834
|
-
"hooks.useProduct.
|
|
842
|
+
"hooks.useProduct.errorGet": {
|
|
835
843
|
"description": "Error message shown to the user when product fetch request fails.",
|
|
836
844
|
"message": "An error occurred while fetching product. Please retry later."
|
|
837
845
|
},
|
|
846
|
+
"hooks.useProduct.errorNotFound": {
|
|
847
|
+
"description": "Error message shown to the user when no product matches.",
|
|
848
|
+
"message": "Cannot find the product."
|
|
849
|
+
},
|
|
838
850
|
"hooks.useResources.errorCreate": {
|
|
839
851
|
"description": "Error message shown to the user when resource creation request fails.",
|
|
840
852
|
"message": "An error occurred while creating a resource. Please retry later."
|
package/i18n/locales/fr-CA.json
CHANGED
|
@@ -175,6 +175,10 @@
|
|
|
175
175
|
"description": "Accessible text for the initial loading spinner displayed when product is fetching",
|
|
176
176
|
"message": "Chargement des informations du produit..."
|
|
177
177
|
},
|
|
178
|
+
"components.CourseProductItem.pending": {
|
|
179
|
+
"description": "Message displayed when authenticated user has purchased the product but order is still pending",
|
|
180
|
+
"message": "Pending"
|
|
181
|
+
},
|
|
178
182
|
"components.CourseProductsList.end": {
|
|
179
183
|
"description": "End label displayed in the header of course run dates section",
|
|
180
184
|
"message": "Fin"
|
|
@@ -827,13 +831,21 @@
|
|
|
827
831
|
"description": "Error message shown to the user when enrollment update request fails.",
|
|
828
832
|
"message": "An error occurred while updating the enrollment. Please retry later."
|
|
829
833
|
},
|
|
830
|
-
"hooks.
|
|
831
|
-
"description": "Error message shown to the user when
|
|
832
|
-
"message": "
|
|
834
|
+
"hooks.useOrders.errorGet": {
|
|
835
|
+
"description": "Error message shown to the user when orders fetch request fails.",
|
|
836
|
+
"message": "An error occurred while fetching orders. Please retry later."
|
|
837
|
+
},
|
|
838
|
+
"hooks.useOrders.errorNotFound": {
|
|
839
|
+
"description": "Error message shown to the user when no orders matches.",
|
|
840
|
+
"message": "Cannot find the orders."
|
|
833
841
|
},
|
|
834
|
-
"hooks.useProduct.
|
|
842
|
+
"hooks.useProduct.errorGet": {
|
|
835
843
|
"description": "Error message shown to the user when product fetch request fails.",
|
|
836
|
-
"message": "
|
|
844
|
+
"message": "An error occurred while fetching product. Please retry later."
|
|
845
|
+
},
|
|
846
|
+
"hooks.useProduct.errorNotFound": {
|
|
847
|
+
"description": "Error message shown to the user when no product matches.",
|
|
848
|
+
"message": "Cannot find the product."
|
|
837
849
|
},
|
|
838
850
|
"hooks.useResources.errorCreate": {
|
|
839
851
|
"description": "Error message shown to the user when resource creation request fails.",
|
package/i18n/locales/fr-FR.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"16uca+": {
|
|
3
3
|
"description": "",
|
|
4
|
-
"message": "
|
|
4
|
+
"message": "Sous \"{value}\""
|
|
5
5
|
},
|
|
6
6
|
"9vqPaF": {
|
|
7
7
|
"description": "",
|
|
8
|
-
"message": "
|
|
8
|
+
"message": "Racine"
|
|
9
9
|
},
|
|
10
10
|
"components.AddressesManagement.actionPromotion": {
|
|
11
11
|
"description": "Action name for address promotion.",
|
|
@@ -175,6 +175,10 @@
|
|
|
175
175
|
"description": "Accessible text for the initial loading spinner displayed when product is fetching",
|
|
176
176
|
"message": "Chargement des informations produit..."
|
|
177
177
|
},
|
|
178
|
+
"components.CourseProductItem.pending": {
|
|
179
|
+
"description": "Message displayed when authenticated user has purchased the product but order is still pending",
|
|
180
|
+
"message": "En attente"
|
|
181
|
+
},
|
|
178
182
|
"components.CourseProductsList.end": {
|
|
179
183
|
"description": "End label displayed in the header of course run dates section",
|
|
180
184
|
"message": "Fin"
|
|
@@ -827,14 +831,22 @@
|
|
|
827
831
|
"description": "Error message shown to the user when enrollment update request fails.",
|
|
828
832
|
"message": "Une erreur s'est produite lors de la modification de votre inscription. Veuillez réessayer plus tard."
|
|
829
833
|
},
|
|
830
|
-
"hooks.
|
|
831
|
-
"description": "Error message shown to the user when
|
|
832
|
-
"message": "
|
|
834
|
+
"hooks.useOrders.errorGet": {
|
|
835
|
+
"description": "Error message shown to the user when orders fetch request fails.",
|
|
836
|
+
"message": "Une erreur s'est produite lors de la récupération de vos commandes. Veuillez réessayer plus tard."
|
|
837
|
+
},
|
|
838
|
+
"hooks.useOrders.errorNotFound": {
|
|
839
|
+
"description": "Error message shown to the user when no orders matches.",
|
|
840
|
+
"message": "Commandes introuvables."
|
|
833
841
|
},
|
|
834
|
-
"hooks.useProduct.
|
|
842
|
+
"hooks.useProduct.errorGet": {
|
|
835
843
|
"description": "Error message shown to the user when product fetch request fails.",
|
|
836
844
|
"message": "Une erreur s'est produite lors de la récupération du produit. Veuillez réessayer plus tard."
|
|
837
845
|
},
|
|
846
|
+
"hooks.useProduct.errorNotFound": {
|
|
847
|
+
"description": "Error message shown to the user when no product matches.",
|
|
848
|
+
"message": "Produit introuvable."
|
|
849
|
+
},
|
|
838
850
|
"hooks.useResources.errorCreate": {
|
|
839
851
|
"description": "Error message shown to the user when resource creation request fails.",
|
|
840
852
|
"message": "Une erreur s'est produite lors de la création de la ressource. Veuillez réessayer plus tard."
|
package/i18n/locales/ko-KR.json
CHANGED
|
@@ -175,6 +175,10 @@
|
|
|
175
175
|
"description": "Accessible text for the initial loading spinner displayed when product is fetching",
|
|
176
176
|
"message": "Loading product information..."
|
|
177
177
|
},
|
|
178
|
+
"components.CourseProductItem.pending": {
|
|
179
|
+
"description": "Message displayed when authenticated user has purchased the product but order is still pending",
|
|
180
|
+
"message": "Pending"
|
|
181
|
+
},
|
|
178
182
|
"components.CourseProductsList.end": {
|
|
179
183
|
"description": "End label displayed in the header of course run dates section",
|
|
180
184
|
"message": "End"
|
|
@@ -827,14 +831,22 @@
|
|
|
827
831
|
"description": "Error message shown to the user when enrollment update request fails.",
|
|
828
832
|
"message": "An error occurred while updating the enrollment. Please retry later."
|
|
829
833
|
},
|
|
830
|
-
"hooks.
|
|
831
|
-
"description": "Error message shown to the user when
|
|
832
|
-
"message": "
|
|
834
|
+
"hooks.useOrders.errorGet": {
|
|
835
|
+
"description": "Error message shown to the user when orders fetch request fails.",
|
|
836
|
+
"message": "An error occurred while fetching orders. Please retry later."
|
|
837
|
+
},
|
|
838
|
+
"hooks.useOrders.errorNotFound": {
|
|
839
|
+
"description": "Error message shown to the user when no orders matches.",
|
|
840
|
+
"message": "Cannot find the orders."
|
|
833
841
|
},
|
|
834
|
-
"hooks.useProduct.
|
|
842
|
+
"hooks.useProduct.errorGet": {
|
|
835
843
|
"description": "Error message shown to the user when product fetch request fails.",
|
|
836
844
|
"message": "An error occurred while fetching product. Please retry later."
|
|
837
845
|
},
|
|
846
|
+
"hooks.useProduct.errorNotFound": {
|
|
847
|
+
"description": "Error message shown to the user when no product matches.",
|
|
848
|
+
"message": "Cannot find the product."
|
|
849
|
+
},
|
|
838
850
|
"hooks.useResources.errorCreate": {
|
|
839
851
|
"description": "Error message shown to the user when resource creation request fails.",
|
|
840
852
|
"message": "An error occurred while creating a resource. Please retry later."
|
package/i18n/locales/pt-PT.json
CHANGED
|
@@ -175,6 +175,10 @@
|
|
|
175
175
|
"description": "Accessible text for the initial loading spinner displayed when product is fetching",
|
|
176
176
|
"message": "A carregar informação do produto..."
|
|
177
177
|
},
|
|
178
|
+
"components.CourseProductItem.pending": {
|
|
179
|
+
"description": "Message displayed when authenticated user has purchased the product but order is still pending",
|
|
180
|
+
"message": "Pending"
|
|
181
|
+
},
|
|
178
182
|
"components.CourseProductsList.end": {
|
|
179
183
|
"description": "End label displayed in the header of course run dates section",
|
|
180
184
|
"message": "Fim"
|
|
@@ -827,13 +831,21 @@
|
|
|
827
831
|
"description": "Error message shown to the user when enrollment update request fails.",
|
|
828
832
|
"message": "Ocorreu um erro durante a atualização das inscrições. Por favor, tente mais tarde."
|
|
829
833
|
},
|
|
830
|
-
"hooks.
|
|
831
|
-
"description": "Error message shown to the user when
|
|
832
|
-
"message": "
|
|
834
|
+
"hooks.useOrders.errorGet": {
|
|
835
|
+
"description": "Error message shown to the user when orders fetch request fails.",
|
|
836
|
+
"message": "Ocorreu um erro durante o processamento das encomendas. Por favor, tente mais tarde."
|
|
837
|
+
},
|
|
838
|
+
"hooks.useOrders.errorNotFound": {
|
|
839
|
+
"description": "Error message shown to the user when no orders matches.",
|
|
840
|
+
"message": "Não foi possível encontrar encomendas."
|
|
833
841
|
},
|
|
834
|
-
"hooks.useProduct.
|
|
842
|
+
"hooks.useProduct.errorGet": {
|
|
835
843
|
"description": "Error message shown to the user when product fetch request fails.",
|
|
836
|
-
"message": "Ocorreu um erro durante o processamento do produto.
|
|
844
|
+
"message": "Ocorreu um erro durante o processamento do produto. Por favor, tente mais tarde."
|
|
845
|
+
},
|
|
846
|
+
"hooks.useProduct.errorNotFound": {
|
|
847
|
+
"description": "Error message shown to the user when no product matches.",
|
|
848
|
+
"message": "Não foi possível encontrar o produto."
|
|
837
849
|
},
|
|
838
850
|
"hooks.useResources.errorCreate": {
|
|
839
851
|
"description": "Error message shown to the user when resource creation request fails.",
|
package/i18n/locales/ru-RU.json
CHANGED
|
@@ -175,6 +175,10 @@
|
|
|
175
175
|
"description": "Accessible text for the initial loading spinner displayed when product is fetching",
|
|
176
176
|
"message": "Loading product information..."
|
|
177
177
|
},
|
|
178
|
+
"components.CourseProductItem.pending": {
|
|
179
|
+
"description": "Message displayed when authenticated user has purchased the product but order is still pending",
|
|
180
|
+
"message": "Pending"
|
|
181
|
+
},
|
|
178
182
|
"components.CourseProductsList.end": {
|
|
179
183
|
"description": "End label displayed in the header of course run dates section",
|
|
180
184
|
"message": "Окончание"
|
|
@@ -827,14 +831,22 @@
|
|
|
827
831
|
"description": "Error message shown to the user when enrollment update request fails.",
|
|
828
832
|
"message": "An error occurred while updating the enrollment. Please retry later."
|
|
829
833
|
},
|
|
830
|
-
"hooks.
|
|
831
|
-
"description": "Error message shown to the user when
|
|
832
|
-
"message": "
|
|
834
|
+
"hooks.useOrders.errorGet": {
|
|
835
|
+
"description": "Error message shown to the user when orders fetch request fails.",
|
|
836
|
+
"message": "An error occurred while fetching orders. Please retry later."
|
|
837
|
+
},
|
|
838
|
+
"hooks.useOrders.errorNotFound": {
|
|
839
|
+
"description": "Error message shown to the user when no orders matches.",
|
|
840
|
+
"message": "Cannot find the orders."
|
|
833
841
|
},
|
|
834
|
-
"hooks.useProduct.
|
|
842
|
+
"hooks.useProduct.errorGet": {
|
|
835
843
|
"description": "Error message shown to the user when product fetch request fails.",
|
|
836
844
|
"message": "An error occurred while fetching product. Please retry later."
|
|
837
845
|
},
|
|
846
|
+
"hooks.useProduct.errorNotFound": {
|
|
847
|
+
"description": "Error message shown to the user when no product matches.",
|
|
848
|
+
"message": "Cannot find the product."
|
|
849
|
+
},
|
|
838
850
|
"hooks.useResources.errorCreate": {
|
|
839
851
|
"description": "Error message shown to the user when resource creation request fails.",
|
|
840
852
|
"message": "An error occurred while creating a resource. Please retry later."
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import fetchMock from 'fetch-mock';
|
|
3
|
+
import { IntlProvider } from 'react-intl';
|
|
4
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
5
|
+
import { ContextFactory as mockContextFactory, ProductFactory } from 'utils/test/factories';
|
|
6
|
+
import { SessionProvider } from 'data/SessionProvider';
|
|
7
|
+
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
8
|
+
import PurchaseButton from './PurchaseButton';
|
|
9
|
+
|
|
10
|
+
jest.mock('utils/context', () => ({
|
|
11
|
+
__esModule: true,
|
|
12
|
+
default: mockContextFactory({
|
|
13
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.endpoint.test' },
|
|
14
|
+
joanie_backend: { endpoint: 'https://joanie.test' },
|
|
15
|
+
}).generate(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
describe('PurchaseButton', () => {
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
// As dialog is rendered through a Portal, we have to add the DOM element in which the dialog will be rendered.
|
|
21
|
+
const modalExclude = document.createElement('div');
|
|
22
|
+
modalExclude.setAttribute('id', 'modal-exclude');
|
|
23
|
+
document.body.appendChild(modalExclude);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
fetchMock.restore();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const Wrapper = ({ client, children }: React.PropsWithChildren<{ client: QueryClient }>) => (
|
|
31
|
+
<IntlProvider locale="en">
|
|
32
|
+
<QueryClientProvider client={client}>
|
|
33
|
+
<SessionProvider>{children}</SessionProvider>
|
|
34
|
+
</QueryClientProvider>
|
|
35
|
+
</IntlProvider>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
it('shows a login button if user is not authenticated', async () => {
|
|
39
|
+
const product = ProductFactory.generate();
|
|
40
|
+
|
|
41
|
+
await act(async () => {
|
|
42
|
+
render(
|
|
43
|
+
<Wrapper client={createTestQueryClient({ user: null })}>
|
|
44
|
+
<PurchaseButton product={product} disabled={false} />
|
|
45
|
+
</Wrapper>,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await screen.findByRole('button', { name: `Login to purchase "${product.title}"` });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('shows cta to open sale tunnel when user is authenticated', async () => {
|
|
53
|
+
const product = ProductFactory.generate();
|
|
54
|
+
fetchMock
|
|
55
|
+
.get('https://joanie.test/api/v1.0/addresses/', [])
|
|
56
|
+
.get('https://joanie.test/api/v1.0/credit-cards/', [])
|
|
57
|
+
.get('https://joanie.test/api/v1.0/orders/', []);
|
|
58
|
+
|
|
59
|
+
await act(async () => {
|
|
60
|
+
render(
|
|
61
|
+
<Wrapper client={createTestQueryClient({ user: true })}>
|
|
62
|
+
<PurchaseButton product={product} disabled={false} />
|
|
63
|
+
</Wrapper>,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
fetchMock.resetHistory();
|
|
68
|
+
|
|
69
|
+
// Only CTA is displayed
|
|
70
|
+
const button = screen.getByRole('button', { name: product.call_to_action });
|
|
71
|
+
|
|
72
|
+
// - SaleTunnel should not be opened
|
|
73
|
+
expect(screen.queryByTestId('SaleTunnel__modal')).toBeNull();
|
|
74
|
+
|
|
75
|
+
// Then user can enter into the sale tunnel and follow its 3 steps
|
|
76
|
+
await act(async () => {
|
|
77
|
+
fireEvent.click(button);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// - SaleTunnel should have been opened
|
|
81
|
+
screen.getByTestId('SaleTunnel__modal');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('renders a disabled CTA if one target course has no course runs', async () => {
|
|
85
|
+
const product = ProductFactory.generate();
|
|
86
|
+
product.target_courses[0].course_runs = [];
|
|
87
|
+
fetchMock
|
|
88
|
+
.get('https://joanie.test/api/v1.0/addresses/', [])
|
|
89
|
+
.get('https://joanie.test/api/v1.0/credit-cards/', [])
|
|
90
|
+
.get('https://joanie.test/api/v1.0/orders/', []);
|
|
91
|
+
|
|
92
|
+
await act(async () => {
|
|
93
|
+
render(
|
|
94
|
+
<Wrapper client={createTestQueryClient({ user: true })}>
|
|
95
|
+
<PurchaseButton product={product} disabled={false} />
|
|
96
|
+
</Wrapper>,
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// CTA is displayed but disabled
|
|
101
|
+
const button: HTMLButtonElement = screen.getByRole('button', { name: product.call_to_action });
|
|
102
|
+
expect(button.disabled).toBe(true);
|
|
103
|
+
|
|
104
|
+
// Further, a message is displayed to explain why the CTA is disabled
|
|
105
|
+
screen.findByText(
|
|
106
|
+
'At least one course has no course runs, this product is not currently available for sale',
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('renders a disabled CTA if product has no target courses', async () => {
|
|
111
|
+
const product = ProductFactory.generate();
|
|
112
|
+
product.target_courses = [];
|
|
113
|
+
fetchMock
|
|
114
|
+
.get('https://joanie.test/api/v1.0/addresses/', [])
|
|
115
|
+
.get('https://joanie.test/api/v1.0/credit-cards/', [])
|
|
116
|
+
.get('https://joanie.test/api/v1.0/orders/', []);
|
|
117
|
+
|
|
118
|
+
await act(async () => {
|
|
119
|
+
render(
|
|
120
|
+
<Wrapper client={createTestQueryClient({ user: true })}>
|
|
121
|
+
<PurchaseButton product={product} disabled={false} />
|
|
122
|
+
</Wrapper>,
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// CTA is displayed but disabled
|
|
127
|
+
const button: HTMLButtonElement = screen.getByRole('button', { name: product.call_to_action });
|
|
128
|
+
expect(button.disabled).toBe(true);
|
|
129
|
+
|
|
130
|
+
// Further, a message is displayed to explain why the CTA is disabled
|
|
131
|
+
screen.findByText(
|
|
132
|
+
'At least one course has no course runs, this product is not currently available for sale',
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('does not render CTA if disabled property is false', async () => {
|
|
137
|
+
const product = ProductFactory.generate();
|
|
138
|
+
fetchMock
|
|
139
|
+
.get('https://joanie.test/api/v1.0/addresses/', [])
|
|
140
|
+
.get('https://joanie.test/api/v1.0/credit-cards/', [])
|
|
141
|
+
.get('https://joanie.test/api/v1.0/orders/', []);
|
|
142
|
+
|
|
143
|
+
await act(async () => {
|
|
144
|
+
render(
|
|
145
|
+
<Wrapper client={createTestQueryClient({ user: true })}>
|
|
146
|
+
<PurchaseButton product={product} disabled={true} />
|
|
147
|
+
</Wrapper>,
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// CTA is not displayed
|
|
152
|
+
expect(screen.queryByRole('button', { name: product.call_to_action })).not.toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import { useSession } from 'data/SessionProvider';
|
|
4
|
+
import * as Joanie from 'types/Joanie';
|
|
5
|
+
import { Priority } from 'types';
|
|
6
|
+
import SaleTunnel from 'components/SaleTunnel';
|
|
7
|
+
|
|
8
|
+
const messages = defineMessages({
|
|
9
|
+
loginToPurchase: {
|
|
10
|
+
defaultMessage: 'Login to purchase {product}',
|
|
11
|
+
description: "Label displayed inside the product's CTA when user is not logged in",
|
|
12
|
+
id: 'components.SaleTunnel.loginToPurchase',
|
|
13
|
+
},
|
|
14
|
+
noCourseRunToPurchase: {
|
|
15
|
+
defaultMessage:
|
|
16
|
+
'At least one course has no course runs, this product is not currently available for sale',
|
|
17
|
+
description: "Label displayed inside the product's when there is no courseRun",
|
|
18
|
+
id: 'components.SaleTunnel.noCourseRunToPurchase',
|
|
19
|
+
},
|
|
20
|
+
callToActionDescription: {
|
|
21
|
+
defaultMessage: 'Purchase {product}',
|
|
22
|
+
description:
|
|
23
|
+
'Additional description announced by screen readers when focusing the call to action buying button',
|
|
24
|
+
id: 'components.SaleTunnel.callToActionDescription',
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
interface PurchaseButtonProps {
|
|
29
|
+
product: Joanie.Product;
|
|
30
|
+
disabled: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PurchaseButton = ({ product, disabled }: PurchaseButtonProps) => {
|
|
34
|
+
const intl = useIntl();
|
|
35
|
+
const { user, login } = useSession();
|
|
36
|
+
const [isSaleTunnelOpen, setIsSaleTunnelOpen] = useState(false);
|
|
37
|
+
|
|
38
|
+
const isOpenedCourseRun = (courseRun: Joanie.CourseRun) =>
|
|
39
|
+
courseRun.state.priority <= Priority.FUTURE_NOT_YET_OPEN;
|
|
40
|
+
|
|
41
|
+
const hasAtLeastOneCourseRun = useMemo(() => {
|
|
42
|
+
return (
|
|
43
|
+
product.target_courses.length > 0 &&
|
|
44
|
+
!product.target_courses.some(({ course_runs }) => !course_runs.some(isOpenedCourseRun))
|
|
45
|
+
);
|
|
46
|
+
}, [product]);
|
|
47
|
+
|
|
48
|
+
if (!user) {
|
|
49
|
+
return (
|
|
50
|
+
<button className="product-item__cta" onClick={login}>
|
|
51
|
+
<FormattedMessage
|
|
52
|
+
{...messages.loginToPurchase}
|
|
53
|
+
values={{ product: <span className="offscreen">"{product.title}"</span> }}
|
|
54
|
+
/>
|
|
55
|
+
</button>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
{!disabled && (
|
|
62
|
+
<>
|
|
63
|
+
<button
|
|
64
|
+
data-testid="PurchaseButton__cta"
|
|
65
|
+
className="product-item__cta"
|
|
66
|
+
onClick={() => hasAtLeastOneCourseRun && setIsSaleTunnelOpen(true)}
|
|
67
|
+
// so that the button is explicit on its own, we add a description that doesn't
|
|
68
|
+
// rely on the text coming from the CMS
|
|
69
|
+
// eslint-disable-next-line jsx-a11y/aria-props
|
|
70
|
+
aria-description={intl.formatMessage(messages.callToActionDescription, {
|
|
71
|
+
product: product.title,
|
|
72
|
+
})}
|
|
73
|
+
disabled={!hasAtLeastOneCourseRun}
|
|
74
|
+
>
|
|
75
|
+
{product.call_to_action}
|
|
76
|
+
</button>
|
|
77
|
+
{!hasAtLeastOneCourseRun && (
|
|
78
|
+
<p className="product-item__no-course-run">
|
|
79
|
+
<FormattedMessage {...messages.noCourseRunToPurchase} />
|
|
80
|
+
</p>
|
|
81
|
+
)}
|
|
82
|
+
</>
|
|
83
|
+
)}
|
|
84
|
+
<SaleTunnel
|
|
85
|
+
isOpen={isSaleTunnelOpen}
|
|
86
|
+
product={product}
|
|
87
|
+
onClose={() => setIsSaleTunnelOpen(false)}
|
|
88
|
+
/>
|
|
89
|
+
</>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export default PurchaseButton;
|
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
min-height: 100px;
|
|
9
9
|
overflow: hidden;
|
|
10
10
|
|
|
11
|
+
&--has-error {
|
|
12
|
+
align-items: center;
|
|
13
|
+
border-color: r-theme-val(product-item, feedback-color);
|
|
14
|
+
display: flex;
|
|
15
|
+
|
|
16
|
+
& > .product-widget__content {
|
|
17
|
+
color: r-theme-val(product-item, feedback-color);
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: row;
|
|
20
|
+
gap: 1rem;
|
|
21
|
+
margin-bottom: 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
&:not(:last-child) {
|
|
12
26
|
margin-bottom: 1.5rem;
|
|
13
27
|
}
|