richie-education 2.24.0 → 2.25.0-b2.dev101

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.
Files changed (492) hide show
  1. package/.eslintignore +2 -0
  2. package/.nvmrc +1 -1
  3. package/.prettierignore +2 -0
  4. package/.storybook/preview-body.html +1 -1
  5. package/.storybook/preview.tsx +5 -2
  6. package/cunningham.cjs +52 -43
  7. package/i18n/locales/ar-SA.json +500 -100
  8. package/i18n/locales/es-ES.json +500 -100
  9. package/i18n/locales/fa-IR.json +500 -100
  10. package/i18n/locales/fr-CA.json +564 -164
  11. package/i18n/locales/fr-FR.json +517 -117
  12. package/i18n/locales/ko-KR.json +500 -100
  13. package/i18n/locales/pt-PT.json +526 -126
  14. package/i18n/locales/ru-RU.json +500 -100
  15. package/i18n/locales/vi-VN.json +1734 -0
  16. package/jest/setup.ts +11 -1
  17. package/js/api/enrollment.ts +1 -1
  18. package/js/api/joanie.spec.ts +63 -2
  19. package/js/api/joanie.ts +218 -141
  20. package/js/api/lms/dummy.spec.ts +9 -1
  21. package/js/api/lms/dummy.ts +63 -10
  22. package/js/api/lms/joanie.spec.ts +49 -31
  23. package/js/api/lms/joanie.ts +53 -35
  24. package/js/api/lms/openedx-hawthorn.spec.ts +27 -11
  25. package/js/api/lms/openedx-hawthorn.ts +7 -6
  26. package/js/components/AddressesManagement/AddressForm/index.spec.tsx +157 -0
  27. package/js/components/AddressesManagement/AddressForm/index.stories.tsx +36 -0
  28. package/js/components/AddressesManagement/AddressForm/index.tsx +163 -0
  29. package/js/components/AddressesManagement/{validationSchema.ts → AddressForm/validationSchema.ts} +1 -23
  30. package/js/components/AddressesManagement/_styles.scss +1 -1
  31. package/js/components/AddressesManagement/index.spec.tsx +171 -202
  32. package/js/components/AddressesManagement/index.stories.tsx +29 -0
  33. package/js/components/AddressesManagement/index.tsx +11 -3
  34. package/js/components/Badge/index.spec.tsx +17 -0
  35. package/js/components/Badge/index.stories.tsx +22 -0
  36. package/js/components/Badge/index.tsx +18 -0
  37. package/js/components/Banner/index.tsx +6 -1
  38. package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +332 -0
  39. package/js/components/ContractFrame/AbstractContractFrame.tsx +289 -0
  40. package/js/components/ContractFrame/LearnerContractFrame.spec.tsx +125 -0
  41. package/js/components/ContractFrame/LearnerContractFrame.tsx +42 -0
  42. package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +167 -0
  43. package/js/components/ContractFrame/OrganizationContractFrame.tsx +70 -0
  44. package/js/components/ContractFrame/_styles.scss +62 -0
  45. package/js/components/ContractFrame/iframe-manager.js +158 -0
  46. package/js/components/ContractFrame/index.ts +5 -0
  47. package/js/components/ContractStatus/index.spec.tsx +120 -0
  48. package/js/components/ContractStatus/index.tsx +67 -0
  49. package/js/components/CourseGlimpse/CourseGlimpseFooter.tsx +7 -7
  50. package/js/components/CourseGlimpse/index.tsx +5 -1
  51. package/js/components/CourseGlimpse/utils.ts +24 -16
  52. package/js/components/CourseGlimpseList/index.spec.tsx +1 -1
  53. package/js/components/CourseGlimpseList/index.tsx +1 -1
  54. package/js/components/CourseGlimpseList/utils.ts +3 -2
  55. package/js/components/DjangoCMSTemplate/index.spec.tsx +2 -2
  56. package/js/components/DownloadCertificateButton/index.tsx +58 -0
  57. package/js/components/DownloadContractButton/index.spec.tsx +155 -0
  58. package/js/components/DownloadContractButton/index.tsx +48 -0
  59. package/js/components/Form/CountrySelectField.tsx +28 -16
  60. package/js/components/Form/Input/index.spec.tsx +76 -0
  61. package/js/components/Form/Input/index.tsx +47 -0
  62. package/js/components/Form/Select/index.spec.tsx +99 -0
  63. package/js/components/Form/Select/index.tsx +43 -0
  64. package/js/components/{AddressesManagement → Form}/ValidationErrors.ts +10 -5
  65. package/js/components/Form/index.ts +5 -1
  66. package/js/components/Form/messages.ts +14 -0
  67. package/js/components/Form/test-utils.ts +19 -0
  68. package/js/components/Form/utils.spec.ts +72 -0
  69. package/js/components/Form/utils.ts +37 -0
  70. package/js/components/Icon/index.stories.tsx +2 -1
  71. package/js/components/Modal/_styles.scss +0 -8
  72. package/js/components/Modal/index.spec.tsx +0 -6
  73. package/js/components/Modal/index.tsx +23 -17
  74. package/js/components/PaymentButton/_styles.scss +26 -0
  75. package/js/{widgets/CourseProductItem → components/PaymentButton}/components/PaymentInterfaces/Dummy.tsx +1 -1
  76. package/js/{widgets/CourseProductItem → components/PaymentButton}/components/PaymentInterfaces/PayplugLightbox.tsx +30 -7
  77. package/js/{widgets/CourseProductItem → components/PaymentButton}/components/PaymentInterfaces/__mocks__/index.tsx +1 -1
  78. package/js/{widgets/CourseProductItem → components/PaymentButton}/components/PaymentInterfaces/index.spec.tsx +5 -3
  79. package/js/{widgets/CourseProductItem → components/PaymentButton}/components/PaymentInterfaces/index.tsx +7 -5
  80. package/js/components/PaymentButton/hooks/useTerms.tsx +74 -0
  81. package/js/components/PaymentButton/index.spec.tsx +1038 -0
  82. package/js/{widgets/CourseProductItem/components → components}/PaymentButton/index.tsx +94 -41
  83. package/js/components/PurchaseButton/index.spec.tsx +377 -0
  84. package/js/components/PurchaseButton/index.stories.tsx +15 -0
  85. package/js/{widgets/CourseProductItem/components → components}/PurchaseButton/index.tsx +72 -23
  86. package/js/components/PurchaseButton/styles.scss +7 -0
  87. package/js/components/RegisteredAddress/_styles.scss +1 -3
  88. package/js/components/RegisteredAddress/index.spec.tsx +1 -1
  89. package/js/components/RegisteredAddress/index.stories.tsx +40 -0
  90. package/js/components/RegisteredAddress/index.tsx +17 -19
  91. package/js/components/SaleTunnel/_styles.scss +11 -0
  92. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/RegisteredCreditCard/index.tsx +4 -10
  93. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/SaleTunnelStepPayment/_styles.scss +7 -0
  94. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/SaleTunnelStepPayment/index.spec.tsx +85 -61
  95. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/SaleTunnelStepPayment/index.tsx +55 -57
  96. package/js/components/SaleTunnel/components/SaleTunnelStepResume/_styles.scss +63 -0
  97. package/js/components/SaleTunnel/components/SaleTunnelStepResume/index.spec.tsx +80 -0
  98. package/js/components/SaleTunnel/components/SaleTunnelStepResume/index.tsx +88 -0
  99. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/SaleTunnelStepValidation/CourseRunsList.tsx +6 -2
  100. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/SaleTunnelStepValidation/_styles.scss +5 -0
  101. package/js/components/SaleTunnel/components/SaleTunnelStepValidation/index.spec.tsx +170 -0
  102. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/SaleTunnelStepValidation/index.tsx +41 -10
  103. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/StepBreadcrumb/index.spec.tsx +1 -1
  104. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/StepBreadcrumb/index.tsx +1 -1
  105. package/js/components/SaleTunnel/context.tsx +44 -0
  106. package/js/{widgets/CourseProductItem/components → components}/SaleTunnel/index.spec.tsx +27 -22
  107. package/js/{widgets/CourseProductItem/components → components}/SaleTunnel/index.tsx +96 -24
  108. package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +135 -0
  109. package/js/components/SignContractButton/index.spec.tsx +213 -0
  110. package/js/components/SignContractButton/index.tsx +97 -0
  111. package/js/components/SuccessIcon/_styles.scss +66 -0
  112. package/js/components/SuccessIcon/index.tsx +10 -0
  113. package/js/components/TeacherDashboardCourseList/_styles.scss +0 -1
  114. package/js/components/TeacherDashboardCourseList/index.spec.tsx +9 -9
  115. package/js/components/TeacherDashboardCourseList/index.tsx +25 -31
  116. package/js/contexts/JoanieApiContext/index.spec.tsx +1 -1
  117. package/js/contexts/SessionContext/BaseSessionProvider.tsx +12 -22
  118. package/js/contexts/SessionContext/JoanieSessionProvider.spec.tsx +14 -0
  119. package/js/contexts/SessionContext/JoanieSessionProvider.tsx +33 -34
  120. package/js/contexts/SessionContext/index.spec.tsx +6 -7
  121. package/js/hooks/useBreadcrumbsPlaceholders.tsx +1 -1
  122. package/js/hooks/useContractAbilities/index.spec.ts +27 -0
  123. package/js/hooks/useContractAbilities/index.ts +8 -0
  124. package/js/hooks/useContractArchive/index.download.spec.tsx +126 -0
  125. package/js/hooks/useContractArchive/index.spec.tsx +91 -0
  126. package/js/hooks/useContractArchive/index.ts +64 -0
  127. package/js/hooks/useContracts/index.tsx +68 -0
  128. package/js/hooks/useCourseProductRelation/index.ts +8 -5
  129. package/js/hooks/useCourseProductUnion/index.spec.tsx +14 -10
  130. package/js/hooks/useCourseProductUnion/index.ts +6 -2
  131. package/js/hooks/useCourseProducts.ts +45 -0
  132. package/js/hooks/useCourseSearchParams/computeNewFilterValue.ts +3 -3
  133. package/js/hooks/useCourses/index.spec.tsx +2 -2
  134. package/js/hooks/useCourses/index.ts +4 -4
  135. package/js/hooks/useCreditCards/index.spec.tsx +4 -4
  136. package/js/hooks/useDashboardAddressForm.tsx +85 -87
  137. package/js/hooks/useDownloadCertificate/index.spec.tsx +19 -6
  138. package/js/hooks/useDownloadCertificate/index.tsx +2 -20
  139. package/js/hooks/useEnrollments.ts +1 -1
  140. package/js/hooks/useJoanieUserAbilities/index.not.isJoanieEnabled.spec.tsx +17 -0
  141. package/js/hooks/useJoanieUserAbilities/index.spec.tsx +68 -0
  142. package/js/hooks/useJoanieUserAbilities/index.tsx +11 -0
  143. package/js/hooks/useJoanieUserProfile.tsx +34 -0
  144. package/js/hooks/useOrders.ts +69 -26
  145. package/js/hooks/useOrganizations/index.ts +1 -1
  146. package/js/hooks/useProductOrder/index.spec.tsx +113 -0
  147. package/js/hooks/useProductOrder/index.tsx +33 -0
  148. package/js/hooks/useQueryKeyInvalidateListener.tsx +16 -0
  149. package/js/hooks/useResources/index.spec.tsx +30 -29
  150. package/js/hooks/useResources/index.tsx +11 -4
  151. package/js/hooks/useResources/useResourcesOmniscient.ts +2 -2
  152. package/js/hooks/useResources/useResourcesRoot.ts +21 -17
  153. package/js/hooks/useTeacherPendingContractsCount/index.ts +34 -0
  154. package/js/hooks/useUnionResource/index.spec.tsx +5 -2
  155. package/js/hooks/useUnionResource/index.ts +20 -3
  156. package/js/hooks/useUnionResource/utils/fetchEntity.ts +5 -4
  157. package/js/index.tsx +2 -2
  158. package/js/pages/DashboardAddressesManagement/DashboardAddressBox.tsx +1 -1
  159. package/js/pages/DashboardAddressesManagement/DashboardCreateAddress.spec.tsx +79 -72
  160. package/js/pages/DashboardAddressesManagement/DashboardCreateAddress.tsx +4 -4
  161. package/js/pages/DashboardAddressesManagement/DashboardEditAddress.spec.tsx +32 -23
  162. package/js/pages/DashboardAddressesManagement/DashboardEditAddress.tsx +6 -6
  163. package/js/pages/DashboardAddressesManagement/DashboardEditAddressLoader.tsx +2 -2
  164. package/js/pages/DashboardAddressesManagement/index.spec.tsx +4 -3
  165. package/js/pages/DashboardAddressesManagement/index.tsx +5 -5
  166. package/js/pages/DashboardCertificates/index.spec.tsx +3 -2
  167. package/js/pages/DashboardCertificates/index.tsx +2 -1
  168. package/js/pages/DashboardContracts/_styles.scss +8 -0
  169. package/js/pages/DashboardContracts/index.spec.tsx +147 -0
  170. package/js/pages/DashboardContracts/index.tsx +76 -0
  171. package/js/pages/DashboardCourses/index.spec.tsx +81 -61
  172. package/js/pages/DashboardCourses/index.tsx +15 -12
  173. package/js/pages/DashboardCourses/useOrdersEnrollments.tsx +34 -8
  174. package/js/pages/DashboardCreditCardsManagement/DashboardCreditCardBox.tsx +1 -1
  175. package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.spec.tsx +7 -6
  176. package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.tsx +43 -45
  177. package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCardLoader.tsx +2 -2
  178. package/js/pages/DashboardCreditCardsManagement/index.spec.tsx +13 -12
  179. package/js/pages/DashboardCreditCardsManagement/index.tsx +3 -3
  180. package/js/pages/DashboardOrderLayout/_styles.scss +5 -0
  181. package/js/pages/DashboardOrderLayout/index.spec.tsx +8 -8
  182. package/js/pages/DashboardOrderLayout/index.tsx +11 -6
  183. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +358 -0
  184. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +129 -0
  185. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardCourseContractsLayout/index.tsx +26 -0
  186. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardOrganizationContractsLayout/index.tsx +26 -0
  187. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +136 -0
  188. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +144 -0
  189. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +73 -0
  190. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +185 -0
  191. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +47 -0
  192. package/js/pages/TeacherDashboardContractsLayout/components/ContractFiltersBar/index.spec.tsx +179 -0
  193. package/js/pages/TeacherDashboardContractsLayout/components/ContractFiltersBar/index.tsx +86 -0
  194. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +109 -0
  195. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +60 -0
  196. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +124 -0
  197. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +73 -0
  198. package/js/pages/TeacherDashboardContractsLayout/hooks/useDefaultOrganizationId/index.spec.tsx +134 -0
  199. package/js/pages/TeacherDashboardContractsLayout/hooks/useDefaultOrganizationId/index.tsx +28 -0
  200. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +85 -0
  201. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +50 -0
  202. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +266 -0
  203. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +153 -0
  204. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.spec.tsx +100 -0
  205. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +27 -0
  206. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +193 -0
  207. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +44 -0
  208. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +32 -0
  209. package/js/pages/TeacherDashboardContractsLayout/index.ts +2 -0
  210. package/js/pages/TeacherDashboardContractsLayout/styles.scss +15 -0
  211. package/js/pages/{TeacherCourseDashboardLoader → TeacherDashboardCourseLoader}/CourseRunList/CourseRunListCell/index.spec.tsx +1 -1
  212. package/js/pages/{TeacherCourseDashboardLoader → TeacherDashboardCourseLoader}/CourseRunList/_styles.scss +1 -2
  213. package/js/pages/{TeacherCourseDashboardLoader → TeacherDashboardCourseLoader}/CourseRunList/index.spec.tsx +2 -2
  214. package/js/pages/{TeacherCourseDashboardLoader → TeacherDashboardCourseLoader}/CourseRunList/utils.spec.tsx +1 -1
  215. package/js/pages/{TeacherCourseDashboardLoader → TeacherDashboardCourseLoader}/CourseRunList/utils.tsx +11 -10
  216. package/js/pages/{TeacherCourseDashboardLoader → TeacherDashboardCourseLoader}/index.tsx +9 -8
  217. package/js/pages/{TeacherCoursesDashboardLoader → TeacherDashboardCoursesLoader}/index.spec.tsx +17 -17
  218. package/js/pages/{TeacherCoursesDashboardLoader → TeacherDashboardCoursesLoader}/index.tsx +7 -7
  219. package/js/pages/{TeacherOrganizationCourseDashboardLoader → TeacherDashboardOrganizationCourseLoader}/index.tsx +5 -5
  220. package/js/pages/{TeacherTrainingDashboard/TeacherTrainingDashboardLoader.tsx → TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx} +10 -9
  221. package/js/pages/{TeacherTrainingDashboard → TeacherDashboardTraining}/index.spec.tsx +76 -6
  222. package/js/pages/{TeacherTrainingDashboard → TeacherDashboardTraining}/index.tsx +6 -6
  223. package/js/settings.dev.dist.ts +3 -0
  224. package/js/settings.ts +29 -1
  225. package/js/translations/ar-SA.json +1 -1
  226. package/js/translations/es-ES.json +1 -1
  227. package/js/translations/fa-IR.json +1 -1
  228. package/js/translations/fr-CA.json +1 -1
  229. package/js/translations/fr-FR.json +1 -1
  230. package/js/translations/ko-KR.json +1 -1
  231. package/js/translations/pt-PT.json +1 -1
  232. package/js/translations/ru-RU.json +1 -1
  233. package/js/translations/vi-VN.json +1 -0
  234. package/js/types/Joanie.ts +263 -80
  235. package/js/types/Suggestion.ts +2 -2
  236. package/js/types/User.ts +19 -1
  237. package/js/types/commonDataProps.ts +3 -0
  238. package/js/types/index.ts +1 -1
  239. package/js/utils/AbilitiesHelper/contractAbilities.spec.ts +35 -0
  240. package/js/utils/AbilitiesHelper/contractAbilities.ts +14 -0
  241. package/js/utils/AbilitiesHelper/index.ts +71 -0
  242. package/js/utils/AbilitiesHelper/joanieUserProfileAbilities.spec.ts +55 -0
  243. package/js/utils/AbilitiesHelper/joanieUserProfileAbilities.ts +16 -0
  244. package/js/utils/AbilitiesHelper/types.ts +36 -0
  245. package/js/utils/ContractHelper/index.spec.ts +73 -0
  246. package/js/utils/ContractHelper/index.ts +72 -0
  247. package/js/utils/CourseRuns/index.spec.tsx +20 -1
  248. package/js/utils/CourseRuns/index.ts +14 -2
  249. package/js/utils/CoursesHelper/index.spec.ts +45 -55
  250. package/js/utils/CoursesHelper/index.ts +6 -7
  251. package/js/utils/CreditCardHelper/index.spec.tsx +26 -22
  252. package/js/utils/CreditCardHelper/index.tsx +19 -6
  253. package/js/utils/ObjectHelper/index.spec.ts +18 -10
  254. package/js/utils/ObjectHelper/index.ts +9 -0
  255. package/js/utils/OrderHelper/index.ts +32 -0
  256. package/js/utils/ProductHelper/index.ts +5 -1
  257. package/js/utils/StringHelper/index.spec.tsx +11 -0
  258. package/js/utils/StringHelper/index.ts +8 -0
  259. package/js/utils/UserHelper/index.spec.ts +18 -0
  260. package/js/utils/UserHelper/index.ts +8 -0
  261. package/js/utils/download.ts +43 -0
  262. package/js/utils/errors/HttpError.ts +10 -0
  263. package/js/utils/indirection/window.ts +1 -1
  264. package/js/utils/react-query/createQueryClient.ts +12 -21
  265. package/js/utils/react-query/useLocalizedQueryKey.ts +1 -1
  266. package/js/utils/react-query/useSessionMutation/index.spec.tsx +8 -8
  267. package/js/utils/react-query/useSessionMutation/index.ts +6 -11
  268. package/js/utils/react-query/useSessionQuery/index.spec.tsx +36 -8
  269. package/js/utils/react-query/useSessionQuery/index.ts +14 -21
  270. package/js/utils/search/getSuggestionsSection/index.spec.ts +4 -3
  271. package/js/utils/search/getSuggestionsSection/index.ts +4 -1
  272. package/js/utils/search/index.tsx +8 -3
  273. package/js/utils/test/createTestQueryClient.ts +7 -7
  274. package/js/utils/test/expectBanner.ts +16 -3
  275. package/js/utils/test/factories/factories.ts +4 -4
  276. package/js/utils/test/factories/joanie.spec.ts +7 -0
  277. package/js/utils/test/factories/joanie.ts +214 -63
  278. package/js/utils/test/factories/reactQuery.ts +1 -1
  279. package/js/utils/test/factories/richie.ts +4 -2
  280. package/js/utils/test/mockCourseProductWithOrder.ts +28 -0
  281. package/js/utils/test/mockPaginatedResponse.ts +1 -1
  282. package/js/utils/test/render.tsx +72 -0
  283. package/js/utils/test/wrappers/IntlWrapper.tsx +23 -0
  284. package/js/utils/test/wrappers/JoanieAppWrapper.tsx +42 -0
  285. package/js/utils/test/wrappers/PresentationalAppWrapper.tsx +18 -0
  286. package/js/utils/test/wrappers/ReactQueryWrapper.tsx +16 -0
  287. package/js/utils/test/wrappers/RouterWrapper.tsx +29 -0
  288. package/js/utils/test/wrappers/types.ts +26 -0
  289. package/js/widgets/Dashboard/components/DashboardAvatar/_styles.scss +17 -5
  290. package/js/widgets/Dashboard/components/DashboardAvatar/index.spec.tsx +9 -2
  291. package/js/widgets/Dashboard/components/DashboardAvatar/index.tsx +16 -5
  292. package/js/widgets/Dashboard/components/DashboardBox/index.stories.tsx +1 -1
  293. package/js/widgets/Dashboard/components/DashboardBreadcrumbs/_styles.scss +1 -0
  294. package/js/widgets/Dashboard/components/DashboardBreadcrumbs/index.tsx +7 -4
  295. package/js/widgets/Dashboard/components/DashboardCard/index.spec.tsx +1 -1
  296. package/js/widgets/Dashboard/components/DashboardCard/index.stories.tsx +3 -3
  297. package/js/widgets/Dashboard/components/DashboardCard/index.tsx +2 -2
  298. package/js/widgets/Dashboard/components/DashboardItem/Certificate/index.spec.tsx +49 -9
  299. package/js/widgets/Dashboard/components/DashboardItem/Certificate/index.tsx +27 -73
  300. package/js/widgets/Dashboard/components/DashboardItem/CertificateStatus/index.spec.tsx +65 -0
  301. package/js/widgets/Dashboard/components/DashboardItem/CertificateStatus/index.tsx +59 -0
  302. package/js/widgets/Dashboard/components/DashboardItem/Contract/_styles.scss +29 -0
  303. package/js/widgets/Dashboard/components/DashboardItem/Contract/index.spec.tsx +197 -0
  304. package/js/widgets/Dashboard/components/DashboardItem/Contract/index.stories.tsx +34 -0
  305. package/js/widgets/Dashboard/components/DashboardItem/Contract/index.tsx +53 -0
  306. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/hooks/useCourseRunPeriodMessage.ts +76 -0
  307. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.spec.tsx +158 -0
  308. package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.stories.tsx → CourseEnrolling/index.stories.tsx} +6 -6
  309. package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.tsx → CourseEnrolling/index.tsx} +141 -84
  310. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.spec.tsx +40 -37
  311. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +29 -11
  312. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +248 -0
  313. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +89 -0
  314. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +188 -117
  315. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +124 -78
  316. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +299 -0
  317. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +286 -0
  318. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderReadonly.stories.tsx +9 -5
  319. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderWritable.stories.tsx +10 -6
  320. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.spec.tsx +121 -0
  321. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +98 -0
  322. package/js/widgets/Dashboard/components/DashboardItem/Order/_styles.scss +43 -0
  323. package/js/widgets/Dashboard/components/DashboardItem/_styles.scss +34 -37
  324. package/js/widgets/Dashboard/components/DashboardItem/index.spec.tsx +74 -4
  325. package/js/widgets/Dashboard/components/DashboardItem/index.stories.tsx +18 -0
  326. package/js/widgets/Dashboard/components/DashboardItem/index.tsx +91 -26
  327. package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +4 -8
  328. package/js/widgets/Dashboard/components/DashboardLayout/_styles.scss +14 -5
  329. package/js/widgets/Dashboard/components/DashboardLayout/index.tsx +10 -3
  330. package/js/widgets/Dashboard/components/DashboardListAvatar/_styles.scss +8 -0
  331. package/js/widgets/Dashboard/components/DashboardListAvatar/index.tsx +11 -0
  332. package/js/widgets/Dashboard/components/DashboardOrderLoader/_styles.scss +5 -0
  333. package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +50 -14
  334. package/js/widgets/Dashboard/components/DashboardSidebar/_styles.scss +37 -24
  335. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +244 -0
  336. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +49 -0
  337. package/js/widgets/Dashboard/components/DashboardSidebar/components/MenuNavLink/index.spec.tsx +40 -0
  338. package/js/widgets/Dashboard/components/DashboardSidebar/components/MenuNavLink/index.tsx +28 -0
  339. package/js/widgets/Dashboard/components/DashboardSidebar/components/NavigationSelect.tsx +58 -0
  340. package/js/widgets/Dashboard/components/DashboardSidebar/index.stories.tsx +11 -1
  341. package/js/widgets/Dashboard/components/DashboardSidebar/index.tsx +18 -69
  342. package/js/widgets/Dashboard/components/DashboardSidebar/utils.ts +6 -0
  343. package/js/widgets/Dashboard/components/FilterOrganization/index.tsx +58 -0
  344. package/js/widgets/Dashboard/components/FiltersBar/index.tsx +9 -0
  345. package/js/widgets/Dashboard/components/LearnerDashboardSidebar/index.tsx +4 -2
  346. package/js/widgets/Dashboard/components/NavigateWithParams/index.spec.tsx +31 -40
  347. package/js/widgets/Dashboard/components/RouterButton/index.tsx +2 -1
  348. package/js/widgets/Dashboard/components/Signature/DummyContractPlaceholder.tsx +25 -0
  349. package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +58 -0
  350. package/js/widgets/Dashboard/components/Signature/SignatureLexPersona.tsx +72 -0
  351. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +215 -0
  352. package/js/widgets/Dashboard/components/{TeacherCourseDashboardSidebar → TeacherDashboardCourseSidebar}/index.tsx +75 -31
  353. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +23 -0
  354. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.spec.tsx +154 -0
  355. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.stories.tsx +42 -0
  356. package/js/widgets/Dashboard/components/{TeacherOrganizationDashboardSidebar → TeacherDashboardOrganizationSidebar}/index.tsx +40 -29
  357. package/js/widgets/Dashboard/components/{TeacherProfileDashboardSidebar → TeacherDashboardProfileSidebar}/components/OrganizationLinks/_styles.scss +27 -10
  358. package/js/widgets/Dashboard/components/TeacherDashboardProfileSidebar/components/OrganizationLinks/index.spec.tsx +49 -0
  359. package/js/widgets/Dashboard/components/{TeacherProfileDashboardSidebar → TeacherDashboardProfileSidebar}/components/OrganizationLinks/index.tsx +18 -7
  360. package/js/widgets/Dashboard/components/{TeacherProfileDashboardSidebar → TeacherDashboardProfileSidebar}/index.spec.tsx +13 -39
  361. package/js/widgets/Dashboard/components/{TeacherProfileDashboardSidebar → TeacherDashboardProfileSidebar}/index.stories.tsx +5 -5
  362. package/js/widgets/Dashboard/components/{TeacherProfileDashboardSidebar → TeacherDashboardProfileSidebar}/index.tsx +4 -3
  363. package/js/widgets/Dashboard/hooks/useEnroll/index.ts +8 -8
  364. package/js/widgets/Dashboard/index.spec.tsx +22 -12
  365. package/js/widgets/Dashboard/utils/learnerRouteMessages.tsx +12 -1
  366. package/js/widgets/Dashboard/utils/learnerRoutes.tsx +6 -0
  367. package/js/widgets/Dashboard/utils/teacherRouteMessages.tsx +60 -4
  368. package/js/widgets/Dashboard/utils/teacherRoutes.tsx +87 -13
  369. package/js/widgets/LtiConsumer/index.spec.tsx +44 -33
  370. package/js/widgets/LtiConsumer/index.tsx +11 -15
  371. package/js/widgets/Search/components/SearchFilterGroup/index.spec.tsx +0 -5
  372. package/js/widgets/Search/components/SearchFilterGroupModal/_styles.scss +0 -9
  373. package/js/widgets/Search/components/SearchFilterGroupModal/index.spec.tsx +0 -5
  374. package/js/widgets/Search/components/SearchFilterGroupModal/index.tsx +86 -60
  375. package/js/widgets/Search/components/SearchFilterValueParent/index.stories.tsx +51 -0
  376. package/js/widgets/Search/components/SearchFilterValueParent/index.tsx +7 -7
  377. package/js/widgets/Search/components/SearchFiltersPane/_styles.scss +2 -16
  378. package/js/widgets/Search/components/SearchFiltersPane/index.tsx +9 -6
  379. package/js/widgets/Search/hooks/useCourseSearch/index.ts +13 -7
  380. package/js/widgets/Search/index.spec.tsx +3 -2
  381. package/js/widgets/Search/utils/getResourceList/index.spec.ts +12 -5
  382. package/js/widgets/Search/utils/getResourceList/index.ts +6 -2
  383. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +74 -0
  384. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/_styles.scss +11 -14
  385. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCertificateItem/_styles.scss +1 -1
  386. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCertificateItem/index.spec.tsx +13 -2
  387. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCertificateItem/index.stories.tsx +33 -0
  388. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCertificateItem/index.tsx +5 -8
  389. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCourseRuns/CourseRunList.tsx +1 -1
  390. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +16 -8
  391. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCourseRuns/EnrolledCourseRun.tsx +5 -3
  392. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCourseRuns/_styles.scss +1 -0
  393. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCourseRuns/index.spec.tsx +100 -35
  394. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseRunItem/index.spec.tsx +7 -4
  395. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.stories.tsx +36 -0
  396. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseRunItem/index.tsx +5 -3
  397. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +40 -0
  398. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +898 -0
  399. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +83 -0
  400. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +253 -0
  401. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/CourseRunUnenrollmentButton/index.tsx +3 -2
  402. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/_styles.scss +0 -25
  403. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +73 -30
  404. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.openedx.spec.tsx +10 -5
  405. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.tsx +54 -26
  406. package/js/widgets/SyllabusCourseRunsList/components/CourseRunItemWithEnrollment/index.tsx +1 -0
  407. package/js/widgets/SyllabusCourseRunsList/components/CourseWishButton/_styles.scss +0 -3
  408. package/js/widgets/SyllabusCourseRunsList/components/CourseWishButton/hooks/useCourseWish/index.spec.tsx +12 -9
  409. package/js/widgets/SyllabusCourseRunsList/components/CourseWishButton/index.login.spec.tsx +14 -12
  410. package/js/widgets/SyllabusCourseRunsList/components/CourseWishButton/index.logout.spec.tsx +4 -6
  411. package/js/widgets/SyllabusCourseRunsList/components/CourseWishButton/index.tsx +5 -5
  412. package/js/widgets/SyllabusCourseRunsList/components/SyllabusAsideList/index.tsx +2 -2
  413. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.tsx +19 -13
  414. package/js/widgets/SyllabusCourseRunsList/components/SyllabusSimpleCourseRunsList/index.tsx +1 -0
  415. package/js/widgets/SyllabusCourseRunsList/hooks/useCourseEnrollment/index.spec.tsx +2 -1
  416. package/js/widgets/SyllabusCourseRunsList/hooks/useCourseEnrollment/index.ts +14 -19
  417. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +54 -14
  418. package/js/widgets/SyllabusCourseRunsList/index.tsx +5 -8
  419. package/js/widgets/UserLogin/components/UserMenu/DesktopUserMenu.tsx +2 -1
  420. package/js/widgets/UserLogin/components/UserMenu/MobileUserMenu.tsx +2 -1
  421. package/js/widgets/UserLogin/components/UserMenu/index.tsx +6 -5
  422. package/js/widgets/UserLogin/index.not.isJoanieEnabled.spec.tsx +120 -0
  423. package/js/widgets/UserLogin/index.spec.tsx +108 -43
  424. package/js/widgets/UserLogin/index.stories.tsx +29 -0
  425. package/js/widgets/UserLogin/index.tsx +33 -15
  426. package/js/widgets/index.tsx +3 -6
  427. package/mocks/browser.ts +1 -1
  428. package/mocks/handlers/contracts.ts +16 -0
  429. package/mocks/handlers.ts +3 -1
  430. package/package.json +82 -78
  431. package/scss/_main.scss +2 -0
  432. package/scss/colors/_palette.scss +2 -2
  433. package/scss/colors/_theme.scss +6 -16
  434. package/scss/components/_content.scss +1 -1
  435. package/scss/components/_index.scss +24 -14
  436. package/scss/generic/_type.scss +1 -1
  437. package/scss/objects/_characteristics.scss +7 -14
  438. package/scss/objects/_course_glimpses.scss +3 -7
  439. package/scss/objects/_dashboard.scss +28 -0
  440. package/scss/objects/_form.scss +14 -355
  441. package/scss/objects/_index.scss +1 -0
  442. package/scss/objects/_list.scss +8 -0
  443. package/scss/objects/_organization_glimpses.scss +2 -8
  444. package/scss/objects/_selector.scss +1 -0
  445. package/scss/trumps/_bootstrap.scss +4 -0
  446. package/scss/vendors/css/cunningham-tokens.css +89 -25
  447. package/scss/vendors/cunningham-tokens.scss +208 -128
  448. package/js/components/AddressesManagement/AddressForm.spec.tsx +0 -206
  449. package/js/components/AddressesManagement/AddressForm.tsx +0 -169
  450. package/js/components/Button/index.spec.tsx +0 -36
  451. package/js/components/Button/index.stories.tsx +0 -26
  452. package/js/components/Button/index.tsx +0 -38
  453. package/js/components/Form/CheckboxField.stories.tsx +0 -12
  454. package/js/components/Form/Field.stories.config.tsx +0 -24
  455. package/js/components/Form/Inputs.tsx +0 -295
  456. package/js/components/Form/RadioField.stories.tsx +0 -18
  457. package/js/components/Form/SelectField.stories.tsx +0 -27
  458. package/js/components/Form/TextAreaField.stories.tsx +0 -12
  459. package/js/components/Form/TextField.stories.tsx +0 -12
  460. package/js/components/Form/index.spec.tsx +0 -297
  461. package/js/hooks/useProduct.ts +0 -28
  462. package/js/utils/test/mockProductWithOrder.ts +0 -17
  463. package/js/widgets/CourseProductItem/components/PaymentButton/_styles.scss +0 -12
  464. package/js/widgets/CourseProductItem/components/PaymentButton/index.spec.tsx +0 -473
  465. package/js/widgets/CourseProductItem/components/PurchaseButton/index.spec.tsx +0 -259
  466. package/js/widgets/CourseProductItem/components/SaleTunnel/_styles.scss +0 -41
  467. package/js/widgets/CourseProductItem/components/SaleTunnelStepResume/_styles.scss +0 -130
  468. package/js/widgets/CourseProductItem/components/SaleTunnelStepResume/index.spec.tsx +0 -29
  469. package/js/widgets/CourseProductItem/components/SaleTunnelStepResume/index.tsx +0 -59
  470. package/js/widgets/CourseProductItem/components/SaleTunnelStepValidation/index.spec.tsx +0 -71
  471. package/js/widgets/CourseProductItem/contexts/CourseProductContext/index.spec.tsx +0 -35
  472. package/js/widgets/CourseProductItem/contexts/CourseProductContext/index.tsx +0 -45
  473. package/js/widgets/CourseProductItem/index.spec.tsx +0 -486
  474. package/js/widgets/CourseProductItem/index.tsx +0 -205
  475. package/js/widgets/Dashboard/components/DashboardItem/DashboardItemCourseEnrolling.spec.tsx +0 -64
  476. package/js/widgets/Dashboard/components/TeacherCourseDashboardSidebar/index.spec.tsx +0 -105
  477. package/js/widgets/Dashboard/components/TeacherOrganizationDashboardSidebar/index.stories.tsx +0 -28
  478. /package/js/components/AddressesManagement/{validationSchema.spec.ts → AddressForm/validationSchema.spec.ts} +0 -0
  479. /package/js/{widgets/CourseProductItem/components → components}/EnrollmentDate/index.tsx +0 -0
  480. /package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/RegisteredCreditCard/_styles.scss +0 -0
  481. /package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/RegisteredCreditCard/index.spec.tsx +0 -0
  482. /package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/SaleTunnelStepValidation/TargetCourseDetail.tsx +0 -0
  483. /package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/StepBreadcrumb/_styles.scss +0 -0
  484. /package/js/{widgets/CourseProductItem/hooks → hooks}/useStepManager/index.spec.ts +0 -0
  485. /package/js/{widgets/CourseProductItem/hooks → hooks}/useStepManager/index.ts +0 -0
  486. /package/js/pages/{TeacherCourseDashboardLoader → TeacherDashboardCourseLoader}/CourseRunList/CourseRunListCell/index.tsx +0 -0
  487. /package/js/pages/{TeacherCourseDashboardLoader → TeacherDashboardCourseLoader}/CourseRunList/index.tsx +0 -0
  488. /package/js/pages/{TeacherTrainingDashboard → TeacherDashboardTraining}/_styles.scss +0 -0
  489. /package/js/widgets/Dashboard/components/{TeacherCourseDashboardSidebar → TeacherDashboardCourseSidebar}/_styles.scss +0 -0
  490. /package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCourseRuns/CourseRunSection.tsx +0 -0
  491. /package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCourseRuns/index.tsx +0 -0
  492. /package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/types/payments/payplug.d.ts +0 -0
