richie-education 2.28.2-dev26 → 2.28.2-dev53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/js/api/joanie.ts +42 -17
  2. package/js/api/lms/dummy.ts +1 -12
  3. package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
  4. package/js/components/ContractFrame/AbstractContractFrame.tsx +28 -23
  5. package/js/components/ContractFrame/LearnerContractFrame.tsx +2 -2
  6. package/js/components/ContractFrame/_styles.scss +6 -14
  7. package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/index.spec.tsx +15 -45
  8. package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/index.tsx +17 -24
  9. package/js/components/DownloadContractButton/index.spec.tsx +1 -1
  10. package/js/components/OpenEdxFullNameForm/index.spec.tsx +229 -0
  11. package/js/components/OpenEdxFullNameForm/index.tsx +7 -7
  12. package/js/components/PaymentInterfaces/LyraPopIn.tsx +2 -2
  13. package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
  14. package/js/components/PaymentInterfaces/__mocks__/index.tsx +1 -4
  15. package/js/components/PaymentInterfaces/types.ts +5 -2
  16. package/js/components/PaymentScheduleGrid/_styles.scss +13 -0
  17. package/js/components/PaymentScheduleGrid/index.tsx +50 -70
  18. package/js/components/PurchaseButton/index.spec.tsx +84 -37
  19. package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -1
  20. package/js/components/SaleTunnel/CertificateSaleTunnel/index.tsx +2 -2
  21. package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +6 -10
  22. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +80 -27
  23. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +16 -20
  24. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/_styles.scss +12 -0
  25. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +160 -0
  26. package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +15 -29
  27. package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.scss +4 -5
  28. package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.tsx +39 -11
  29. package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
  30. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +201 -0
  31. package/js/components/SaleTunnel/_styles.scss +16 -5
  32. package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
  33. package/js/components/SaleTunnel/index.credential.spec.tsx +14 -25
  34. package/js/components/SaleTunnel/index.full-process.spec.tsx +116 -48
  35. package/js/components/SaleTunnel/index.spec.tsx +334 -717
  36. package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +16 -11
  37. package/js/components/SignContractButton/index.spec.tsx +16 -20
  38. package/js/components/SignContractButton/index.tsx +3 -1
  39. package/js/hooks/useCreditCards/index.spec.tsx +70 -6
  40. package/js/hooks/useCreditCards/index.ts +49 -11
  41. package/js/hooks/useOrders/index.spec.tsx +322 -0
  42. package/js/hooks/{useOrders.ts → useOrders/index.ts} +40 -14
  43. package/js/hooks/usePaymentSchedule.tsx +23 -0
  44. package/js/hooks/useProductOrder/index.spec.tsx +77 -60
  45. package/js/hooks/useProductOrder/index.tsx +2 -2
  46. package/js/hooks/useResources/useResourcesRoot.ts +4 -3
  47. package/js/index.tsx +2 -0
  48. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.spec.tsx +1 -1
  49. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.tsx +4 -2
  50. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +8 -5
  51. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +8 -9
  52. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +1 -1
  53. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +1 -6
  54. package/js/settings/settings.test.ts +11 -2
  55. package/js/types/Joanie.ts +77 -31
  56. package/js/utils/OrderHelper/index.ts +47 -38
  57. package/js/utils/test/factories/joanie.ts +66 -68
  58. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -18
  59. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +26 -32
  60. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +11 -6
  61. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +114 -5
  62. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +99 -12
  63. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +3 -1
  64. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +6 -7
  65. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/_styles.scss +7 -0
  66. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +126 -0
  67. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +209 -0
  68. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
  69. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +40 -25
  70. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +28 -22
  71. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +18 -73
  72. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +32 -16
  73. package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +3 -11
  74. package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +25 -3
  75. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -6
  76. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +7 -14
  77. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.spec.tsx +7 -5
  78. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.tsx +5 -7
  79. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +242 -332
  80. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +12 -13
  81. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +10 -21
  82. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +2 -2
  83. package/package.json +2 -1
  84. package/scss/components/_index.scss +4 -2
  85. package/js/components/PaymentButton/_styles.scss +0 -27
  86. package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -338
  87. package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
  88. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +0 -41
  89. /package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/_styles.scss +0 -0
