richie-education 3.2.2-dev43 → 3.2.2-dev52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/i18n/locales/ar-SA.json +8 -4
- package/i18n/locales/es-ES.json +8 -4
- package/i18n/locales/fa-IR.json +8 -4
- package/i18n/locales/fr-CA.json +8 -4
- package/i18n/locales/fr-FR.json +10 -6
- package/i18n/locales/ko-KR.json +8 -4
- package/i18n/locales/pt-PT.json +8 -4
- package/i18n/locales/ru-RU.json +8 -4
- package/i18n/locales/vi-VN.json +8 -4
- package/js/api/auth/keycloak.spec.ts +63 -2
- package/js/api/auth/keycloak.ts +40 -2
- package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationGroup.tsx +12 -2
- package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationSingular.tsx +85 -4
- package/js/components/SaleTunnel/_styles.scss +8 -0
- package/js/components/SaleTunnel/index.spec.tsx +118 -0
- package/js/hooks/useOpenEdxProfile/index.ts +4 -2
- package/js/index.tsx +1 -1
- package/js/translations/ar-SA.json +1 -1
- package/js/translations/es-ES.json +1 -1
- package/js/translations/fa-IR.json +1 -1
- package/js/translations/fr-CA.json +1 -1
- package/js/translations/fr-FR.json +1 -1
- package/js/translations/ko-KR.json +1 -1
- package/js/translations/pt-PT.json +1 -1
- package/js/translations/ru-RU.json +1 -1
- package/js/translations/vi-VN.json +1 -1
- package/js/types/api.ts +14 -5
- package/js/types/keycloak.ts +8 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/index.tsx +17 -6
- package/js/widgets/index.tsx +3 -2
- package/package.json +1 -1
- package/scss/colors/_theme.scss +1 -0
|
@@ -11,6 +11,9 @@ import WithdrawRightCheckbox from 'components/SaleTunnel/WithdrawRightCheckbox';
|
|
|
11
11
|
import { PaymentSchedule, ProductType } from 'types/Joanie';
|
|
12
12
|
import { usePaymentPlan } from 'hooks/usePaymentPlan';
|
|
13
13
|
import { HttpError } from 'utils/errors/HttpError';
|
|
14
|
+
import { APIBackend, KeycloakAccountApi } from 'types/api';
|
|
15
|
+
import context from 'utils/context';
|
|
16
|
+
import { AuthenticationApi } from 'api/authentication';
|
|
14
17
|
|
|
15
18
|
const messages = defineMessages({
|
|
16
19
|
title: {
|
|
@@ -49,6 +52,31 @@ const messages = defineMessages({
|
|
|
49
52
|
defaultMessage:
|
|
50
53
|
'This email will be used to send you confirmation mails, it is the one you created your account with.',
|
|
51
54
|
},
|
|
55
|
+
keycloakUsernameLabel: {
|
|
56
|
+
id: 'components.SaleTunnel.Information.keycloak.account.label',
|
|
57
|
+
description: 'Label for the name',
|
|
58
|
+
defaultMessage: 'Account name',
|
|
59
|
+
},
|
|
60
|
+
keycloakUsernameInfo: {
|
|
61
|
+
id: 'components.SaleTunnel.Information.keycloak.account.info',
|
|
62
|
+
description: 'Info for the name',
|
|
63
|
+
defaultMessage: 'This name will be used in legal documents.',
|
|
64
|
+
},
|
|
65
|
+
keycloakEmailInfo: {
|
|
66
|
+
id: 'components.SaleTunnel.Information.keycloak.email.info',
|
|
67
|
+
description: 'Info for the email',
|
|
68
|
+
defaultMessage: 'This email will be used to send you confirmation mails.',
|
|
69
|
+
},
|
|
70
|
+
keycloakAccountLinkInfo: {
|
|
71
|
+
id: 'components.SaleTunnel.Information.keycloak.updateLinkInfo',
|
|
72
|
+
description: 'Text before the keycloak account update link',
|
|
73
|
+
defaultMessage: 'If any of the information above is incorrect,',
|
|
74
|
+
},
|
|
75
|
+
keycloakAccountLinkLabel: {
|
|
76
|
+
id: 'components.SaleTunnel.Information.keycloak.updateLinkLabel',
|
|
77
|
+
description: 'Label of the keycloak link to update account',
|
|
78
|
+
defaultMessage: 'please update your account',
|
|
79
|
+
},
|
|
52
80
|
voucherTitle: {
|
|
53
81
|
id: 'components.SaleTunnel.Information.voucher.title',
|
|
54
82
|
description: 'Title for the voucher',
|
|
@@ -130,6 +158,8 @@ export const SaleTunnelInformationSingular = () => {
|
|
|
130
158
|
setNeedsPayment(!fromBatchOrder);
|
|
131
159
|
}, [fromBatchOrder, setNeedsPayment]);
|
|
132
160
|
|
|
161
|
+
const isKeycloakBackend = context?.authentication.backend === APIBackend.KEYCLOAK;
|
|
162
|
+
|
|
133
163
|
return (
|
|
134
164
|
<>
|
|
135
165
|
<div>
|
|
@@ -148,11 +178,17 @@ export const SaleTunnelInformationSingular = () => {
|
|
|
148
178
|
<div className="description mb-s">
|
|
149
179
|
<FormattedMessage {...messages.description} />
|
|
150
180
|
</div>
|
|
151
|
-
<OpenEdxFullNameForm />
|
|
152
181
|
<AddressSelector />
|
|
153
|
-
|
|
154
|
-
<
|
|
155
|
-
|
|
182
|
+
{isKeycloakBackend ? (
|
|
183
|
+
<KeycloakAccountEdit />
|
|
184
|
+
) : (
|
|
185
|
+
<>
|
|
186
|
+
<OpenEdxFullNameForm />
|
|
187
|
+
<div className="mt-s">
|
|
188
|
+
<Email />
|
|
189
|
+
</div>
|
|
190
|
+
</>
|
|
191
|
+
)}
|
|
156
192
|
</div>
|
|
157
193
|
)}
|
|
158
194
|
<div>
|
|
@@ -163,6 +199,51 @@ export const SaleTunnelInformationSingular = () => {
|
|
|
163
199
|
);
|
|
164
200
|
};
|
|
165
201
|
|
|
202
|
+
const KeycloakAccountEdit = () => {
|
|
203
|
+
const accountApi = AuthenticationApi!.account as KeycloakAccountApi;
|
|
204
|
+
const { user } = useSession();
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<>
|
|
208
|
+
<div className="mt-s">
|
|
209
|
+
<div className="sale-tunnel__username">
|
|
210
|
+
<div className="sale-tunnel__username__top">
|
|
211
|
+
<h4>
|
|
212
|
+
<FormattedMessage {...messages.keycloakUsernameLabel} />
|
|
213
|
+
</h4>
|
|
214
|
+
<div className="fw-bold">{user?.username}</div>
|
|
215
|
+
</div>
|
|
216
|
+
<div className="sale-tunnel__username__description">
|
|
217
|
+
<FormattedMessage {...messages.keycloakUsernameInfo} />
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
<div className="mt-s">
|
|
222
|
+
<div className="sale-tunnel__email">
|
|
223
|
+
<div className="sale-tunnel__email__top">
|
|
224
|
+
<h4>
|
|
225
|
+
<FormattedMessage {...messages.emailLabel} />
|
|
226
|
+
</h4>
|
|
227
|
+
<div className="fw-bold">{user?.email}</div>
|
|
228
|
+
</div>
|
|
229
|
+
<div className="sale-tunnel__email__description">
|
|
230
|
+
<FormattedMessage {...messages.keycloakEmailInfo} />
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
<div className="mt-s">
|
|
235
|
+
<div className="sale-tunnel__account-link">
|
|
236
|
+
<FormattedMessage {...messages.keycloakAccountLinkInfo} />{' '}
|
|
237
|
+
<a href={accountApi.updateUrl()}>
|
|
238
|
+
<FormattedMessage {...messages.keycloakAccountLinkLabel} />
|
|
239
|
+
</a>
|
|
240
|
+
.
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</>
|
|
244
|
+
);
|
|
245
|
+
};
|
|
246
|
+
|
|
166
247
|
const Email = () => {
|
|
167
248
|
const { user } = useSession();
|
|
168
249
|
const { data: openEdxProfileData } = useOpenEdxProfile({
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
&__username,
|
|
70
71
|
&__email {
|
|
71
72
|
&__top {
|
|
72
73
|
display: flex;
|
|
@@ -82,6 +83,13 @@
|
|
|
82
83
|
font-size: 0.75rem;
|
|
83
84
|
}
|
|
84
85
|
}
|
|
86
|
+
|
|
87
|
+
&__account-link {
|
|
88
|
+
a {
|
|
89
|
+
color: r-theme-val(sale-tunnel, account-link-color);
|
|
90
|
+
text-decoration: underline;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
85
93
|
.price--striked {
|
|
86
94
|
text-decoration: line-through;
|
|
87
95
|
opacity: 0.5;
|
|
@@ -36,6 +36,8 @@ import { OpenEdxApiProfileFactory } from 'utils/test/factories/openEdx';
|
|
|
36
36
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
37
37
|
import { StringHelper } from 'utils/StringHelper';
|
|
38
38
|
import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
|
|
39
|
+
import { AuthenticationApi } from 'api/authentication';
|
|
40
|
+
import { APIAuthentication } from 'types/api';
|
|
39
41
|
import { Deferred } from 'utils/test/deferred';
|
|
40
42
|
|
|
41
43
|
jest.mock('utils/context', () => ({
|
|
@@ -850,3 +852,119 @@ describe.each([
|
|
|
850
852
|
expect(screen.queryByText('DISCOUNT30')).not.toBeInTheDocument();
|
|
851
853
|
});
|
|
852
854
|
});
|
|
855
|
+
|
|
856
|
+
describe('SaleTunnel with Keycloak backend', () => {
|
|
857
|
+
const mockAccountUpdateUrl = 'https://keycloak.test/auth/realms/richie-realm/account';
|
|
858
|
+
const course = PacedCourseFactory().one();
|
|
859
|
+
let richieUser: User;
|
|
860
|
+
let originalBackend: string;
|
|
861
|
+
let originalAccount: APIAuthentication['account'];
|
|
862
|
+
|
|
863
|
+
const Wrapper = (props: Omit<SaleTunnelProps, 'isOpen' | 'onClose'>) => {
|
|
864
|
+
const [open, setOpen] = useState(true);
|
|
865
|
+
return <SaleTunnel {...props} course={course} isOpen={open} onClose={() => setOpen(false)} />;
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
beforeEach(() => {
|
|
869
|
+
jest.useFakeTimers();
|
|
870
|
+
jest.clearAllTimers();
|
|
871
|
+
jest.resetAllMocks();
|
|
872
|
+
|
|
873
|
+
fetchMock.restore();
|
|
874
|
+
sessionStorage.clear();
|
|
875
|
+
|
|
876
|
+
richieUser = UserFactory({
|
|
877
|
+
username: 'John Doe',
|
|
878
|
+
email: 'johndoe@example.com',
|
|
879
|
+
}).one();
|
|
880
|
+
|
|
881
|
+
// Mock OpenEdx profile endpoints that may still be triggered by the fonzie-based
|
|
882
|
+
// AuthenticationApi (resolved at module load). These should not be called by
|
|
883
|
+
// the keycloak code paths we are testing.
|
|
884
|
+
fetchMock.get(`begin:https://auth.test/api/user/v1/accounts/`, {});
|
|
885
|
+
fetchMock.get(`begin:https://auth.test/api/user/v1/preferences/`, {});
|
|
886
|
+
|
|
887
|
+
// Temporarily switch context to keycloak backend
|
|
888
|
+
const context = require('utils/context').default;
|
|
889
|
+
originalBackend = context.authentication.backend;
|
|
890
|
+
context.authentication.backend = 'keycloak';
|
|
891
|
+
|
|
892
|
+
// Add keycloak account methods to AuthenticationApi
|
|
893
|
+
originalAccount = AuthenticationApi!.account;
|
|
894
|
+
AuthenticationApi!.account = {
|
|
895
|
+
get: () => ({
|
|
896
|
+
username: 'johndoe',
|
|
897
|
+
firstName: 'John',
|
|
898
|
+
lastName: 'Doe',
|
|
899
|
+
email: 'johndoe@example.com',
|
|
900
|
+
}),
|
|
901
|
+
updateUrl: () => mockAccountUpdateUrl,
|
|
902
|
+
};
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
// Must be called after beforeEach so fetchMock.restore() doesn't clear joanie mocks
|
|
906
|
+
setupJoanieSession();
|
|
907
|
+
|
|
908
|
+
afterEach(() => {
|
|
909
|
+
// Restore original backend and account
|
|
910
|
+
const context = require('utils/context').default;
|
|
911
|
+
context.authentication.backend = originalBackend;
|
|
912
|
+
AuthenticationApi!.account = originalAccount;
|
|
913
|
+
|
|
914
|
+
act(() => {
|
|
915
|
+
jest.runOnlyPendingTimers();
|
|
916
|
+
});
|
|
917
|
+
jest.useRealTimers();
|
|
918
|
+
cleanup();
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it('should render keycloak account name, email, and update link instead of OpenEdx profile', async () => {
|
|
922
|
+
const product = CredentialProductFactory().one();
|
|
923
|
+
const paymentPlan = PaymentPlanFactory().one();
|
|
924
|
+
|
|
925
|
+
fetchMock
|
|
926
|
+
.get(
|
|
927
|
+
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify({
|
|
928
|
+
course_code: course.code,
|
|
929
|
+
product_id: product.id,
|
|
930
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
931
|
+
})}`,
|
|
932
|
+
[],
|
|
933
|
+
)
|
|
934
|
+
.get(
|
|
935
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
936
|
+
paymentPlan,
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
render(<Wrapper product={product} isWithdrawable={true} paymentPlan={paymentPlan} />, {
|
|
940
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
// Should display the "Account name" heading
|
|
944
|
+
await screen.findByRole('heading', { level: 4, name: 'Account name' });
|
|
945
|
+
|
|
946
|
+
// Should display the username from the session
|
|
947
|
+
screen.getByText(richieUser.username);
|
|
948
|
+
|
|
949
|
+
// Should display the username info
|
|
950
|
+
screen.getByText('This name will be used in legal documents.');
|
|
951
|
+
|
|
952
|
+
// Should display the email from the session
|
|
953
|
+
screen.getByText(richieUser.email!);
|
|
954
|
+
|
|
955
|
+
// Should display the email info
|
|
956
|
+
screen.getByText('This email will be used to send you confirmation mails.');
|
|
957
|
+
|
|
958
|
+
// Should display the keycloak account update link (only the link part)
|
|
959
|
+
const updateLink = screen.getByRole('link', {
|
|
960
|
+
name: 'please update your account',
|
|
961
|
+
});
|
|
962
|
+
expect(updateLink).toHaveAttribute('href', mockAccountUpdateUrl);
|
|
963
|
+
|
|
964
|
+
// Should NOT render the OpenEdx full name form
|
|
965
|
+
expect(screen.queryByLabelText('First name and last name')).not.toBeInTheDocument();
|
|
966
|
+
|
|
967
|
+
// No OpenEdx profile API calls should have been made
|
|
968
|
+
expect(fetchMock.calls().filter(([url]) => url.includes('/api/user/v1/'))).toHaveLength(0);
|
|
969
|
+
});
|
|
970
|
+
});
|
|
@@ -7,6 +7,7 @@ import { useSessionMutation } from 'utils/react-query/useSessionMutation';
|
|
|
7
7
|
import { OpenEdxFullNameFormValues } from 'components/OpenEdxFullNameForm';
|
|
8
8
|
import { HttpError } from 'utils/errors/HttpError';
|
|
9
9
|
import { TSessionQueryKey } from 'utils/react-query/useSessionKey';
|
|
10
|
+
import { OpenEdxAccountApi } from 'types/api';
|
|
10
11
|
import { OpenEdxProfile, parseOpenEdxApiProfile } from './utils';
|
|
11
12
|
|
|
12
13
|
const messages = defineMessages({
|
|
@@ -61,7 +62,8 @@ const useOpenEdxProfile = (
|
|
|
61
62
|
|
|
62
63
|
const queryFn: () => Promise<OpenEdxProfile> = useCallback(async () => {
|
|
63
64
|
try {
|
|
64
|
-
const
|
|
65
|
+
const account = AuthenticationApi!.account as OpenEdxAccountApi;
|
|
66
|
+
const openEdxApiProfile = await account.get(username);
|
|
65
67
|
return parseOpenEdxApiProfile(intl, openEdxApiProfile);
|
|
66
68
|
} catch {
|
|
67
69
|
setError(intl.formatMessage(messages.errorGet));
|
|
@@ -79,7 +81,7 @@ const useOpenEdxProfile = (
|
|
|
79
81
|
const writeHandlers = {
|
|
80
82
|
update: mutation({
|
|
81
83
|
mutationFn: (data: OpenEdxFullNameFormValues) =>
|
|
82
|
-
AuthenticationApi!.account
|
|
84
|
+
(AuthenticationApi!.account as OpenEdxAccountApi).update(username, data),
|
|
83
85
|
onSuccess,
|
|
84
86
|
onError: () => setError(intl.formatMessage(messages.errorUpdate)),
|
|
85
87
|
}),
|
package/js/index.tsx
CHANGED
|
@@ -119,7 +119,7 @@ async function render() {
|
|
|
119
119
|
<QueryClientProvider client={queryClient}>
|
|
120
120
|
<ReactQueryDevtools initialIsOpen={false} />
|
|
121
121
|
<IntlProvider locale={locale} messages={translatedMessages} defaultLocale="en-US">
|
|
122
|
-
<Root richieReactSpots={richieReactSpots} />
|
|
122
|
+
<Root richieReactSpots={richieReactSpots} locale={locale} />
|
|
123
123
|
</IntlProvider>
|
|
124
124
|
</QueryClientProvider>,
|
|
125
125
|
);
|