@@ -0,0 +1,1038 @@
1
+ import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
2
+ import fetchMock from 'fetch-mock';
3
+ import { PropsWithChildren, useMemo } from 'react';
4
+ import { IntlProvider } from 'react-intl';
5
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
6
+ import { faker } from '@faker-js/faker';
7
+ import queryString from 'query-string';
8
+ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
9
+ import {
10
+ AddressFactory,
11
+ CertificateOrderWithOneClickPaymentFactory,
12
+ CertificateOrderWithPaymentFactory,
13
+ CertificateProductFactory,
14
+ CredentialOrderWithOneClickPaymentFactory,
15
+ CredentialOrderWithPaymentFactory,
16
+ CredentialProductFactory,
17
+ CreditCardFactory,
18
+ OrderGroupFactory,
19
+ CourseLightFactory,
20
+ EnrollmentFactory,
21
+ } from 'utils/test/factories/joanie';
22
+ import { PAYMENT_SETTINGS } from 'settings';
23
+ import type * as Joanie from 'types/Joanie';
24
+ import {
25
+ OrderCredentialCreationPayload,
26
+ OrderState,
27
+ ProductType,
28
+ OrderGroup,
29
+ CertificateProduct,
30
+ CredentialProduct,
31
+ } from 'types/Joanie';
32
+ import { createTestQueryClient } from 'utils/test/createTestQueryClient';
33
+ import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
34
+ import { HttpStatusCode } from 'utils/errors/HttpError';
35
+ import { Maybe } from 'types/utils';
36
+ import { noop } from 'utils';
37
+ import {
38
+ SaleTunnelContextType,
39
+ SaleTunnelContext,
40
+ SaleTunnelCredentialContext,
41
+ SaleTunnelCertificateContext,
42
+ } from 'components/SaleTunnel/context';
43
+ import { ObjectHelper } from 'utils/ObjectHelper';
44
+ import useProductOrder from 'hooks/useProductOrder';
45
+ import PaymentButton from '.';
46
+
47
+ jest.mock('utils/context', () => ({
48
+ __esModule: true,
49
+ default: mockRichieContextFactory({
50
+ authentication: {
51
+ backend: 'fonzie',
52
+ endpoint: 'https://authentication.test',
53
+ },
54
+ joanie_backend: {
55
+ endpoint: 'https://joanie.test',
56
+ },
57
+ site_urls: {
58
+ terms_and_conditions: '/en/about/terms-and-conditions/',
59
+ },
60
+ }).one(),
61
+ }));
62
+
63
+ jest.mock('./components/PaymentInterfaces');
64
+
65
+ type WrapperProps = PropsWithChildren<{
66
+ client?: QueryClient;
67
+ product: CredentialProduct | CertificateProduct;
68
+ orderGroup?: OrderGroup;
69
+ }>;
70
+
71
+ describe.each([
72
+ {
73
+ productType: ProductType.CREDENTIAL,
74
+ ProductFactory: CredentialProductFactory,
75
+ OrderWithOneClickPaymentFactory: CredentialOrderWithOneClickPaymentFactory,
76
+ OrderWithPaymentFactory: CredentialOrderWithPaymentFactory,
77
+ },
78
+ {
79
+ productType: ProductType.CERTIFICATE,
80
+ ProductFactory: CertificateProductFactory,
81
+ OrderWithOneClickPaymentFactory: CertificateOrderWithOneClickPaymentFactory,
82
+ OrderWithPaymentFactory: CertificateOrderWithPaymentFactory,
83
+ },
84
+ ])(
85
+ 'PaymentButton for $productType product',
86
+ ({ productType, ProductFactory, OrderWithOneClickPaymentFactory, OrderWithPaymentFactory }) => {
87
+ let nbApiCalls: number;
88
+ const TEST_COURSE_CODE = '00000';
89
+ const TEST_ENROLLMENT_ID = faker.string.uuid();
90
+
91
+ const formatPrice = (price: number, currency: string) =>
92
+ new Intl.NumberFormat('en', {
93
+ currency,
94
+ style: 'currency',
95
+ }).format(price);
96
+
97
+ const SaleTunnelWrapper = ({
98
+ product,
99
+ orderGroup,
100
+ children,
101
+ }: Exclude<WrapperProps, 'client'>) => {
102
+ const { item: order } = useProductOrder({
103
+ courseCode: product.type === ProductType.CREDENTIAL ? TEST_COURSE_CODE : undefined,
104
+ enrollmentId: product.type === ProductType.CERTIFICATE ? TEST_ENROLLMENT_ID : undefined,
105
+ productId: product.id,
106
+ });
107
+
108
+ const context: SaleTunnelContextType = useMemo(() => {
109
+ if (product.type === ProductType.CREDENTIAL) {
110
+ return {
111
+ product,
112
+ order,
113
+ key: `${TEST_COURSE_CODE}+${product.id}`,
114
+ course: CourseLightFactory({ code: TEST_COURSE_CODE }).one(),
115
+ orderGroup,
116
+ } as SaleTunnelCredentialContext;
117
+ } else {
118
+ return {
119
+ product,
120
+ order,
121
+ key: `${TEST_ENROLLMENT_ID}+${product.id}`,
122
+ enrollment: EnrollmentFactory({ id: TEST_ENROLLMENT_ID }).one(),
123
+ orderGroup,
124
+ } as SaleTunnelCertificateContext;
125
+ }
126
+ }, [product, order, orderGroup]);
127
+
128
+ return <SaleTunnelContext.Provider value={context}>{children}</SaleTunnelContext.Provider>;
129
+ };
130
+
131
+ const Wrapper = ({
132
+ client = createTestQueryClient({ user: true }),
133
+ ...props
134
+ }: WrapperProps) => {
135
+ return (
136
+ <IntlProvider locale="en">
137
+ <QueryClientProvider client={client}>
138
+ <JoanieSessionProvider>
139
+ <SaleTunnelWrapper {...props} />
140
+ </JoanieSessionProvider>
141
+ </QueryClientProvider>
142
+ </IntlProvider>
143
+ );
144
+ };
145
+
146
+ beforeEach(() => {
147
+ jest.useFakeTimers();
148
+ jest.clearAllTimers();
149
+ jest.resetAllMocks();
150
+
151
+ fetchMock.restore();
152
+ sessionStorage.clear();
153
+
154
+ // Joanie providers calls
155
+ fetchMock.get('https://joanie.test/api/v1.0/orders/', []);
156
+ fetchMock.get('https://joanie.test/api/v1.0/credit-cards/', []);
157
+ fetchMock.get('https://joanie.test/api/v1.0/addresses/', []);
158
+ nbApiCalls = 3;
159
+ });
160
+
161
+ afterEach(() => {
162
+ jest.runOnlyPendingTimers();
163
+ jest.useRealTimers();
164
+ cleanup();
165
+ });
166
+
167
+ it('should render a payment button', async () => {
168
+ const product: Joanie.Product = ProductFactory().one();
169
+ const fetchOrderQueryParams =
170
+ product.type === ProductType.CREDENTIAL
171
+ ? {
172
+ course_code: TEST_COURSE_CODE,
173
+ product_id: product.id,
174
+ state: ['pending', 'validated', 'submitted'],
175
+ }
176
+ : {
177
+ enrollment_id: TEST_ENROLLMENT_ID,
178
+ product_id: product.id,
179
+ state: ['pending', 'validated', 'submitted'],
180
+ };
181
+
182
+ fetchMock.get(
183
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
184
+ [],
185
+ );
186
+
187
+ render(
188
+ <Wrapper product={product}>
189
+ <PaymentButton onSuccess={jest.fn()} />
190
+ </Wrapper>,
191
+ );
192
+
193
+ const $terms = screen.getByLabelText(
194
+ 'By checking this box, you accept the General Terms of Sale',
195
+ );
196
+ await act(async () => {
197
+ fireEvent.click($terms);
198
+ });
199
+
200
+ const $button = screen.getByRole('button', {
201
+ name: `Pay ${formatPrice(product.price, product.price_currency)}`,
202
+ }) as HTMLButtonElement;
203
+
204
+ // a billing address is missing, but the button stays enabled
205
+ // this allows the user to get feedback on what's missing to make the payment by clicking on the button
206
+ expect($button.disabled).toBe(false);
207
+
208
+ // clicking the button should show an error and focus it so that screen reader users know what's happening
209
+ await act(async () => {
210
+ fireEvent.click($button);
211
+ });
212
+ const $error = screen.getByText('You must have a billing address.');
213
+ expect(document.activeElement).toBe($error);
214
+ });
215
+
216
+ it('should render a payment button with a specific label when a credit card is provided', () => {
217
+ /*
218
+ If a credit card is provided, it seems that the payment should be a one click,
219
+ so the payment button label should mention this information.
220
+ */
221
+ const product: Joanie.Product = ProductFactory().one();
222
+ const creditCard: Joanie.CreditCard = CreditCardFactory().one();
223
+
224
+ const fetchOrderQueryParams =
225
+ product.type === ProductType.CREDENTIAL
226
+ ? {
227
+ course_code: TEST_COURSE_CODE,
228
+ product_id: product.id,
229
+ state: ['pending', 'validated', 'submitted'],
230
+ }
231
+ : {
232
+ enrollment_id: TEST_ENROLLMENT_ID,
233
+ product_id: product.id,
234
+ state: ['pending', 'validated', 'submitted'],
235
+ };
236
+
237
+ fetchMock.get(
238
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
239
+ [],
240
+ );
241
+
242
+ const { rerender } = render(
243
+ <Wrapper product={product}>
244
+ <PaymentButton creditCard={creditCard.id} onSuccess={jest.fn()} />
245
+ </Wrapper>,
246
+ );
247
+
248
+ const $button = screen.getByRole('button', {
249
+ name: `Pay in one click ${formatPrice(product.price, product.price_currency)}`,
250
+ }) as HTMLButtonElement;
251
+
252
+ // a billing address is missing, but the button stays enabled
253
+ // this allows the user to get feedback on what's missing to make the payment by clicking on the button
254
+ expect($button.disabled).toBe(false);
255
+
256
+ const billingAddress: Joanie.Address = AddressFactory().one();
257
+
258
+ rerender(
259
+ <Wrapper product={product}>
260
+ <PaymentButton
261
+ billingAddress={billingAddress}
262
+ creditCard={creditCard.id}
263
+ onSuccess={jest.fn()}
264
+ />
265
+ </Wrapper>,
266
+ );
267
+
268
+ // the button should be active
269
+ expect($button.disabled).toBe(false);
270
+ });
271
+
272
+ it('should render an enabled payment button if a billing address is provided', () => {
273
+ const product: Joanie.Product = ProductFactory().one();
274
+ const billingAddress: Joanie.Address = AddressFactory().one();
275
+
276
+ const fetchOrderQueryParams =
277
+ product.type === ProductType.CREDENTIAL
278
+ ? {
279
+ course_code: TEST_COURSE_CODE,
280
+ product_id: product.id,
281
+ state: ['pending', 'validated', 'submitted'],
282
+ }
283
+ : {
284
+ enrollment_id: TEST_ENROLLMENT_ID,
285
+ product_id: product.id,
286
+ state: ['pending', 'validated', 'submitted'],
287
+ };
288
+
289
+ fetchMock.get(
290
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
291
+ [],
292
+ );
293
+
294
+ render(
295
+ <Wrapper product={product}>
296
+ <PaymentButton billingAddress={billingAddress} onSuccess={jest.fn()} />
297
+ </Wrapper>,
298
+ );
299
+
300
+ const $button = screen.getByRole('button', {
301
+ name: `Pay ${formatPrice(product.price, product.price_currency)}`,
302
+ }) as HTMLButtonElement;
303
+
304
+ // the button should be active
305
+ expect($button.disabled).toBe(false);
306
+ });
307
+
308
+ it('should create an order then display the payment interface when user clicks on payment button', async () => {
309
+ const product: Joanie.Product = ProductFactory().one();
310
+ const billingAddress: Joanie.Address = AddressFactory().one();
311
+ const handleSuccess = jest.fn();
312
+ const { payment_info: paymentInfo, ...order } = OrderWithPaymentFactory().one();
313
+
314
+ const fetchOrderQueryParams =
315
+ product.type === ProductType.CREDENTIAL
316
+ ? {
317
+ course_code: TEST_COURSE_CODE,
318
+ product_id: product.id,
319
+ state: ['pending', 'validated', 'submitted'],
320
+ }
321
+ : {
322
+ enrollment_id: TEST_ENROLLMENT_ID,
323
+ product_id: product.id,
324
+ state: ['pending', 'validated', 'submitted'],
325
+ };
326
+
327
+ fetchMock
328
+ .get(
329
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
330
+ [],
331
+ )
332
+ .post('https://joanie.test/api/v1.0/orders/', order)
333
+ .patch(`https://joanie.test/api/v1.0/orders/${order.id}/submit/`, {
334
+ paymentInfo,
335
+ })
336
+ .get(`https://joanie.test/api/v1.0/orders/${order.id}/`, {
337
+ ...order,
338
+ });
339
+
340
+ render(
341
+ <Wrapper client={createTestQueryClient({ user: true })} product={product}>
342
+ <PaymentButton billingAddress={billingAddress} onSuccess={handleSuccess} />
343
+ </Wrapper>,
344
+ );
345
+ nbApiCalls += 1; // useProductOrder call.
346
+ expect(fetchMock.calls()).toHaveLength(nbApiCalls);
347
+
348
+ const $terms = screen.getByLabelText(
349
+ 'By checking this box, you accept the General Terms of Sale',
350
+ );
351
+ await act(async () => {
352
+ fireEvent.click($terms);
353
+ });
354
+
355
+ const $button = screen.getByRole('button', {
356
+ name: `Pay ${formatPrice(product.price, product.price_currency)}`,
357
+ }) as HTMLButtonElement;
358
+
359
+ // - Payment button should not be disabled.
360
+ expect($button.disabled).toBe(false);
361
+
362
+ // - User clicks on pay button
363
+ await act(async () => {
364
+ fireEvent.click($button);
365
+ });
366
+
367
+ // - Route to create order should have been called
368
+ nbApiCalls += 1; // order post create (invalidate queries)
369
+ nbApiCalls += 1; // useProductOrder call (invalidate from create)
370
+ nbApiCalls += 1; // order submit
371
+
372
+ await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
373
+ expect(fetchMock.lastUrl()).toBe(`https://joanie.test/api/v1.0/orders/${order.id}/submit/`);
374
+
375
+ // - Spinner should be displayed
376
+ screen.getByText('Payment in progress');
377
+
378
+ // - Payment interface should be displayed
379
+ screen.getByText('Payment interface component');
380
+
381
+ // - Simulate the payment has succeeded
382
+ await act(async () => {
383
+ fireEvent.click(screen.getByTestId('payment-success'));
384
+ });
385
+
386
+ // - Once payment succeeded, order should be refetch
387
+ nbApiCalls += 1; // fetch validated order
388
+ expect(fetchMock.calls()).toHaveLength(nbApiCalls);
389
+ expect(fetchMock.lastUrl()).toBe(`https://joanie.test/api/v1.0/orders/${order.id}/`);
390
+
391
+ // - Order should be polled until its state is validated
392
+ fetchMock.get(
393
+ `https://joanie.test/api/v1.0/orders/${order.id}/`,
394
+ {
395
+ ...order,
396
+ state: OrderState.VALIDATED,
397
+ },
398
+ {
399
+ overwriteRoutes: true,
400
+ },
401
+ );
402
+
403
+ // - Advance timer to one tick
404
+ await act(async () => {
405
+ jest.runOnlyPendingTimers();
406
+ });
407
+
408
+ // - Order should have been refetched
409
+ expect(fetchMock.calls()).toHaveLength(nbApiCalls);
410
+ expect(fetchMock.lastUrl()).toBe(`https://joanie.test/api/v1.0/orders/${order.id}/`);
411
+
412
+ // - As order state is validated, the onSuccess callback should be triggered.
413
+ expect(handleSuccess).toHaveBeenCalledTimes(1);
414
+
415
+ // - And poller should be stopped
416
+ await act(async () => {
417
+ jest.runOnlyPendingTimers();
418
+ });
419
+ expect(fetchMock.calls()).toHaveLength(nbApiCalls);
420
+ });
421
+
422
+ it('should create an order only the first time the payment interface is shown, and not after aborting', async () => {
423
+ const product: Joanie.Product = ProductFactory().one();
424
+ const billingAddress: Joanie.Address = AddressFactory().one();
425
+ const handleSuccess = jest.fn();
426
+ const { payment_info: paymentInfo, ...order } = OrderWithPaymentFactory().one();
427
+
428
+ const fetchOrderQueryParams =
429
+ product.type === ProductType.CREDENTIAL
430
+ ? {
431
+ course_code: TEST_COURSE_CODE,
432
+ product_id: product.id,
433
+ state: ['pending', 'validated', 'submitted'],
434
+ }
435
+ : {
436
+ enrollment_id: TEST_ENROLLMENT_ID,
437
+ product_id: product.id,
438
+ state: ['pending', 'validated', 'submitted'],
439
+ };
440
+
441
+ fetchMock
442
+ .get(
443
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
444
+ [],
445
+ )
446
+ .post('https://joanie.test/api/v1.0/orders/', order)
447
+ .patch(`https://joanie.test/api/v1.0/orders/${order.id}/submit/`, {
448
+ paymentInfo,
449
+ })
450
+ .get(`https://joanie.test/api/v1.0/orders/${order.id}/`, {
451
+ ...order,
452
+ })
453
+ .post(`https://joanie.test/api/v1.0/orders/${order.id}/abort/`, HttpStatusCode.OK);
454
+
455
+ render(
456
+ <Wrapper client={createTestQueryClient({ user: true })} product={product}>
457
+ <PaymentButton billingAddress={billingAddress} onSuccess={handleSuccess} />
458
+ </Wrapper>,
459
+ );
460
+ nbApiCalls += 1; // useProductOrder call.
461
+ expect(fetchMock.calls()).toHaveLength(nbApiCalls);
462
+
463
+ const $terms = screen.getByLabelText(
464
+ 'By checking this box, you accept the General Terms of Sale',
465
+ );
466
+ await act(async () => {
467
+ fireEvent.click($terms);
468
+ });
469
+
470
+ const $button = screen.getByRole('button', {
471
+ name: `Pay ${formatPrice(product.price, product.price_currency)}`,
472
+ }) as HTMLButtonElement;
473
+
474
+ // - Payment button should not be disabled.
475
+ expect($button.disabled).toBe(false);
476
+
477
+ // - User clicks on pay button
478
+ await act(async () => {
479
+ fireEvent.click($button);
480
+ });
481
+
482
+ // - Route to create order should have been called
483
+ nbApiCalls += 1; // order post create (invalidate queries)
484
+ nbApiCalls += 1; // useProductOrder call (invalidate from create)
485
+ nbApiCalls += 1; // order submit
486
+
487
+ await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
488
+ expect(fetchMock.lastUrl()).toBe(`https://joanie.test/api/v1.0/orders/${order.id}/submit/`);
489
+
490
+ // - Spinner should be displayed
491
+ screen.getByText('Payment in progress');
492
+
493
+ // - Payment interface should be displayed
494
+ screen.getByText('Payment interface component');
495
+
496
+ // - Simulate the payment aborting.
497
+ fetchMock.get(
498
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
499
+ [
500
+ {
501
+ ...order,
502
+ state: OrderState.PENDING,
503
+ },
504
+ ],
505
+ { overwriteRoutes: true },
506
+ );
507
+ await act(async () => {
508
+ fireEvent.click(screen.getByTestId('payment-abort'));
509
+ });
510
+
511
+ nbApiCalls += 1; // abort order.
512
+ nbApiCalls += 1; // useProductOrder call (invalidate from create)
513
+ await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
514
+ expect(fetchMock.calls()[fetchMock.calls().length - 2][0]).toBe(
515
+ `https://joanie.test/api/v1.0/orders/${order.id}/abort/`,
516
+ );
517
+ expect(fetchMock.calls()[fetchMock.calls().length - 1][0]).toBe(
518
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
519
+ );
520
+
521
+ screen.getByText('You have aborted the payment.');
522
+
523
+ // screen.logTestingPlaygroundURL();
524
+
525
+ // - User clicks on pay button again.
526
+ await act(async () => {
527
+ fireEvent.click($button);
528
+ });
529
+
530
+ // - Spinner should be displayed
531
+ screen.getByText('Payment in progress');
532
+
533
+ // - Payment interface should be displayed
534
+ screen.getByText('Payment interface component');
535
+
536
+ // - Now we make sure the order is not created again and just submitted.
537
+ nbApiCalls += 1; // submits order.
538
+ await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
539
+ expect(fetchMock.lastUrl()).toBe(`https://joanie.test/api/v1.0/orders/${order.id}/submit/`);
540
+ });
541
+
542
+ it('should render a payment button and not call the order creation route', async () => {
543
+ const product: Joanie.Product = ProductFactory().one();
544
+ const billingAddress: Joanie.Address = AddressFactory().one();
545
+ const creditCard: Joanie.CreditCard = CreditCardFactory().one();
546
+ const { payment_info: paymentInfo, ...order } = OrderWithOneClickPaymentFactory().one();
547
+ const handleSuccess = jest.fn();
548
+ const initialOrder = {
549
+ ...order,
550
+ state: OrderState.PENDING,
551
+ };
552
+ const orderSubmitted = {
553
+ ...order,
554
+ state: OrderState.SUBMITTED,
555
+ };
556
+
557
+ const fetchOrderQueryParams =
558
+ product.type === ProductType.CREDENTIAL
559
+ ? {
560
+ course_code: TEST_COURSE_CODE,
561
+ product_id: product.id,
562
+ state: ['pending', 'validated', 'submitted'],
563
+ }
564
+ : {
565
+ enrollment_id: TEST_ENROLLMENT_ID,
566
+ product_id: product.id,
567
+ state: ['pending', 'validated', 'submitted'],
568
+ };
569
+
570
+ fetchMock
571
+ .get(
572
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
573
+ [initialOrder],
574
+ )
575
+ .post('https://joanie.test/api/v1.0/orders/', order)
576
+ .patch(`https://joanie.test/api/v1.0/orders/${order.id}/submit/`, {
577
+ payment_info: paymentInfo,
578
+ })
579
+ .get(`https://joanie.test/api/v1.0/orders/${order.id}/`, orderSubmitted);
580
+
581
+ render(
582
+ <Wrapper client={createTestQueryClient({ user: true })} product={product}>
583
+ <PaymentButton
584
+ billingAddress={billingAddress}
585
+ creditCard={creditCard.id}
586
+ onSuccess={handleSuccess}
587
+ />
588
+ </Wrapper>,
589
+ );
590
+ await waitFor(() => {
591
+ expect(screen.getByTestId('payment-button-order-loaded')).toBeInTheDocument();
592
+ });
593
+
594
+ const $terms = screen.getByLabelText(
595
+ 'By checking this box, you accept the General Terms of Sale',
596
+ );
597
+ await act(async () => {
598
+ fireEvent.click($terms);
599
+ });
600
+
601
+ nbApiCalls += 1; // useProductOrder get order with filters
602
+ expect(fetchMock.calls()).toHaveLength(nbApiCalls);
603
+ const $button = screen.getByRole('button', {
604
+ name: `Pay in one click ${formatPrice(product.price, product.price_currency)}`,
605
+ }) as HTMLButtonElement;
606
+
607
+ // - Payment button should not be disabled.
608
+ expect($button.disabled).toBe(false);
609
+
610
+ // - User clicks on pay button
611
+ fetchMock.get(
612
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
613
+ [orderSubmitted],
614
+ { overwriteRoutes: true },
615
+ );
616
+ await act(async () => {
617
+ fireEvent.click($button);
618
+ });
619
+
620
+ // - In real world condition the success callback is immediately called for one click payments.
621
+ // - but here we need to click manually.
622
+ const $success = screen.getByTestId('payment-success');
623
+ await act(async () => {
624
+ fireEvent.click($success);
625
+ });
626
+
627
+ // - Route to submit an existing order
628
+ // - Furthermore, as payment succeeded immediately, order should have been refetched
629
+ nbApiCalls += 1; // order submit
630
+ nbApiCalls += 1; // order get on id
631
+ expect(fetchMock.calls()).toHaveLength(nbApiCalls);
632
+
633
+ const submitCall = fetchMock
634
+ .calls()
635
+ .find((call) => call[0] === `https://joanie.test/api/v1.0/orders/${order.id}/submit/`);
636
+ expect(submitCall).not.toBeUndefined();
637
+ expect(JSON.parse(submitCall![1]!.body as string)).toEqual({
638
+ billing_address: ObjectHelper.omit(billingAddress, 'id', 'is_main'),
639
+ credit_card_id: creditCard.id,
640
+ });
641
+ expect(fetchMock.lastUrl()).toBe(`https://joanie.test/api/v1.0/orders/${order.id}/`);
642
+
643
+ // - Spinner should be displayed
644
+ screen.getByText('Payment in progress');
645
+
646
+ // - Order should be polled until its state is validated
647
+ fetchMock.get(
648
+ `https://joanie.test/api/v1.0/orders/${order.id}/`,
649
+ {
650
+ ...order,
651
+ state: OrderState.VALIDATED,
652
+ },
653
+ {
654
+ overwriteRoutes: true,
655
+ },
656
+ );
657
+
658
+ // - Advance timer to one tick
659
+ await act(async () => {
660
+ jest.runOnlyPendingTimers();
661
+ });
662
+
663
+ // - Order should have been refetched
664
+ nbApiCalls += 1; // order get on id
665
+ expect(fetchMock.calls()).toHaveLength(nbApiCalls);
666
+ expect(fetchMock.lastUrl()).toBe(`https://joanie.test/api/v1.0/orders/${order.id}/`);
667
+
668
+ // - As order state is validated, the onSuccess callback should be triggered.
669
+ expect(handleSuccess).toHaveBeenCalledTimes(1);
670
+ // - And poller should be stopped
671
+ await act(async () => {
672
+ jest.runOnlyPendingTimers();
673
+ });
674
+ expect(fetchMock.calls()).toHaveLength(nbApiCalls);
675
+ });
676
+
677
+ it('should abort the order if payment does not succeed after a given delay', async () => {
678
+ const product: Joanie.Product = ProductFactory().one();
679
+ const billingAddress: Joanie.Address = AddressFactory().one();
680
+ const creditCard: Joanie.CreditCard = CreditCardFactory().one();
681
+ const { payment_info: paymentInfo, ...order } = OrderWithOneClickPaymentFactory().one();
682
+ const orderSubmitted = {
683
+ ...order,
684
+ state: OrderState.SUBMITTED,
685
+ };
686
+ const handleSuccess = jest.fn();
687
+
688
+ const fetchOrderQueryParams =
689
+ product.type === ProductType.CREDENTIAL
690
+ ? {
691
+ course_code: TEST_COURSE_CODE,
692
+ product_id: product.id,
693
+ state: ['pending', 'validated', 'submitted'],
694
+ }
695
+ : {
696
+ enrollment_id: TEST_ENROLLMENT_ID,
697
+ product_id: product.id,
698
+ state: ['pending', 'validated', 'submitted'],
699
+ };
700
+
701
+ fetchMock
702
+ .get(
703
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
704
+ [orderSubmitted],
705
+ )
706
+ .post('https://joanie.test/api/v1.0/orders/', order)
707
+ .patch(`https://joanie.test/api/v1.0/orders/${order.id}/submit/`, {
708
+ payment_info: paymentInfo,
709
+ })
710
+ .get(`https://joanie.test/api/v1.0/orders/${order.id}/`, orderSubmitted)
711
+ .post(`https://joanie.test/api/v1.0/orders/${order.id}/abort/`, HttpStatusCode.OK);
712
+
713
+ render(
714
+ <Wrapper client={createTestQueryClient({ user: true })} product={product}>
715
+ <PaymentButton
716
+ billingAddress={billingAddress}
717
+ creditCard={creditCard.id}
718
+ onSuccess={handleSuccess}
719
+ />
720
+ </Wrapper>,
721
+ );
722
+ await waitFor(() => {
723
+ expect(screen.getByTestId('payment-button-order-loaded')).toBeInTheDocument();
724
+ });
725
+ nbApiCalls += 1; // fetcher order for userProductOrder
726
+ const apiCalls = fetchMock.calls().map((call) => call[0]);
727
+ expect(apiCalls).toContain(
728
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
729
+ );
730
+
731
+ const $terms = screen.getByLabelText(
732
+ 'By checking this box, you accept the General Terms of Sale',
733
+ );
734
+ await act(async () => {
735
+ fireEvent.click($terms);
736
+ });
737
+
738
+ const $button = screen.getByRole('button', {
739
+ name: `Pay in one click ${formatPrice(product.price, product.price_currency)}`,
740
+ }) as HTMLButtonElement;
741
+
742
+ // - Payment button should not be disabled.
743
+ expect($button.disabled).toBe(false);
744
+
745
+ // - User clicks on pay button
746
+ await act(async () => {
747
+ fireEvent.click($button);
748
+ });
749
+
750
+ // - In real world condition the success callback is immediately called for one click payments.
751
+ // - but here we need to click manually.
752
+ const $success = screen.getByTestId('payment-success');
753
+ await act(async () => {
754
+ fireEvent.click($success);
755
+ });
756
+
757
+ // - Route to create order should have been called
758
+ // - Furthermore, as payment succeeded immediately, order should have been refetched
759
+ const onClickApiCalls = fetchMock.calls().splice(nbApiCalls);
760
+ nbApiCalls += 1; // order submit
761
+ nbApiCalls += 1; // fetch validated order
762
+ expect(fetchMock.calls()).toHaveLength(nbApiCalls);
763
+
764
+ expect(onClickApiCalls[0][0]).toBe(`https://joanie.test/api/v1.0/orders/${order.id}/submit/`);
765
+ expect(JSON.parse(onClickApiCalls[0][1]!.body as string)).toEqual({
766
+ billing_address: ObjectHelper.omit(billingAddress, 'id', 'is_main'),
767
+ credit_card_id: creditCard.id,
768
+ });
769
+
770
+ expect(onClickApiCalls[1][0]).toBe(`https://joanie.test/api/v1.0/orders/${order.id}/`);
771
+
772
+ // - Spinner should be displayed
773
+ screen.getByText('Payment in progress');
774
+
775
+ fetchMock.resetHistory();
776
+ // - Wait until order has been polled 29 times.
777
+ await jest.advanceTimersToNextTimerAsync(PAYMENT_SETTINGS.pollLimit);
778
+ expect(fetchMock.calls()).toHaveLength(PAYMENT_SETTINGS.pollLimit - 1);
779
+
780
+ // - This round should be the last after which the order should be aborted
781
+ await act(async () => {
782
+ jest.runOnlyPendingTimers();
783
+ });
784
+
785
+ await waitFor(
786
+ async () => {
787
+ // +1 is for useProductOrder call invalidation.
788
+ expect(fetchMock.calls()).toHaveLength(PAYMENT_SETTINGS.pollLimit + 1);
789
+ expect(fetchMock.calls()[fetchMock.calls().length - 2][0]).toBe(
790
+ `https://joanie.test/api/v1.0/orders/${order.id}/abort/`,
791
+ );
792
+ },
793
+ {
794
+ timeout: 1100,
795
+ },
796
+ );
797
+
798
+ expect(
799
+ JSON.parse(fetchMock.calls()[fetchMock.calls().length - 2][1]!.body!.toString()),
800
+ ).toEqual({
801
+ payment_id: paymentInfo.payment_id,
802
+ });
803
+
804
+ // - An error message should be displayed and focused (for screen reader users)
805
+ const $error = screen.getByText('An error occurred during payment. Please retry later.');
806
+ expect(document.activeElement).toBe($error);
807
+ }, 10000);
808
+
809
+ it('should render an error message when payment failed', async () => {
810
+ const product: Joanie.Product = ProductFactory().one();
811
+ const billingAddress: Joanie.Address = AddressFactory().one();
812
+ const { payment_info: paymentInfo, ...order } = OrderWithPaymentFactory().one();
813
+ const handleSuccess = jest.fn();
814
+ const fetchOrderQueryParams =
815
+ product.type === ProductType.CREDENTIAL
816
+ ? {
817
+ course_code: TEST_COURSE_CODE,
818
+ product_id: product.id,
819
+ state: ['pending', 'validated', 'submitted'],
820
+ }
821
+ : {
822
+ enrollment_id: TEST_ENROLLMENT_ID,
823
+ product_id: product.id,
824
+ state: ['pending', 'validated', 'submitted'],
825
+ };
826
+
827
+ fetchMock
828
+ .get(
829
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
830
+ [],
831
+ )
832
+ .post('https://joanie.test/api/v1.0/orders/', order)
833
+ .patch(`https://joanie.test/api/v1.0/orders/${order.id}/submit/`, {
834
+ payment_info: paymentInfo,
835
+ })
836
+ .get(`https://joanie.test/api/v1.0/orders/${order.id}/`, {
837
+ ...order,
838
+ state: OrderState.SUBMITTED,
839
+ });
840
+
841
+ render(
842
+ <Wrapper client={createTestQueryClient({ user: true })} product={product}>
843
+ <PaymentButton billingAddress={billingAddress} onSuccess={handleSuccess} />
844
+ </Wrapper>,
845
+ );
846
+ nbApiCalls += 1; // useProductOrder get order with filters
847
+ expect(fetchMock.calls()).toHaveLength(nbApiCalls);
848
+
849
+ const $terms = screen.getByLabelText(
850
+ 'By checking this box, you accept the General Terms of Sale',
851
+ );
852
+ await act(async () => {
853
+ fireEvent.click($terms);
854
+ });
855
+
856
+ const $button = screen.getByRole('button', {
857
+ name: `Pay ${formatPrice(product.price, product.price_currency)}`,
858
+ }) as HTMLButtonElement;
859
+
860
+ // - As all information are provided, payment button should not be disabled.
861
+ expect($button.disabled).toBe(false);
862
+
863
+ // - User clicks on pay button
864
+ await act(async () => {
865
+ fireEvent.click($button);
866
+ });
867
+
868
+ // - Route to create order should have been called
869
+ nbApiCalls += 1; // order post create (invalidate queries)
870
+ nbApiCalls += 1; // refetch useProductOrder
871
+ nbApiCalls += 1; // order submit
872
+ expect(fetchMock.calls()).toHaveLength(nbApiCalls);
873
+
874
+ expect(fetchMock.lastUrl()).toBe(`https://joanie.test/api/v1.0/orders/${order.id}/submit/`);
875
+ expect(JSON.parse(fetchMock.lastOptions()!.body!.toString())).toEqual({
876
+ billing_address: ObjectHelper.omit(billingAddress, 'id', 'is_main'),
877
+ });
878
+
879
+ // - Spinner should be displayed and payment button should be disabled
880
+ screen.getByText('Payment in progress');
881
+ expect($button.disabled).toBe(true);
882
+
883
+ // - Payment interface should be displayed
884
+ await screen.findByText('Payment interface component');
885
+
886
+ // - Simulate the payment has failed
887
+ await act(async () => {
888
+ fireEvent.click(screen.getByTestId('payment-failure'));
889
+ });
890
+
891
+ // - An error message should be displayed
892
+ const $error = screen.getByText('An error occurred during payment. Please retry later.');
893
+ expect(document.activeElement).toBe($error);
894
+ // - Payment interface should have been closed
895
+ expect(screen.queryByText('Payment interface component')).toBeNull();
896
+ // - Payment button should have been restore to its idle state
897
+ expect($button.disabled).toBe(false);
898
+ screen.getByRole('button', {
899
+ name: `Pay ${formatPrice(product.price, product.price_currency)}`,
900
+ });
901
+ });
902
+
903
+ it('should show an error if user does not accept the terms', async () => {
904
+ const product: Joanie.Product = ProductFactory().one();
905
+ const billingAddress: Joanie.Address = AddressFactory().one();
906
+
907
+ const fetchOrderQueryParams =
908
+ product.type === ProductType.CREDENTIAL
909
+ ? {
910
+ course_code: TEST_COURSE_CODE,
911
+ product_id: product.id,
912
+ state: ['pending', 'validated', 'submitted'],
913
+ }
914
+ : {
915
+ enrollment_id: TEST_ENROLLMENT_ID,
916
+ product_id: product.id,
917
+ state: ['pending', 'validated', 'submitted'],
918
+ };
919
+
920
+ fetchMock.get(
921
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
922
+ [],
923
+ );
924
+
925
+ render(
926
+ <Wrapper client={createTestQueryClient({ user: true })} product={product}>
927
+ <PaymentButton billingAddress={billingAddress} onSuccess={noop} />
928
+ </Wrapper>,
929
+ );
930
+
931
+ const $button = screen.getByRole('button', {
932
+ name: `Pay ${formatPrice(product.price, product.price_currency)}`,
933
+ }) as HTMLButtonElement;
934
+
935
+ // - As all information are provided, payment button should not be disabled.
936
+ expect($button.disabled).toBe(false);
937
+
938
+ expect(screen.queryByText('You must accept the terms')).not.toBeInTheDocument();
939
+
940
+ // - User clicks on pay button
941
+ await act(async () => {
942
+ fireEvent.click($button);
943
+ });
944
+
945
+ expect(screen.getByText('You must accept the terms.')).toBeInTheDocument();
946
+ });
947
+
948
+ it('should show a link to the platform terms and conditions', async () => {
949
+ const product: Joanie.Product = ProductFactory().one();
950
+ const billingAddress: Joanie.Address = AddressFactory().one();
951
+
952
+ const fetchOrderQueryParams =
953
+ product.type === ProductType.CREDENTIAL
954
+ ? {
955
+ course_code: TEST_COURSE_CODE,
956
+ product_id: product.id,
957
+ state: ['pending', 'validated', 'submitted'],
958
+ }
959
+ : {
960
+ enrollment_id: TEST_ENROLLMENT_ID,
961
+ product_id: product.id,
962
+ state: ['pending', 'validated', 'submitted'],
963
+ };
964
+
965
+ fetchMock.get(
966
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
967
+ [],
968
+ );
969
+
970
+ render(
971
+ <Wrapper client={createTestQueryClient({ user: true })} product={product}>
972
+ <PaymentButton billingAddress={billingAddress} onSuccess={noop} />
973
+ </Wrapper>,
974
+ );
975
+
976
+ const $terms = screen.getByRole('link', { name: 'General Terms of Sale' });
977
+ expect($terms).toHaveAttribute('href', '/en/about/terms-and-conditions/');
978
+ });
979
+
980
+ if (productType === ProductType.CREDENTIAL) {
981
+ it('should create an order with an order group', async () => {
982
+ const product: Joanie.Product = ProductFactory().one();
983
+ const orderGroup = OrderGroupFactory().one();
984
+ const billingAddress: Joanie.Address = AddressFactory().one();
985
+ const handleSuccess = jest.fn();
986
+
987
+ let createOrderPayload: Maybe<OrderCredentialCreationPayload>;
988
+ const { payment_info: paymentInfo, ...order } = OrderWithPaymentFactory().one();
989
+ fetchMock
990
+ .get(
991
+ `https://joanie.test/api/v1.0/orders/?course_code=00000&product_id=${product.id}&state=pending&state=validated&state=submitted`,
992
+ [],
993
+ )
994
+ .post('https://joanie.test/api/v1.0/orders/', (url, { body }) => {
995
+ createOrderPayload = JSON.parse(body as any);
996
+ return order;
997
+ })
998
+ .patch(`https://joanie.test/api/v1.0/orders/${order.id}/submit/`, {
999
+ paymentInfo,
1000
+ })
1001
+ .get(`https://joanie.test/api/v1.0/orders/${order.id}/`, {
1002
+ ...order,
1003
+ });
1004
+
1005
+ render(
1006
+ <Wrapper
1007
+ client={createTestQueryClient({ user: true })}
1008
+ product={product}
1009
+ orderGroup={orderGroup}
1010
+ >
1011
+ <PaymentButton billingAddress={billingAddress} onSuccess={handleSuccess} />
1012
+ </Wrapper>,
1013
+ );
1014
+
1015
+ const $button = screen.getByRole('button', {
1016
+ name: `Pay ${formatPrice(product.price, product.price_currency)}`,
1017
+ }) as HTMLButtonElement;
1018
+
1019
+ const $terms = screen.getByLabelText(
1020
+ 'By checking this box, you accept the General Terms of Sale',
1021
+ );
1022
+ await act(async () => {
1023
+ fireEvent.click($terms);
1024
+ });
1025
+
1026
+ // - Payment button should not be disabled.
1027
+ expect($button.disabled).toBe(false);
1028
+
1029
+ // - User clicks on pay button
1030
+ await act(async () => {
1031
+ fireEvent.click($button);
1032
+ });
1033
+
1034
+ await waitFor(() => expect(createOrderPayload?.order_group_id).toEqual(orderGroup.id));
1035
+ });
1036
+ }
1037
+ },
1038
+ );