@@ -5,11 +5,12 @@ import { MemoryRouter } from 'react-router-dom';
5
5
  import userEvent from '@testing-library/user-event';
6
6
  import { QueryClientProvider } from '@tanstack/react-query';
7
7
  import fetchMock from 'fetch-mock';
8
- import { ContractFactory, CredentialOrderFactory } from 'utils/test/factories/joanie';
8
+ import { CunninghamProvider } from '@openfun/cunningham-react';
9
9
  import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
10
+ import { ContractFactory, CredentialOrderFactory } from 'utils/test/factories/joanie';
10
11
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
11
12
  import { useOmniscientOrders } from 'hooks/useOrders';
12
- import { CredentialOrder } from 'types/Joanie';
13
+ import { OrderState, CredentialOrder } from 'types/Joanie';
13
14
  import { SessionProvider } from 'contexts/SessionContext';
14
15
  import SignContractButton from '.';
15
16
 
@@ -30,13 +31,15 @@ jest.mock('settings', () => ({
30
31
  describe('<SignContractButton/>', () => {
31
32
  const Wrapper = ({ children }: PropsWithChildren) => {
32
33
  return (
33
- <QueryClientProvider client={createTestQueryClient({ user: true })}>
34
- <IntlProvider locale="en">
35
- <SessionProvider>
36
- <MemoryRouter>{children}</MemoryRouter>
37
- </SessionProvider>
38
- </IntlProvider>
39
- </QueryClientProvider>
34
+ <CunninghamProvider>
35
+ <QueryClientProvider client={createTestQueryClient({ user: true })}>
36
+ <IntlProvider locale="en">
37
+ <SessionProvider>
38
+ <MemoryRouter>{children}</MemoryRouter>
39
+ </SessionProvider>
40
+ </IntlProvider>
41
+ </QueryClientProvider>
42
+ </CunninghamProvider>
40
43
  );
41
44
  };
42
45
 
@@ -64,6 +67,7 @@ describe('<SignContractButton/>', () => {
64
67
  );
65
68
  };
66
69
  const order = CredentialOrderFactory({
70
+ state: OrderState.TO_SIGN,
67
71
  contract: ContractFactory({ student_signed_on: undefined }).one(),
68
72
  }).one();
69
73
  fetchMock.get(
@@ -114,6 +118,7 @@ describe('<SignContractButton/>', () => {
114
118
  fetchMock.get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, signedOrder, {
115
119
  overwriteRoutes: true,
116
120
  });
121
+ fetchMock.post(`https://joanie.endpoint/api/v1.0/signature/notifications/`, 200);
117
122
  const $modal = screen.getByTestId('dashboard-contract-frame');
118
123
  await user.click(await within($modal).findByRole('button', { name: 'Sign' }));
119
124
 
@@ -121,11 +126,11 @@ describe('<SignContractButton/>', () => {
121
126
  expect(
122
127
  await within($modal).findByRole('heading', { name: 'Congratulations!' }),
123
128
  ).toBeInTheDocument();
124
- // Orders's cache validation shouln't have close the ContractFrame.
129
+ // Orders's cache validation shouln't have closed the ContractFrame.
125
130
  expect(screen.queryByTestId('dashboard-contract-frame')).toBeInTheDocument();
126
131
 
127
132
  // Close modal.
128
- const $closeButton = screen.getByRole('button', { name: 'Close dialog' });
133
+ const $closeButton = screen.getByRole('button', { name: 'close' });
129
134
  await user.click($closeButton);
130
135
  expect(screen.queryByTestId('dashboard-contract-frame')).not.toBeInTheDocument();
131
136
 
@@ -5,12 +5,13 @@ import { MemoryRouter } from 'react-router-dom';
5
5
  import userEvent from '@testing-library/user-event';
6
6
  import { QueryClientProvider } from '@tanstack/react-query';
7
7
  import { faker } from '@faker-js/faker';
8
+ import { CunninghamProvider } from '@openfun/cunningham-react';
9
+ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
8
10
  import {
9
11
  ContractFactory,
10
12
  CredentialOrderFactory,
11
13
  NestedCredentialOrderFactory,
12
14
  } from 'utils/test/factories/joanie';
13
- import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
14
15
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
15
16
  import JoanieApiProvider from 'contexts/JoanieApiContext';
16
17
  import { Contract, CredentialOrder, NestedCredentialOrder, OrderState } from 'types/Joanie';
@@ -57,13 +58,15 @@ describe('<SignContractButton/>', () => {
57
58
  let order: CredentialOrder | NestedCredentialOrder;
58
59
  const Wrapper = ({ children }: PropsWithChildren) => {
59
60
  return (
60
- <QueryClientProvider client={createTestQueryClient({ user: true })}>
61
- <IntlProvider locale="en">
62
- <JoanieApiProvider>
63
- <MemoryRouter>{children}</MemoryRouter>
64
- </JoanieApiProvider>
65
- </IntlProvider>
66
- </QueryClientProvider>
61
+ <CunninghamProvider>
62
+ <QueryClientProvider client={createTestQueryClient({ user: true })}>
63
+ <IntlProvider locale="en">
64
+ <JoanieApiProvider>
65
+ <MemoryRouter>{children}</MemoryRouter>
66
+ </JoanieApiProvider>
67
+ </IntlProvider>
68
+ </QueryClientProvider>
69
+ </CunninghamProvider>
67
70
  );
68
71
  };
69
72
 
@@ -77,7 +80,7 @@ describe('<SignContractButton/>', () => {
77
80
  testCase === TestCase.FROM_ORDER_WITH_CONTRACT
78
81
  ? ContractFactory({ student_signed_on: null }).one()
79
82
  : undefined;
80
- order = OrderFactory({ contract: orderContract }).one();
83
+ order = OrderFactory({ contract: orderContract, state: OrderState.TO_SIGN }).one();
81
84
  contract = order.contract;
82
85
  }
83
86
  });
@@ -88,11 +91,10 @@ describe('<SignContractButton/>', () => {
88
91
  <SignContractButton order={order} contract={contract} writable={false} />
89
92
  </Wrapper>,
90
93
  );
91
- expect(screen.getByRole('link', { name: 'Sign' })).toBeInTheDocument();
92
94
 
95
+ expect(screen.getByRole('link', { name: 'Sign' })).toBeInTheDocument();
93
96
  expect(screen.queryByRole('button', { name: 'Sign' })).not.toBeInTheDocument();
94
97
  expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument();
95
- expect(document.querySelector('div.ReactModalPortal')).not.toBeInTheDocument();
96
98
  });
97
99
 
98
100
  it('should display a button that open ContractFrame modal', async () => {
@@ -101,9 +103,9 @@ describe('<SignContractButton/>', () => {
101
103
  <SignContractButton order={order} contract={contract} writable={true} />
102
104
  </Wrapper>,
103
105
  );
106
+
104
107
  const $signButton = screen.queryByRole('button', { name: 'Sign' });
105
108
  expect($signButton).toBeInTheDocument();
106
- expect(document.querySelector('div.ReactModalPortal')).toBeInTheDocument();
107
109
 
108
110
  expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument();
109
111
  expect(screen.queryByRole('link', { name: 'Sign' })).not.toBeInTheDocument();
@@ -145,7 +147,6 @@ describe('<SignContractButton/>', () => {
145
147
  <SignContractButton order={order} contract={contract} writable={false} />
146
148
  </Wrapper>,
147
149
  );
148
- expect(document.querySelector('div.ReactModalPortal')).toBeInTheDocument();
149
150
 
150
151
  expect(screen.queryByRole('link', { name: 'Sign' })).not.toBeInTheDocument();
151
152
  expect(screen.queryByRole('button', { name: 'Sign' })).not.toBeInTheDocument();
@@ -164,8 +165,6 @@ describe('<SignContractButton/>', () => {
164
165
  </Wrapper>,
165
166
  );
166
167
 
167
- expect(document.querySelector('div.ReactModalPortal')).toBeInTheDocument();
168
-
169
168
  expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument();
170
169
  expect(screen.queryByRole('button', { name: 'Sign' })).not.toBeInTheDocument();
171
170
  expect(screen.queryByRole('link', { name: 'Sign' })).not.toBeInTheDocument();
@@ -194,11 +193,10 @@ describe('<SignContractButton/>', () => {
194
193
  <SignContractButton order={order} contract={contract} writable={false} />
195
194
  </Wrapper>,
196
195
  );
197
- expect(screen.queryByRole('button', { name: 'Download' })).toBeInTheDocument();
198
- expect(document.querySelector('div.ReactModalPortal')).toBeInTheDocument();
199
196
 
200
- expect(screen.queryByRole('link', { name: 'Sign' })).not.toBeInTheDocument();
197
+ expect(screen.queryByRole('button', { name: 'Download' })).toBeInTheDocument();
201
198
  expect(screen.queryByRole('button', { name: 'Sign' })).not.toBeInTheDocument();
199
+ expect(screen.queryByRole('link', { name: 'Sign' })).not.toBeInTheDocument();
202
200
  });
203
201
 
204
202
  it('should display a button to download the training contract', async () => {
@@ -214,8 +212,6 @@ describe('<SignContractButton/>', () => {
214
212
  );
215
213
 
216
214
  expect(screen.queryByRole('button', { name: 'Download' })).toBeInTheDocument();
217
- expect(document.querySelector('div.ReactModalPortal')).toBeInTheDocument();
218
-
219
215
  expect(screen.queryByRole('button', { name: 'Sign' })).not.toBeInTheDocument();
220
216
  expect(screen.queryByRole('link', { name: 'Sign' })).not.toBeInTheDocument();
221
217
  });
@@ -67,7 +67,9 @@ const SignContractButton = ({ order, contract, writable, className }: SignContra
67
67
  const [contractLoading, setContractLoading] = useState(false);
68
68
  const contractState = ContractHelper.getState(contract);
69
69
  const notReadyToSign =
70
- order.state !== OrderState.VALIDATED || contractLoading || contractFrameOpened;
70
+ ![OrderState.TO_SIGN, OrderState.SIGNING].includes(order.state) ||
71
+ contractLoading ||
72
+ contractFrameOpened;
71
73
 
72
74
  if (!writable && contractState === ContractState.UNSIGNED) {
73
75
  return <SignContractButtonLink orderId={order.id} className={className} />;
@@ -9,6 +9,7 @@ import { useCreditCard, useCreditCards } from 'hooks/useCreditCards/index';
9
9
  import { SessionProvider } from 'contexts/SessionContext';
10
10
  import { Deferred } from 'utils/test/deferred';
11
11
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
12
+ import { HttpStatusCode } from 'utils/errors/HttpError';
12
13
  import { CreditCard } from 'types/Joanie';
13
14
 
14
15
  jest.mock('utils/context', () => ({
@@ -23,6 +24,7 @@ describe('useCreditCards', () => {
23
24
  beforeEach(() => {
24
25
  fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', []);
25
26
  fetchMock.get('https://joanie.endpoint/api/v1.0/addresses/', []);
27
+ fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', []);
26
28
  });
27
29
 
28
30
  afterEach(() => {
@@ -43,7 +45,9 @@ describe('useCreditCards', () => {
43
45
  it('retrieves all the credit cards', async () => {
44
46
  const creditCards = CreditCardFactory().many(5);
45
47
  const responseDeferred = new Deferred();
46
- fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', responseDeferred.promise);
48
+ fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', responseDeferred.promise, {
49
+ overwriteRoutes: true,
50
+ });
47
51
 
48
52
  const { result } = renderHook(() => useCreditCards(), {
49
53
  wrapper: Wrapper,
@@ -53,7 +57,6 @@ describe('useCreditCards', () => {
53
57
  expect(result.current.states.fetching).toBe(true);
54
58
  expect(result.current.items).toEqual([]);
55
59
  });
56
- expect(result.current.states.creating).toBe(false);
57
60
  expect(result.current.states.deleting).toBe(false);
58
61
  expect(result.current.states.updating).toBe(false);
59
62
  expect(result.current.states.isPending).toBe(true);
@@ -67,7 +70,6 @@ describe('useCreditCards', () => {
67
70
  expect(result.current.states.fetching).toBe(false);
68
71
  expect(JSON.stringify(result.current.items)).toBe(JSON.stringify(creditCards));
69
72
  });
70
- expect(result.current.states.creating).toBe(false);
71
73
  expect(result.current.states.deleting).toBe(false);
72
74
  expect(result.current.states.updating).toBe(false);
73
75
  expect(result.current.states.isPending).toBe(false);
@@ -78,7 +80,9 @@ describe('useCreditCards', () => {
78
80
  const creditCards = CreditCardFactory().many(5);
79
81
  const creditCard: CreditCard = creditCards[3];
80
82
  const responseDeferred = new Deferred();
81
- fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', responseDeferred.promise);
83
+ fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', responseDeferred.promise, {
84
+ overwriteRoutes: true,
85
+ });
82
86
  const { result } = renderHook(() => useCreditCard(creditCard.id), {
83
87
  wrapper: Wrapper,
84
88
  });
@@ -88,7 +92,6 @@ describe('useCreditCards', () => {
88
92
  expect(result.current.item).toEqual(undefined);
89
93
  });
90
94
 
91
- expect(result.current.states.creating).toBe(false);
92
95
  expect(result.current.states.deleting).toBe(false);
93
96
  expect(result.current.states.updating).toBe(false);
94
97
  expect(result.current.states.isPending).toBe(true);
@@ -103,10 +106,71 @@ describe('useCreditCards', () => {
103
106
  expect(JSON.stringify(result.current.item)).toBe(JSON.stringify(creditCard));
104
107
  });
105
108
 
106
- expect(result.current.states.creating).toBe(false);
107
109
  expect(result.current.states.deleting).toBe(false);
108
110
  expect(result.current.states.updating).toBe(false);
109
111
  expect(result.current.states.isPending).toBe(false);
110
112
  expect(result.current.states.error).toBe(undefined);
111
113
  });
114
+
115
+ it('tokenize a credit card', async () => {
116
+ const responseDeferred = new Deferred();
117
+ fetchMock.post(
118
+ 'https://joanie.endpoint/api/v1.0/credit-cards/tokenize-card/',
119
+ responseDeferred.promise,
120
+ );
121
+ const { result } = renderHook(() => useCreditCards(undefined, { enabled: false }), {
122
+ wrapper: Wrapper,
123
+ });
124
+
125
+ await waitFor(() => {
126
+ expect(result.current).not.toBeNull();
127
+ });
128
+
129
+ await act(async () => {
130
+ result.current.methods.tokenize();
131
+ });
132
+
133
+ await waitFor(() => {
134
+ expect(result.current.states.tokenizing).toBe(true);
135
+ });
136
+
137
+ expect(result.current.states.deleting).toBe(false);
138
+ expect(result.current.states.updating).toBe(false);
139
+ expect(result.current.states.fetching).toBe(false);
140
+ expect(result.current.states.isFetched).toBe(true);
141
+ expect(result.current.states.isPending).toBe(true);
142
+ expect(result.current.states.error).toBe(undefined);
143
+
144
+ await act(async () => {
145
+ responseDeferred.resolve({});
146
+ });
147
+
148
+ expect(result.current.states.tokenizing).toBe(false);
149
+ expect(result.current.states.isPending).toBe(false);
150
+ expect(result.current.states.error).toBe(undefined);
151
+ });
152
+
153
+ it('manages error during credit card tokenization', async () => {
154
+ fetchMock.post(
155
+ 'https://joanie.endpoint/api/v1.0/credit-cards/tokenize-card/',
156
+ HttpStatusCode.INTERNAL_SERVER_ERROR,
157
+ );
158
+ const { result } = renderHook(() => useCreditCards(undefined, { enabled: true }), {
159
+ wrapper: Wrapper,
160
+ });
161
+
162
+ await waitFor(() => {
163
+ expect(result.current).not.toBeNull();
164
+ });
165
+
166
+ await act(async () => {
167
+ await expect(result.current.methods.tokenize()).rejects.toThrow('Internal Server Error');
168
+ });
169
+
170
+ expect(result.current.states.error).toBe(
171
+ 'An error occurred while adding a credit card. Please retry later.',
172
+ );
173
+ expect(result.current.states.isPending).toBe(false);
174
+ expect(result.current.states.tokenizing).toBe(false);
175
+ });
112
176
  });
@@ -1,8 +1,15 @@
1
- import { defineMessages } from 'react-intl';
2
- import { CreditCard } from 'types/Joanie';
3
-
1
+ import { defineMessages, useIntl } from 'react-intl';
2
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
3
+ import { API, CreditCard } from 'types/Joanie';
4
4
  import { useJoanieApi } from 'contexts/JoanieApiContext';
5
- import { useResource, useResources, UseResourcesProps } from '../useResources';
5
+ import { useSessionMutation } from 'utils/react-query/useSessionMutation';
6
+ import {
7
+ QueryOptions,
8
+ ResourcesQuery,
9
+ useResource,
10
+ useResourcesCustom,
11
+ UseResourcesProps,
12
+ } from '../useResources';
6
13
 
7
14
  const messages = defineMessages({
8
15
  errorUpdate: {
@@ -20,10 +27,10 @@ const messages = defineMessages({
20
27
  description: 'Error message shown to the user when credit card deletion request fails.',
21
28
  defaultMessage: 'An error occurred while deleting the credit card. Please retry later.',
22
29
  },
23
- errorCreate: {
24
- id: 'hooks.useCreditCards.errorCreate',
25
- description: 'Error message shown to the user when credit card creation request fails.',
26
- defaultMessage: 'An error occurred while creating the credit card. Please retry later.',
30
+ errorTokenize: {
31
+ id: 'hooks.useCreditCards.errorTokenize',
32
+ description: 'Error message shown to the user when credit card tokenize request fails.',
33
+ defaultMessage: 'An error occurred while adding a credit card. Please retry later.',
27
34
  },
28
35
  errorNotFound: {
29
36
  id: 'hooks.useCreditCards.errorNotFound',
@@ -32,16 +39,47 @@ const messages = defineMessages({
32
39
  },
33
40
  });
34
41
 
42
+ const useCreditCardResources =
43
+ (props: UseResourcesProps<CreditCard, ResourcesQuery, API['user']['creditCards']>) =>
44
+ (filters?: ResourcesQuery, queryOptions?: QueryOptions<CreditCard>) => {
45
+ const custom = useResourcesCustom({ ...props, filters, queryOptions });
46
+ const queryClient = useQueryClient();
47
+ const intl = useIntl();
48
+ const api = props.apiInterface();
49
+ const mutation = (props.session ? useSessionMutation : useMutation) as typeof useMutation;
50
+ const tokenizeHandler = mutation({
51
+ mutationFn: api.tokenize,
52
+ onSuccess: async () => {
53
+ custom.methods.setError(undefined);
54
+ props.onMutationSuccess?.(queryClient);
55
+ },
56
+ onError: () => custom.methods.setError(intl.formatMessage(messages.errorTokenize)),
57
+ });
58
+
59
+ return {
60
+ ...custom,
61
+ methods: {
62
+ ...custom.methods,
63
+ tokenize: tokenizeHandler.mutateAsync,
64
+ },
65
+ states: {
66
+ ...custom.states,
67
+ isPending: [tokenizeHandler, custom.states].some((value) => value?.isPending),
68
+ tokenizing: tokenizeHandler.isPending,
69
+ },
70
+ };
71
+ };
72
+
35
73
  /**
36
74
  * Joanie Api hook to retrieve/create/update/delete credit cards
37
75
  * owned by the authenticated user.
38
76
  */
39
- const props: UseResourcesProps<CreditCard> = {
77
+ const props: UseResourcesProps<CreditCard, ResourcesQuery, API['user']['creditCards']> = {
40
78
  queryKey: ['creditCards'],
41
79
  apiInterface: () => useJoanieApi().user.creditCards,
42
80
  omniscient: true,
43
81
  session: true,
44
82
  messages,
45
83
  };
46
- export const useCreditCards = useResources<CreditCard>(props);
47
- export const useCreditCard = useResource<CreditCard>(props);
84
+ export const useCreditCards = useCreditCardResources(props);
85
+ export const useCreditCard = useResource(props);