@redacto.io/consent-sdk-react 10.0.0-beta.11 → 10.0.0-beta.12

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 (259) hide show
  1. package/NOTICE.md +17 -0
  2. package/README.md +67 -3
  3. package/dist/chunk-FEGTW73Q.mjs +182 -0
  4. package/dist/fonts.d.mts +35 -0
  5. package/dist/fonts.d.ts +35 -0
  6. package/dist/fonts.js +183 -0
  7. package/dist/fonts.mjs +10 -0
  8. package/dist/index.d.mts +12 -1
  9. package/dist/index.d.ts +12 -1
  10. package/dist/index.js +613 -249
  11. package/dist/index.mjs +235 -85
  12. package/dist/montserrat.generated-NBTECYRW.mjs +22 -0
  13. package/dist/types-6Aj7faxL.d.mts +57 -0
  14. package/dist/types-6Aj7faxL.d.ts +57 -0
  15. package/package.json +10 -1
  16. package/.turbo/turbo-build.log +0 -22
  17. package/.turbo/turbo-test.log +0 -203
  18. package/.turbo/turbo-verify-pack.log +0 -6
  19. package/CHANGELOG.md +0 -383
  20. package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.test.tsx +0 -934
  21. package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.tsx +0 -2160
  22. package/src/RedactoNoticeAssisted/api/index.ts +0 -275
  23. package/src/RedactoNoticeAssisted/api/types.ts +0 -182
  24. package/src/RedactoNoticeAssisted/assets/redacto-logo.png +0 -0
  25. package/src/RedactoNoticeAssisted/constants.ts +0 -51
  26. package/src/RedactoNoticeAssisted/i18n.ts +0 -1408
  27. package/src/RedactoNoticeAssisted/index.ts +0 -1
  28. package/src/RedactoNoticeAssisted/injectStyles.ts +0 -251
  29. package/src/RedactoNoticeAssisted/styles.ts +0 -730
  30. package/src/RedactoNoticeAssisted/types.ts +0 -26
  31. package/src/RedactoNoticeAssisted/useMediaQuery.ts +0 -36
  32. package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +0 -1186
  33. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +0 -4299
  34. package/src/RedactoNoticeConsent/api/index.ts +0 -486
  35. package/src/RedactoNoticeConsent/api/sandbox.ts +0 -150
  36. package/src/RedactoNoticeConsent/api/types.ts +0 -505
  37. package/src/RedactoNoticeConsent/assets/redacto-logo.png +0 -0
  38. package/src/RedactoNoticeConsent/components/DigiLockerCallback.tsx +0 -602
  39. package/src/RedactoNoticeConsent/components/index.ts +0 -7
  40. package/src/RedactoNoticeConsent/constants.ts +0 -20
  41. package/src/RedactoNoticeConsent/index.ts +0 -1
  42. package/src/RedactoNoticeConsent/injectStyles.ts +0 -151
  43. package/src/RedactoNoticeConsent/language-codes.ts +0 -95
  44. package/src/RedactoNoticeConsent/services/storage.ts +0 -122
  45. package/src/RedactoNoticeConsent/styles.ts +0 -409
  46. package/src/RedactoNoticeConsent/types.ts +0 -80
  47. package/src/RedactoNoticeConsent/useMediaQuery.ts +0 -36
  48. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.test.tsx +0 -369
  49. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +0 -709
  50. package/src/RedactoNoticeConsentInline/api/index.ts +0 -115
  51. package/src/RedactoNoticeConsentInline/api/notice-read-routing.test.ts +0 -72
  52. package/src/RedactoNoticeConsentInline/api/types.ts +0 -185
  53. package/src/RedactoNoticeConsentInline/assets/redacto-logo.png +0 -0
  54. package/src/RedactoNoticeConsentInline/index.ts +0 -1
  55. package/src/RedactoNoticeConsentInline/injectStyles.ts +0 -40
  56. package/src/RedactoNoticeConsentInline/styles.ts +0 -397
  57. package/src/RedactoNoticeConsentInline/types.ts +0 -51
  58. package/src/RedactoNoticeConsentInline/useMediaQuery.ts +0 -36
  59. package/src/RedactoPrivacyCenter/NotificationsPage.tsx +0 -182
  60. package/src/RedactoPrivacyCenter/PrivacyCenterActivityPage.tsx +0 -19
  61. package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +0 -156
  62. package/src/RedactoPrivacyCenter/PrivacyCenterConsentManagerPage.tsx +0 -17
  63. package/src/RedactoPrivacyCenter/PrivacyCenterFormPage.tsx +0 -79
  64. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +0 -36
  65. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +0 -177
  66. package/src/RedactoPrivacyCenter/PrivacyCenterMyReceiptPage.tsx +0 -26
  67. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +0 -93
  68. package/src/RedactoPrivacyCenter/api/actions.ts +0 -682
  69. package/src/RedactoPrivacyCenter/api/client.ts +0 -1003
  70. package/src/RedactoPrivacyCenter/api/config.ts +0 -33
  71. package/src/RedactoPrivacyCenter/api/fetcher.ts +0 -275
  72. package/src/RedactoPrivacyCenter/api/orgBranding.ts +0 -16
  73. package/src/RedactoPrivacyCenter/api/upload.ts +0 -93
  74. package/src/RedactoPrivacyCenter/assets/character.svg +0 -31
  75. package/src/RedactoPrivacyCenter/assets/fileAttach.svg +0 -9
  76. package/src/RedactoPrivacyCenter/assets/greenTick.svg +0 -4
  77. package/src/RedactoPrivacyCenter/assets/mail.svg +0 -3
  78. package/src/RedactoPrivacyCenter/assets/poweredByRedacto.svg +0 -9
  79. package/src/RedactoPrivacyCenter/assets/sentImg.svg +0 -9
  80. package/src/RedactoPrivacyCenter/assets/uploadFileIcon.svg +0 -9
  81. package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +0 -252
  82. package/src/RedactoPrivacyCenter/components/ActivityTimeline.tsx +0 -239
  83. package/src/RedactoPrivacyCenter/components/AppShell/AppShell.tsx +0 -268
  84. package/src/RedactoPrivacyCenter/components/AppShell/MobileNavDrawer.tsx +0 -218
  85. package/src/RedactoPrivacyCenter/components/AppShell/NavItem.tsx +0 -205
  86. package/src/RedactoPrivacyCenter/components/AppShell/PageHeader.tsx +0 -107
  87. package/src/RedactoPrivacyCenter/components/AppShell/SidebarNav.tsx +0 -100
  88. package/src/RedactoPrivacyCenter/components/AppShell/TopAppBar.tsx +0 -339
  89. package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +0 -245
  90. package/src/RedactoPrivacyCenter/components/AppShell/hooks/useIsTruncated.ts +0 -28
  91. package/src/RedactoPrivacyCenter/components/AppShell/hooks/useKeyboardShortcut.ts +0 -40
  92. package/src/RedactoPrivacyCenter/components/AppShell/hooks/useSidebarState.ts +0 -62
  93. package/src/RedactoPrivacyCenter/components/AppShell/usePrivacyCenterExitAction.ts +0 -36
  94. package/src/RedactoPrivacyCenter/components/CaseDetails/AttachmentCard.tsx +0 -38
  95. package/src/RedactoPrivacyCenter/components/CaseDetails/ChatBubble.tsx +0 -31
  96. package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +0 -79
  97. package/src/RedactoPrivacyCenter/components/CaseDetails/FinalizedTooltipWrapper.tsx +0 -25
  98. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +0 -96
  99. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +0 -97
  100. package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +0 -156
  101. package/src/RedactoPrivacyCenter/components/CaseDetails/RequestDetailsTab.tsx +0 -171
  102. package/src/RedactoPrivacyCenter/components/CaseDetails/StatusEvent.tsx +0 -23
  103. package/src/RedactoPrivacyCenter/components/CaseDetails/useFileUpload.ts +0 -116
  104. package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +0 -43
  105. package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +0 -71
  106. package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +0 -469
  107. package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +0 -426
  108. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +0 -1199
  109. package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +0 -201
  110. package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +0 -138
  111. package/src/RedactoPrivacyCenter/components/ConsentManagerConstants.ts +0 -7
  112. package/src/RedactoPrivacyCenter/components/ConsentManagerExpandedRow.tsx +0 -179
  113. package/src/RedactoPrivacyCenter/components/ConsentManagerGroupedColumns.tsx +0 -318
  114. package/src/RedactoPrivacyCenter/components/ConsentManagerProductCards.tsx +0 -333
  115. package/src/RedactoPrivacyCenter/components/ConsentManagerPurposeCards.tsx +0 -102
  116. package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +0 -159
  117. package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +0 -189
  118. package/src/RedactoPrivacyCenter/components/DataRequest/GrievanceDataSection.tsx +0 -118
  119. package/src/RedactoPrivacyCenter/components/DataRequest/NominationDataSection.tsx +0 -66
  120. package/src/RedactoPrivacyCenter/components/DataRequest/ProductSelectionSection.tsx +0 -222
  121. package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +0 -331
  122. package/src/RedactoPrivacyCenter/components/Form.tsx +0 -1821
  123. package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +0 -174
  124. package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +0 -337
  125. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +0 -334
  126. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationBell.tsx +0 -173
  127. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationCTAButton.tsx +0 -18
  128. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdown.tsx +0 -54
  129. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdownItem.tsx +0 -57
  130. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationIcon.tsx +0 -19
  131. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationListPanel.tsx +0 -92
  132. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRefreshButton.tsx +0 -20
  133. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRow.tsx +0 -58
  134. package/src/RedactoPrivacyCenter/components/NotificationHub/StatCard.tsx +0 -10
  135. package/src/RedactoPrivacyCenter/components/NotificationHub/useNotificationActions.ts +0 -112
  136. package/src/RedactoPrivacyCenter/components/NotificationHub/utils.ts +0 -27
  137. package/src/RedactoPrivacyCenter/components/Pagination.tsx +0 -140
  138. package/src/RedactoPrivacyCenter/components/PrivacyCenterEmptyState.tsx +0 -76
  139. package/src/RedactoPrivacyCenter/components/PrivacyCenterSessionExpired.tsx +0 -97
  140. package/src/RedactoPrivacyCenter/components/ReceiptsList.tsx +0 -454
  141. package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +0 -270
  142. package/src/RedactoPrivacyCenter/components/SelectUser.tsx +0 -95
  143. package/src/RedactoPrivacyCenter/components/SelectUserGate.tsx +0 -195
  144. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +0 -25
  145. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +0 -21
  146. package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +0 -159
  147. package/src/RedactoPrivacyCenter/components/constant.ts +0 -52
  148. package/src/RedactoPrivacyCenter/components/groupHelpers.ts +0 -155
  149. package/src/RedactoPrivacyCenter/components/receiptColumns.tsx +0 -203
  150. package/src/RedactoPrivacyCenter/components/types.ts +0 -123
  151. package/src/RedactoPrivacyCenter/components/utils.tsx +0 -10
  152. package/src/RedactoPrivacyCenter/config/routes.ts +0 -9
  153. package/src/RedactoPrivacyCenter/config/table.ts +0 -25
  154. package/src/RedactoPrivacyCenter/context/AuthContext.tsx +0 -252
  155. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +0 -47
  156. package/src/RedactoPrivacyCenter/context/BrandingContext.tsx +0 -52
  157. package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.session-expired.test.tsx +0 -92
  158. package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.tsx +0 -181
  159. package/src/RedactoPrivacyCenter/context/LocaleContext.tsx +0 -11
  160. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +0 -96
  161. package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +0 -30
  162. package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +0 -80
  163. package/src/RedactoPrivacyCenter/context/ProfileSwitchContext.tsx +0 -15
  164. package/src/RedactoPrivacyCenter/context/types.ts +0 -57
  165. package/src/RedactoPrivacyCenter/lib/api.ts +0 -759
  166. package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +0 -67
  167. package/src/RedactoPrivacyCenter/lib/constants.ts +0 -283
  168. package/src/RedactoPrivacyCenter/lib/i18n.ts +0 -81
  169. package/src/RedactoPrivacyCenter/lib/locales.ts +0 -102
  170. package/src/RedactoPrivacyCenter/lib/queries.ts +0 -141
  171. package/src/RedactoPrivacyCenter/lib/sandbox.ts +0 -104
  172. package/src/RedactoPrivacyCenter/lib/types.ts +0 -892
  173. package/src/RedactoPrivacyCenter/lib/useCurrentLanguage.ts +0 -37
  174. package/src/RedactoPrivacyCenter/lib/utils.refresh-circuit.test.ts +0 -127
  175. package/src/RedactoPrivacyCenter/lib/utils.refresh-storage.test.ts +0 -85
  176. package/src/RedactoPrivacyCenter/lib/utils.session-expired.test.ts +0 -109
  177. package/src/RedactoPrivacyCenter/lib/utils.ts +0 -798
  178. package/src/RedactoPrivacyCenter/locales/as/translation.json +0 -340
  179. package/src/RedactoPrivacyCenter/locales/bn/translation.json +0 -340
  180. package/src/RedactoPrivacyCenter/locales/brx/translation.json +0 -340
  181. package/src/RedactoPrivacyCenter/locales/doi/translation.json +0 -340
  182. package/src/RedactoPrivacyCenter/locales/en/translation.json +0 -355
  183. package/src/RedactoPrivacyCenter/locales/gom/translation.json +0 -340
  184. package/src/RedactoPrivacyCenter/locales/gu/translation.json +0 -340
  185. package/src/RedactoPrivacyCenter/locales/hi/translation.json +0 -343
  186. package/src/RedactoPrivacyCenter/locales/kn/translation.json +0 -340
  187. package/src/RedactoPrivacyCenter/locales/ks/translation.json +0 -340
  188. package/src/RedactoPrivacyCenter/locales/mai/translation.json +0 -340
  189. package/src/RedactoPrivacyCenter/locales/ml/translation.json +0 -340
  190. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +0 -340
  191. package/src/RedactoPrivacyCenter/locales/mr/translation.json +0 -340
  192. package/src/RedactoPrivacyCenter/locales/ne/translation.json +0 -340
  193. package/src/RedactoPrivacyCenter/locales/or/translation.json +0 -340
  194. package/src/RedactoPrivacyCenter/locales/pa/translation.json +0 -340
  195. package/src/RedactoPrivacyCenter/locales/sa/translation.json +0 -340
  196. package/src/RedactoPrivacyCenter/locales/sat/translation.json +0 -340
  197. package/src/RedactoPrivacyCenter/locales/sd/translation.json +0 -340
  198. package/src/RedactoPrivacyCenter/locales/ta/translation.json +0 -340
  199. package/src/RedactoPrivacyCenter/locales/te/translation.json +0 -340
  200. package/src/RedactoPrivacyCenter/locales/ur/translation.json +0 -340
  201. package/src/RedactoPrivacyCenter/styles/applyBrandingTheme.ts +0 -66
  202. package/src/RedactoPrivacyCenter/styles/base.css +0 -117
  203. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +0 -3108
  204. package/src/RedactoPrivacyCenter/styles/pcStyles.ts +0 -831
  205. package/src/RedactoPrivacyCenter/styles/responsive.ts +0 -138
  206. package/src/RedactoPrivacyCenter/styles/variables.css +0 -96
  207. package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +0 -387
  208. package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +0 -468
  209. package/src/RedactoPrivacyCenter/table/index.ts +0 -2
  210. package/src/RedactoPrivacyCenter/types/images.d.ts +0 -29
  211. package/src/RedactoPrivacyCenter/ui/Footer.tsx +0 -69
  212. package/src/RedactoPrivacyCenter/ui/FormBlockWrapper.tsx +0 -54
  213. package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +0 -93
  214. package/src/RedactoPrivacyCenter/ui/PCActionIcon.tsx +0 -47
  215. package/src/RedactoPrivacyCenter/ui/PCAvatar.test.tsx +0 -83
  216. package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +0 -93
  217. package/src/RedactoPrivacyCenter/ui/PCBadge.tsx +0 -48
  218. package/src/RedactoPrivacyCenter/ui/PCButton.tsx +0 -77
  219. package/src/RedactoPrivacyCenter/ui/PCCheckbox.tsx +0 -55
  220. package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +0 -107
  221. package/src/RedactoPrivacyCenter/ui/PCEmpty.tsx +0 -35
  222. package/src/RedactoPrivacyCenter/ui/PCInput.tsx +0 -132
  223. package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +0 -31
  224. package/src/RedactoPrivacyCenter/ui/PCModal.tsx +0 -106
  225. package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +0 -206
  226. package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +0 -433
  227. package/src/RedactoPrivacyCenter/ui/PCTable.tsx +0 -149
  228. package/src/RedactoPrivacyCenter/ui/PCText.tsx +0 -41
  229. package/src/RedactoPrivacyCenter/ui/PCTextarea.tsx +0 -43
  230. package/src/RedactoPrivacyCenter/ui/PCTitle.tsx +0 -37
  231. package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +0 -121
  232. package/src/RedactoPrivacyCenter/ui/SimpleBar.tsx +0 -25
  233. package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +0 -53
  234. package/src/RedactoPrivacyCenter/ui/index.ts +0 -18
  235. package/src/RedactoPrivacyCenter/utils/cn.ts +0 -28
  236. package/src/RedactoPrivacyCenter/utils/date.ts +0 -48
  237. package/src/RedactoPrivacyCenter/utils/status.ts +0 -111
  238. package/src/RedactoPrivacyCenter/utils/toast.ts +0 -130
  239. package/src/index.ts +0 -19
  240. package/src/privacy-center.ts +0 -113
  241. package/src/shared/api-errors.ts +0 -180
  242. package/src/shared/notice-consent-status.ts +0 -190
  243. package/src/shared/sandbox.ts +0 -6
  244. package/src/types/css.d.ts +0 -4
  245. package/src/types/images.d.ts +0 -19
  246. package/src/types/react-jsx.d.ts +0 -26
  247. package/tests/DataAvailabilityContext.test.tsx +0 -123
  248. package/tests/Form.test.tsx +0 -1424
  249. package/tests/ProductSelectionSection.test.tsx +0 -328
  250. package/tests/RequestSubmit.test.tsx +0 -135
  251. package/tests/RevokeWarningMessage.test.tsx +0 -151
  252. package/tests/consentClient.test.ts +0 -217
  253. package/tests/manageConsentServer.test.ts +0 -38
  254. package/tests/mocks.ts +0 -257
  255. package/tests/resolveContactFromJwt.test.ts +0 -44
  256. package/tests/setup.ts +0 -16
  257. package/tsconfig.json +0 -16
  258. package/tsup.config.ts +0 -31
  259. package/vitest.config.ts +0 -16
@@ -1,934 +0,0 @@
1
- import "@testing-library/jest-dom";
2
- import {
3
- fireEvent,
4
- render,
5
- screen,
6
- waitFor,
7
- within,
8
- } from "@testing-library/react";
9
- import { beforeEach, describe, expect, it, vi } from "vitest";
10
- import { RedactoNoticeAssisted } from "./RedactoNoticeAssisted";
11
- import { ASSISTED_CONFIG } from "./constants";
12
- import {
13
- fetchAssistedNotice,
14
- fetchAssistedTTSAudioUrls,
15
- createOtp,
16
- verifyOtp,
17
- submitAssistedConsent,
18
- } from "./api";
19
- import type { ConsentContent, TTSAudioUrlsResponse } from "./api/types";
20
- import { ApiError } from "../shared/api-errors";
21
- import { fetchNoticeConsentStatusForScope } from "../shared/notice-consent-status";
22
- import { makeStaticT } from "./i18n";
23
-
24
- vi.mock("./api", () => ({
25
- fetchAssistedNotice: vi.fn(),
26
- fetchAssistedTTSAudioUrls: vi.fn(),
27
- createOtp: vi.fn(),
28
- verifyOtp: vi.fn(),
29
- submitAssistedConsent: vi.fn(),
30
- }));
31
-
32
- // Mock only the ledger check-consent network read; keep the real
33
- // isNoticeFullyConsented predicate so the already-consented decision is exercised.
34
- vi.mock("../shared/notice-consent-status", async (importActual) => {
35
- const actual =
36
- await importActual<typeof import("../shared/notice-consent-status")>();
37
- return {
38
- ...actual,
39
- fetchNoticeConsentStatusForScope: vi.fn(),
40
- };
41
- });
42
-
43
- const LEDGER_BASE_URL = "https://ledger.example.test/consent-ledger";
44
-
45
- const otpLength = ASSISTED_CONFIG.otpLength;
46
- const mockedFetch = vi.mocked(fetchAssistedNotice);
47
- const mockedFetchTTS = vi.mocked(fetchAssistedTTSAudioUrls);
48
- const mockedCreateOtp = vi.mocked(createOtp);
49
- const mockedVerifyOtp = vi.mocked(verifyOtp);
50
- const mockedSubmitConsent = vi.mocked(submitAssistedConsent);
51
- const mockedFetchScope = vi.mocked(fetchNoticeConsentStatusForScope);
52
-
53
- const OTP_DETAIL = {
54
- uuid: "otp-1",
55
- to: "+919876543210",
56
- channel: "sms",
57
- expire_at: "2026-06-24T09:55:34Z",
58
- success: true,
59
- message: "OTP created successfully",
60
- };
61
-
62
- const VERIFY_DETAIL = {
63
- token: null,
64
- access_token: "access-token-1",
65
- refresh_token: "refresh-token-1",
66
- expires_in: 3600,
67
- token_type: "Bearer",
68
- success: true,
69
- message: "OTP verified successfully",
70
- };
71
-
72
- const PROPS = {
73
- organisationUuid: "org-1",
74
- workspaceUuid: "ws-1",
75
- noticeUuid: "ntc-1",
76
- baseUrl: "https://api.example.test/consent",
77
- };
78
-
79
- // Trimmed EA-Sports notice payload (shape of the public API response).
80
- const NOTICE = {
81
- detail: {
82
- active_config: {
83
- notice_text: "We use data to personalise your experience.",
84
- additional_text: "Manage or withdraw consent anytime at",
85
- confirm_button_text: "Agree",
86
- decline_button_text: "Decline",
87
- logo_url: "https://cdn.example.test/ea.svg",
88
- privacy_policy_url: "https://www.ea.com/legal/privacy",
89
- privacy_center_url: "https://privacy.example.test/",
90
- primary_color: "#9c0716",
91
- secondary_color: "#000bdb",
92
- font_preference: "Arial",
93
- privacy_policy_prefix_text: "Learn more in our",
94
- privacy_policy_anchor_text: "Privacy Policy",
95
- privacy_center_anchor_text: "Privacy Center",
96
- purpose_section_heading: "Manage What You Share",
97
- notice_banner_heading: "EA Sports",
98
- default_language: "English",
99
- purposes: [
100
- {
101
- uuid: "p-account",
102
- name: "Account Management",
103
- description: "To maintain and update your account information.",
104
- data_elements: [
105
- { uuid: "e-email", name: "Email", enabled: true, required: true },
106
- {
107
- uuid: "e-phone",
108
- name: "Phone Number",
109
- enabled: true,
110
- required: false,
111
- },
112
- ],
113
- },
114
- {
115
- uuid: "p-identity",
116
- name: "Identity Verification",
117
- description: "To authenticate your identity securely.",
118
- data_elements: [
119
- {
120
- uuid: "e-govid",
121
- name: "Government ID",
122
- enabled: true,
123
- required: true,
124
- },
125
- ],
126
- },
127
- ],
128
- dpo_info: {
129
- grievance_text: "If you have any grievances",
130
- grievance_url: "https://google.com",
131
- grievance_anchor_text: "click here",
132
- grievance_email_connector_text: "or email to",
133
- grievance_email: "help@redacto.io",
134
- dp_board_text: "You can escalate to the DP board",
135
- dp_board_url: "https://google.com",
136
- dp_board_anchor_text: "click here",
137
- dpo_text: "To connect with the DPO",
138
- dpo_email: "sadan@redacto.ai",
139
- dpo_anchor_text: "Click here",
140
- },
141
- supported_languages_and_translations: {
142
- Hindi: {
143
- notice_banner_heading: "ईए स्पोर्ट्स",
144
- notice_text: "हम डेटा का उपयोग करते हैं।",
145
- purpose_section_heading: "आप जो साझा करते हैं उसे प्रबंधित करें",
146
- purposes: {
147
- "p-account": { name: "खाता प्रबंधन", description: "..." },
148
- },
149
- data_elements: { "e-email": "ईमेल" },
150
- },
151
- },
152
- },
153
- },
154
- } as unknown as ConsentContent;
155
-
156
- // Notice variant carrying a notice_uuid — the audio effect only fires when one is
157
- // present, so the base NOTICE (no uuid) leaves TTS disabled.
158
- const NOTICE_WITH_AUDIO = {
159
- detail: {
160
- active_config: {
161
- ...NOTICE.detail.active_config,
162
- notice_uuid: "ntc-1",
163
- },
164
- },
165
- } as unknown as ConsentContent;
166
-
167
- // Minimal audio response in the new endpoint shape (incl. language_name +
168
- // full_notice_audio_url). Only a couple of clip URLs are needed to exercise the
169
- // player; the sequence builder skips any missing ones.
170
- const TTS_RESPONSE = {
171
- code: 200,
172
- status: "OK",
173
- detail: {
174
- language_name: "English",
175
- uuid: "tts-1",
176
- language: "en",
177
- notice_text_audio_url: "https://cdn.example.test/notice.mp3",
178
- additional_text_audio_url: "https://cdn.example.test/additional.mp3",
179
- notice_summary_audio_url: null,
180
- confirm_button_text_audio_url: "https://cdn.example.test/confirm.mp3",
181
- decline_button_text_audio_url: "https://cdn.example.test/decline.mp3",
182
- privacy_policy_prefix_text_audio_url: "https://cdn.example.test/pp-prefix.mp3",
183
- privacy_policy_anchor_text_audio_url: "https://cdn.example.test/pp-anchor.mp3",
184
- privacy_center_anchor_text_audio_url: "https://cdn.example.test/pc.mp3",
185
- purpose_section_heading_audio_url: "https://cdn.example.test/section.mp3",
186
- notice_banner_heading_audio_url: "https://cdn.example.test/banner.mp3",
187
- purposes_audio: {
188
- "p-account": {
189
- name_audio_url: "https://cdn.example.test/p-account-name.mp3",
190
- description_audio_url: "https://cdn.example.test/p-account-desc.mp3",
191
- },
192
- },
193
- data_elements_audio: {
194
- "e-email": { name_audio_url: "https://cdn.example.test/e-email.mp3" },
195
- },
196
- dpo_info_audio: {
197
- dpo_text_audio_url: "https://cdn.example.test/dpo.mp3",
198
- },
199
- full_notice_audio_url: "https://cdn.example.test/full.mp3",
200
- },
201
- } as unknown as TTSAudioUrlsResponse;
202
-
203
- const typeOtp = (value: string) => {
204
- const boxes = screen.getAllByLabelText(/OTP digit/);
205
- expect(boxes).toHaveLength(otpLength);
206
- value.split("").forEach((digit, i) => {
207
- fireEvent.change(boxes[i], { target: { value: digit } });
208
- });
209
- };
210
-
211
- const renderLoaded = async () => {
212
- const result = render(<RedactoNoticeAssisted {...PROPS} />);
213
- await screen.findByRole("heading", { name: "EA Sports" });
214
- return result;
215
- };
216
-
217
- // Ticking both purposes selects their required data elements (Email, Government
218
- // ID), which the Confirm CTA now requires before the verify step can open.
219
- const selectRequired = () => {
220
- fireEvent.click(
221
- screen.getByRole("checkbox", {
222
- name: /Select all data elements for Account Management/,
223
- }),
224
- );
225
- fireEvent.click(
226
- screen.getByRole("checkbox", {
227
- name: /Select all data elements for Identity Verification/,
228
- }),
229
- );
230
- };
231
-
232
- describe("RedactoNoticeAssisted", () => {
233
- beforeEach(() => {
234
- mockedFetch.mockReset();
235
- mockedFetchTTS.mockReset();
236
- mockedCreateOtp.mockReset();
237
- mockedVerifyOtp.mockReset();
238
- mockedSubmitConsent.mockReset();
239
- mockedFetch.mockResolvedValue(NOTICE);
240
- mockedFetchTTS.mockResolvedValue(TTS_RESPONSE);
241
- mockedCreateOtp.mockResolvedValue(OTP_DETAIL);
242
- mockedVerifyOtp.mockResolvedValue(VERIFY_DETAIL);
243
- mockedSubmitConsent.mockResolvedValue(null);
244
- // Default: ledger reports no prior full consent, so flows proceed to submit.
245
- mockedFetchScope.mockReset();
246
- mockedFetchScope.mockResolvedValue(null);
247
- });
248
-
249
- it("fetches with the public-notice props (no auth)", async () => {
250
- await renderLoaded();
251
- expect(mockedFetch).toHaveBeenCalledWith(
252
- expect.objectContaining({
253
- organisationUuid: "org-1",
254
- workspaceUuid: "ws-1",
255
- noticeUuid: "ntc-1",
256
- baseUrl: "https://api.example.test/consent",
257
- }),
258
- );
259
- });
260
-
261
- it("renders the fetched notice (logo, title, purposes, footer)", async () => {
262
- await renderLoaded();
263
- expect(screen.getByAltText("Logo")).toHaveAttribute(
264
- "src",
265
- "https://cdn.example.test/ea.svg",
266
- );
267
- expect(
268
- screen.getByRole("link", { name: /Privacy Policy/ }),
269
- ).toHaveAttribute("href", "https://www.ea.com/legal/privacy");
270
- expect(
271
- screen.getByRole("heading", { name: "Manage What You Share" }),
272
- ).toBeInTheDocument();
273
- expect(screen.getByText("Account Management")).toBeInTheDocument();
274
- expect(screen.getByText("Identity Verification")).toBeInTheDocument();
275
- expect(screen.getByRole("button", { name: "Agree" })).toBeInTheDocument();
276
- expect(screen.getByRole("button", { name: "Decline" })).toBeInTheDocument();
277
- });
278
-
279
- it("keeps Confirm disabled until every required data element is selected", async () => {
280
- await renderLoaded();
281
- const confirm = screen.getByRole("button", { name: "Agree" });
282
- // Required elements (Email, Government ID) start unchecked → CTA disabled.
283
- expect(confirm).toBeDisabled();
284
-
285
- // Selecting only one purpose still leaves a required element unchecked.
286
- fireEvent.click(
287
- screen.getByRole("checkbox", {
288
- name: /Select all data elements for Account Management/,
289
- }),
290
- );
291
- expect(confirm).toBeDisabled();
292
-
293
- // Once both required elements are selected, the CTA enables.
294
- fireEvent.click(
295
- screen.getByRole("checkbox", {
296
- name: /Select all data elements for Identity Verification/,
297
- }),
298
- );
299
- expect(confirm).toBeEnabled();
300
- });
301
-
302
- it("checks the parent purpose once its required element is selected", async () => {
303
- await renderLoaded();
304
- // Expand Account Management (Email = required, Phone = optional).
305
- fireEvent.click(screen.getByRole("button", { name: /Account Management/ }));
306
- const purpose = screen.getByRole("checkbox", {
307
- name: "Select all data elements for Account Management",
308
- });
309
- const email = screen.getByRole("checkbox", {
310
- name: "Select Email (required)",
311
- });
312
- expect(purpose).not.toBeChecked();
313
- // Email is the only required element → ticking it checks the purpose.
314
- fireEvent.click(email);
315
- expect(email).toBeChecked();
316
- expect(purpose).toBeChecked();
317
- });
318
-
319
- it("unchecks the parent purpose when a required element is deselected", async () => {
320
- await renderLoaded();
321
- fireEvent.click(screen.getByRole("button", { name: /Account Management/ }));
322
- const purpose = screen.getByRole("checkbox", {
323
- name: "Select all data elements for Account Management",
324
- });
325
- const email = screen.getByRole("checkbox", {
326
- name: "Select Email (required)",
327
- });
328
- // Ticking the purpose selects every element.
329
- fireEvent.click(purpose);
330
- expect(purpose).toBeChecked();
331
- expect(email).toBeChecked();
332
- // Removing the required Email element drops the purpose.
333
- fireEvent.click(email);
334
- expect(email).not.toBeChecked();
335
- expect(purpose).not.toBeChecked();
336
- });
337
-
338
- it("selects and clears every data element when the purpose is toggled", async () => {
339
- await renderLoaded();
340
- fireEvent.click(screen.getByRole("button", { name: /Account Management/ }));
341
- const purpose = screen.getByRole("checkbox", {
342
- name: "Select all data elements for Account Management",
343
- });
344
- const email = screen.getByRole("checkbox", {
345
- name: "Select Email (required)",
346
- });
347
- const phone = screen.getByRole("checkbox", {
348
- name: "Select Phone Number",
349
- });
350
- fireEvent.click(purpose);
351
- expect(email).toBeChecked();
352
- expect(phone).toBeChecked();
353
- fireEvent.click(purpose);
354
- expect(email).not.toBeChecked();
355
- expect(phone).not.toBeChecked();
356
- });
357
-
358
- it("lists the API languages in the dropdown", async () => {
359
- await renderLoaded();
360
- fireEvent.click(screen.getByRole("button", { name: /Select language/ }));
361
- const list = screen.getByRole("listbox");
362
- expect(
363
- within(list).getByRole("option", { name: "English" }),
364
- ).toBeInTheDocument();
365
- expect(
366
- within(list).getByRole("option", { name: /Hindi/ }),
367
- ).toBeInTheDocument();
368
- });
369
-
370
- it("renders a non-English default_language from the translations", async () => {
371
- // default_language points at Hindi; the base active_config fields are English,
372
- // so the Hindi copy must come from supported_languages_and_translations.
373
- const hindiDefault = {
374
- detail: {
375
- active_config: {
376
- ...NOTICE.detail.active_config,
377
- default_language: "Hindi",
378
- },
379
- },
380
- } as unknown as ConsentContent;
381
- mockedFetch.mockResolvedValue(hindiDefault);
382
-
383
- render(<RedactoNoticeAssisted {...PROPS} />);
384
- // Heading + notice text render in Hindi, not the English base copy.
385
- expect(
386
- await screen.findByRole("heading", { name: "ईए स्पोर्ट्स" }),
387
- ).toBeInTheDocument();
388
- expect(screen.getByText("हम डेटा का उपयोग करते हैं।")).toBeInTheDocument();
389
- expect(
390
- screen.queryByText("We use data to personalise your experience."),
391
- ).not.toBeInTheDocument();
392
- });
393
-
394
- it("expands a purpose accordion to reveal its data elements", async () => {
395
- await renderLoaded();
396
- expect(screen.queryByText("Email")).not.toBeInTheDocument();
397
- fireEvent.click(screen.getByRole("button", { name: /Account Management/ }));
398
- expect(screen.getByText("Email")).toBeInTheDocument();
399
- expect(screen.getByText("Phone Number")).toBeInTheDocument();
400
- });
401
-
402
- it("walks the single-screen flow: Confirm → mobile → Send OTP → Confirm OTP → onComplete", async () => {
403
- const onComplete = vi.fn();
404
- render(<RedactoNoticeAssisted {...PROPS} onComplete={onComplete} />);
405
- await screen.findByRole("heading", { name: "EA Sports" });
406
-
407
- // The notice stays mounted throughout; clicking Confirm reveals the inline
408
- // verify panel below it. (API confirm label is "Agree" in this fixture.)
409
- selectRequired();
410
- fireEvent.click(screen.getByRole("button", { name: "Agree" }));
411
- expect(
412
- screen.getByRole("heading", { name: "Manage What You Share" }),
413
- ).toBeInTheDocument();
414
- const mobileInput = screen.getByLabelText("Mobile number");
415
- expect(screen.queryByLabelText(/OTP digit/)).not.toBeInTheDocument();
416
-
417
- const sendOtpBtn = screen.getByRole("button", { name: "Send OTP" });
418
- expect(sendOtpBtn).toBeDisabled();
419
- fireEvent.change(mobileInput, { target: { value: "9876543210" } });
420
- expect(sendOtpBtn).toBeEnabled();
421
-
422
- fireEvent.click(sendOtpBtn);
423
- // Create-OTP is called with the E.164 recipient + sms channel.
424
- await waitFor(() =>
425
- expect(mockedCreateOtp).toHaveBeenCalledWith(
426
- expect.objectContaining({ to: "+919876543210", channel: "sms" }),
427
- ),
428
- );
429
- expect(await screen.findByText(/OTP sent to/)).toHaveTextContent(
430
- "OTP sent to +91 9876543210",
431
- );
432
- expect(
433
- screen.getByRole("button", { name: "Change Mobile Number" }),
434
- ).toBeInTheDocument();
435
-
436
- const confirmOtpBtn = screen.getByRole("button", { name: "Confirm OTP" });
437
- expect(confirmOtpBtn).toBeDisabled();
438
- typeOtp("1".repeat(otpLength));
439
- expect(confirmOtpBtn).toBeEnabled();
440
-
441
- fireEvent.click(confirmOtpBtn);
442
- // Verify is called with the create-OTP uuid, then consent is submitted with
443
- // the access_token from verify.
444
- await waitFor(() =>
445
- expect(mockedVerifyOtp).toHaveBeenCalledWith(
446
- expect.objectContaining({
447
- uuid: "otp-1",
448
- otp: "1".repeat(otpLength),
449
- purpose: "login",
450
- }),
451
- ),
452
- );
453
- expect(mockedSubmitConsent).toHaveBeenCalledWith(
454
- expect.objectContaining({
455
- accessToken: "access-token-1",
456
- organisationUuid: "org-1",
457
- workspaceUuid: "ws-1",
458
- }),
459
- );
460
- // No confirmation screen — onComplete fires immediately on success.
461
- await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(1));
462
- expect(
463
- screen.queryByRole("heading", { name: "Consent confirmed" }),
464
- ).not.toBeInTheDocument();
465
- });
466
-
467
- it("verifies by email: Confirm → Email toggle → Send OTP (channel email) → Confirm OTP → onComplete", async () => {
468
- const onComplete = vi.fn();
469
- render(<RedactoNoticeAssisted {...PROPS} onComplete={onComplete} />);
470
- await screen.findByRole("heading", { name: "EA Sports" });
471
-
472
- selectRequired();
473
- fireEvent.click(screen.getByRole("button", { name: "Agree" }));
474
-
475
- // Mobile is the default; its field is shown. Switch to email via the link.
476
- expect(screen.getByLabelText("Mobile number")).toBeInTheDocument();
477
- fireEvent.click(screen.getByRole("button", { name: /Use email instead/ }));
478
-
479
- // The mobile field is replaced by the email field.
480
- expect(screen.queryByLabelText("Mobile number")).not.toBeInTheDocument();
481
- const emailInput = screen.getByLabelText("Email address");
482
-
483
- const sendOtpBtn = screen.getByRole("button", { name: "Send OTP" });
484
- // An invalid address keeps Send OTP disabled; a valid one enables it.
485
- fireEvent.change(emailInput, { target: { value: "not-an-email" } });
486
- expect(sendOtpBtn).toBeDisabled();
487
- fireEvent.change(emailInput, { target: { value: "user@example.com" } });
488
- expect(sendOtpBtn).toBeEnabled();
489
-
490
- fireEvent.click(sendOtpBtn);
491
- // Create-OTP is called with the raw email recipient + the "email" channel.
492
- await waitFor(() =>
493
- expect(mockedCreateOtp).toHaveBeenCalledWith(
494
- expect.objectContaining({ to: "user@example.com", channel: "email" }),
495
- ),
496
- );
497
- expect(await screen.findByText(/OTP sent to/)).toHaveTextContent(
498
- "OTP sent to user@example.com",
499
- );
500
- expect(
501
- screen.getByRole("button", { name: "Change Email" }),
502
- ).toBeInTheDocument();
503
-
504
- const confirmOtpBtn = screen.getByRole("button", { name: "Confirm OTP" });
505
- expect(confirmOtpBtn).toBeDisabled();
506
- typeOtp("1".repeat(otpLength));
507
- expect(confirmOtpBtn).toBeEnabled();
508
-
509
- fireEvent.click(confirmOtpBtn);
510
- await waitFor(() =>
511
- expect(mockedVerifyOtp).toHaveBeenCalledWith(
512
- expect.objectContaining({
513
- uuid: "otp-1",
514
- otp: "1".repeat(otpLength),
515
- purpose: "login",
516
- }),
517
- ),
518
- );
519
- expect(mockedSubmitConsent).toHaveBeenCalledWith(
520
- expect.objectContaining({
521
- accessToken: "access-token-1",
522
- organisationUuid: "org-1",
523
- workspaceUuid: "ws-1",
524
- }),
525
- );
526
- await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(1));
527
- });
528
-
529
- it("locks the notice selections once Confirm is pressed", async () => {
530
- await renderLoaded();
531
- // Editable before Confirm.
532
- const accountCheckbox = screen.getByRole("checkbox", {
533
- name: /Select all data elements for Account Management/,
534
- });
535
- expect(accountCheckbox).toBeEnabled();
536
- selectRequired();
537
- fireEvent.click(screen.getByRole("button", { name: "Agree" }));
538
- // Locked after Confirm; the notice is still visible.
539
- expect(accountCheckbox).toBeDisabled();
540
- expect(screen.getByText("Account Management")).toBeInTheDocument();
541
- });
542
-
543
- it("reopens the notice for editing via Edit selections", async () => {
544
- await renderLoaded();
545
- selectRequired();
546
- fireEvent.click(screen.getByRole("button", { name: "Agree" }));
547
- expect(screen.getByLabelText("Mobile number")).toBeInTheDocument();
548
- fireEvent.click(screen.getByRole("button", { name: "Edit selections" }));
549
- // Back on the notice step: panel gone, CTA reverts, selections editable.
550
- expect(screen.queryByLabelText("Mobile number")).not.toBeInTheDocument();
551
- expect(screen.getByRole("button", { name: "Agree" })).toBeInTheDocument();
552
- expect(
553
- screen.getByRole("checkbox", {
554
- name: /Select all data elements for Account Management/,
555
- }),
556
- ).toBeEnabled();
557
- });
558
-
559
- it("returns to the mobile field via Change Mobile Number", async () => {
560
- await renderLoaded();
561
- selectRequired();
562
- fireEvent.click(screen.getByRole("button", { name: "Agree" }));
563
- fireEvent.change(screen.getByLabelText("Mobile number"), {
564
- target: { value: "9876543210" },
565
- });
566
- fireEvent.click(screen.getByRole("button", { name: "Send OTP" }));
567
- await screen.findByText(/OTP sent to/);
568
- typeOtp("12");
569
- fireEvent.click(
570
- screen.getByRole("button", { name: "Change Mobile Number" }),
571
- );
572
- expect(screen.queryByLabelText(/OTP digit/)).not.toBeInTheDocument();
573
- expect(screen.getByLabelText("Mobile number")).toBeEnabled();
574
- expect(
575
- screen.getByRole("button", { name: "Send OTP" }),
576
- ).toBeInTheDocument();
577
- });
578
-
579
- it("shows a localized inline error when create-OTP fails", async () => {
580
- mockedCreateOtp.mockRejectedValue(new Error("SMS service unavailable"));
581
- await renderLoaded();
582
- selectRequired();
583
- fireEvent.click(screen.getByRole("button", { name: "Agree" }));
584
- fireEvent.change(screen.getByLabelText("Mobile number"), {
585
- target: { value: "9876543210" },
586
- });
587
- fireEvent.click(screen.getByRole("button", { name: "Send OTP" }));
588
- // Server copy is English-only, so the SDK shows a localized generic message.
589
- expect(
590
- await screen.findByText("Could not send the OTP. Please try again."),
591
- ).toBeInTheDocument();
592
- // Stays on the mobile-entry step (no OTP boxes).
593
- expect(screen.queryByLabelText(/OTP digit/)).not.toBeInTheDocument();
594
- });
595
-
596
- it("treats an already-consented 409 as success (no error)", async () => {
597
- mockedSubmitConsent.mockRejectedValue(
598
- new ApiError({
599
- status: 409,
600
- code: "CONSENT_ALREADY_PROVIDED",
601
- message: "You have already provided consent for this notice.",
602
- }),
603
- );
604
- const onComplete = vi.fn();
605
- render(<RedactoNoticeAssisted {...PROPS} onComplete={onComplete} />);
606
- await screen.findByRole("heading", { name: "EA Sports" });
607
-
608
- selectRequired();
609
- fireEvent.click(screen.getByRole("button", { name: "Agree" }));
610
- fireEvent.change(screen.getByLabelText("Mobile number"), {
611
- target: { value: "9876543210" },
612
- });
613
- fireEvent.click(screen.getByRole("button", { name: "Send OTP" }));
614
- await screen.findByText(/OTP sent to/);
615
- typeOtp("1".repeat(otpLength));
616
- fireEvent.click(screen.getByRole("button", { name: "Confirm OTP" }));
617
-
618
- // Treated as success: onComplete fires and the 409 message is never shown.
619
- await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(1));
620
- expect(
621
- screen.queryByText("You have already provided consent for this notice."),
622
- ).not.toBeInTheDocument();
623
- });
624
-
625
- // ── Go ledger (ledgerBaseUrl) ─────────────────────────────────────────────
626
- // Drives the full notice → OTP → Confirm flow with a notice that carries a
627
- // notice_uuid (so the check-consent read runs), and returns the onComplete spy.
628
- const runConfirmFlow = async (extraProps: Record<string, unknown>) => {
629
- mockedFetch.mockResolvedValue(NOTICE_WITH_AUDIO);
630
- const onComplete = vi.fn();
631
- render(
632
- <RedactoNoticeAssisted {...PROPS} onComplete={onComplete} {...extraProps} />,
633
- );
634
- await screen.findByRole("heading", { name: "EA Sports" });
635
- selectRequired();
636
- fireEvent.click(screen.getByRole("button", { name: "Agree" }));
637
- fireEvent.change(screen.getByLabelText("Mobile number"), {
638
- target: { value: "9876543210" },
639
- });
640
- fireEvent.click(screen.getByRole("button", { name: "Send OTP" }));
641
- await screen.findByText(/OTP sent to/);
642
- typeOtp("1".repeat(otpLength));
643
- fireEvent.click(screen.getByRole("button", { name: "Confirm OTP" }));
644
- return onComplete;
645
- };
646
-
647
- it("routes the write through ledgerBaseUrl and submits when not already consented", async () => {
648
- const onComplete = await runConfirmFlow({ ledgerBaseUrl: LEDGER_BASE_URL });
649
-
650
- await waitFor(() => expect(mockedSubmitConsent).toHaveBeenCalledTimes(1));
651
- // The check-consent read runs with props org/workspace + the OTP token.
652
- expect(mockedFetchScope).toHaveBeenCalledWith(
653
- expect.objectContaining({
654
- accessToken: "access-token-1",
655
- ledgerBaseUrl: LEDGER_BASE_URL,
656
- organisationUuid: "org-1",
657
- workspaceUuid: "ws-1",
658
- noticeUuid: "ntc-1",
659
- }),
660
- );
661
- // The submit carries ledgerBaseUrl so the api layer routes it to the ledger.
662
- expect(mockedSubmitConsent).toHaveBeenCalledWith(
663
- expect.objectContaining({ ledgerBaseUrl: LEDGER_BASE_URL }),
664
- );
665
- await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(1));
666
- });
667
-
668
- it("short-circuits an already fully-consented principal via the ledger (no submit)", async () => {
669
- mockedFetchScope.mockResolvedValue({
670
- consented: true,
671
- all_mandatory_active: true,
672
- purposes: [],
673
- });
674
- const onComplete = await runConfirmFlow({ ledgerBaseUrl: LEDGER_BASE_URL });
675
-
676
- await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(1));
677
- expect(mockedSubmitConsent).not.toHaveBeenCalled();
678
- });
679
-
680
- it("skips the ledger check-consent and stays on baseUrl when ledgerBaseUrl is absent", async () => {
681
- const onComplete = await runConfirmFlow({});
682
-
683
- await waitFor(() => expect(mockedSubmitConsent).toHaveBeenCalledTimes(1));
684
- expect(mockedFetchScope).not.toHaveBeenCalled();
685
- expect(mockedSubmitConsent).toHaveBeenCalledWith(
686
- expect.objectContaining({
687
- ledgerBaseUrl: undefined,
688
- baseUrl: "https://api.example.test/consent",
689
- }),
690
- );
691
- await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(1));
692
- });
693
-
694
- // The verify endpoint returns English copy with no error_code, so the SDK
695
- // localizes by HTTP status + the `should_request_new` flag instead of echoing
696
- // the server message.
697
- const submitVerifyAttempt = async () => {
698
- await renderLoaded();
699
- selectRequired();
700
- fireEvent.click(screen.getByRole("button", { name: "Agree" }));
701
- fireEvent.change(screen.getByLabelText("Mobile number"), {
702
- target: { value: "9876543210" },
703
- });
704
- fireEvent.click(screen.getByRole("button", { name: "Send OTP" }));
705
- await screen.findByText(/OTP sent to/);
706
- typeOtp("1".repeat(otpLength));
707
- fireEvent.click(screen.getByRole("button", { name: "Confirm OTP" }));
708
- };
709
-
710
- it("shows a localized 'incorrect code' error on a 400 verify failure", async () => {
711
- mockedVerifyOtp.mockRejectedValue(
712
- new ApiError({ status: 400, message: "incorrect (server copy)" }),
713
- );
714
- await submitVerifyAttempt();
715
- expect(
716
- await screen.findByText(
717
- "The verification code you entered is incorrect. Please check and try again.",
718
- ),
719
- ).toBeInTheDocument();
720
- // Stays on the OTP step; consent is never submitted.
721
- expect(mockedSubmitConsent).not.toHaveBeenCalled();
722
- });
723
-
724
- it("shows a localized 'expired code' error on 400 with should_request_new", async () => {
725
- mockedVerifyOtp.mockRejectedValue(
726
- new ApiError({
727
- status: 400,
728
- message: "expired (server copy)",
729
- shouldRequestNew: true,
730
- }),
731
- );
732
- await submitVerifyAttempt();
733
- expect(
734
- await screen.findByText(
735
- "Your verification code has expired. Please request a new code.",
736
- ),
737
- ).toBeInTheDocument();
738
- expect(mockedSubmitConsent).not.toHaveBeenCalled();
739
- });
740
-
741
- it("shows a localized 'not found' error on a 404 verify failure", async () => {
742
- mockedVerifyOtp.mockRejectedValue(
743
- new ApiError({ status: 404, message: "not found (server copy)" }),
744
- );
745
- await submitVerifyAttempt();
746
- expect(
747
- await screen.findByText(
748
- "Verification code not found. Please request a new code.",
749
- ),
750
- ).toBeInTheDocument();
751
- expect(mockedSubmitConsent).not.toHaveBeenCalled();
752
- });
753
-
754
- // ── Audio narration (talkback) ────────────────────────────────────────────
755
- describe("audio narration", () => {
756
- // jsdom doesn't implement media playback — stub play/pause so the player runs.
757
- beforeEach(() => {
758
- window.HTMLMediaElement.prototype.play = vi
759
- .fn()
760
- .mockResolvedValue(undefined);
761
- window.HTMLMediaElement.prototype.pause = vi.fn();
762
- });
763
-
764
- const renderWithAudio = async () => {
765
- mockedFetch.mockResolvedValue(NOTICE_WITH_AUDIO);
766
- const result = render(<RedactoNoticeAssisted {...PROPS} />);
767
- await screen.findByRole("heading", { name: "EA Sports" });
768
- return result;
769
- };
770
-
771
- it("does not fetch audio or show the speaker without a notice_uuid", async () => {
772
- // The base NOTICE has no notice_uuid → the audio effect returns early.
773
- await renderLoaded();
774
- expect(mockedFetchTTS).not.toHaveBeenCalled();
775
- expect(
776
- screen.queryByRole("button", { name: "Start audio playback" }),
777
- ).not.toBeInTheDocument();
778
- });
779
-
780
- it("fetches audio with the public props + language and shows the speaker", async () => {
781
- await renderWithAudio();
782
- await waitFor(() =>
783
- expect(mockedFetchTTS).toHaveBeenCalledWith(
784
- expect.objectContaining({
785
- organisationUuid: "org-1",
786
- workspaceUuid: "ws-1",
787
- noticeUuid: "ntc-1",
788
- language: "English",
789
- baseUrl: "https://api.example.test/consent",
790
- }),
791
- ),
792
- );
793
- expect(
794
- await screen.findByRole("button", { name: "Start audio playback" }),
795
- ).toBeInTheDocument();
796
- });
797
-
798
- it("toggles play/pause when the speaker is clicked", async () => {
799
- await renderWithAudio();
800
- const speaker = await screen.findByRole("button", {
801
- name: "Start audio playback",
802
- });
803
- fireEvent.click(speaker);
804
- // Now playing → the same control flips to a pause affordance.
805
- expect(
806
- await screen.findByRole("button", { name: "Pause audio playback" }),
807
- ).toBeInTheDocument();
808
- fireEvent.click(
809
- screen.getByRole("button", { name: "Pause audio playback" }),
810
- );
811
- expect(
812
- screen.getByRole("button", { name: "Start audio playback" }),
813
- ).toBeInTheDocument();
814
- });
815
-
816
- it("hides the speaker once the notice advances to verify", async () => {
817
- await renderWithAudio();
818
- await screen.findByRole("button", { name: "Start audio playback" });
819
- selectRequired();
820
- fireEvent.click(screen.getByRole("button", { name: "Agree" }));
821
- // Narration is notice-only — the speaker is gone on the verify step.
822
- expect(
823
- screen.queryByRole("button", { name: "Start audio playback" }),
824
- ).not.toBeInTheDocument();
825
- expect(
826
- screen.queryByRole("button", { name: "Pause audio playback" }),
827
- ).not.toBeInTheDocument();
828
- });
829
-
830
- it("refetches audio in the newly selected language", async () => {
831
- await renderWithAudio();
832
- await waitFor(() => expect(mockedFetchTTS).toHaveBeenCalled());
833
- mockedFetchTTS.mockClear();
834
- fireEvent.click(screen.getByRole("button", { name: /Select language/ }));
835
- fireEvent.click(
836
- within(screen.getByRole("listbox")).getByRole("option", {
837
- name: /Hindi/,
838
- }),
839
- );
840
- await waitFor(() =>
841
- expect(mockedFetchTTS).toHaveBeenCalledWith(
842
- expect.objectContaining({ language: "Hindi" }),
843
- ),
844
- );
845
- });
846
- });
847
-
848
- it("fires onDecline from the notice screen", async () => {
849
- const onDecline = vi.fn();
850
- render(<RedactoNoticeAssisted {...PROPS} onDecline={onDecline} />);
851
- await screen.findByRole("heading", { name: "EA Sports" });
852
- fireEvent.click(screen.getByRole("button", { name: "Decline" }));
853
- expect(onDecline).toHaveBeenCalledTimes(1);
854
- });
855
-
856
- it("shows a spinner while loading", () => {
857
- mockedFetch.mockReturnValue(new Promise(() => {}));
858
- render(<RedactoNoticeAssisted {...PROPS} />);
859
- expect(screen.getByLabelText("Loading notice")).toBeInTheDocument();
860
- });
861
-
862
- it("shows an error state when the fetch fails", async () => {
863
- mockedFetch.mockRejectedValue(new Error("Notice not found"));
864
- render(<RedactoNoticeAssisted {...PROPS} />);
865
- await waitFor(() =>
866
- expect(
867
- screen.getByText(/Couldn.t load the consent notice/),
868
- ).toBeInTheDocument(),
869
- );
870
- expect(screen.getByText("Notice not found")).toBeInTheDocument();
871
- });
872
-
873
- it("localizes the static OTP-flow strings when the language is switched", async () => {
874
- await renderLoaded();
875
- // Select required elements while labels are still English, then switch the
876
- // in-notice picker to Hindi.
877
- selectRequired();
878
- fireEvent.click(screen.getByRole("button", { name: /Select language/ }));
879
- fireEvent.click(
880
- within(screen.getByRole("listbox")).getByRole("option", {
881
- name: /Hindi/,
882
- }),
883
- );
884
-
885
- // Agree label has no Hindi API translation → falls back to English "Agree".
886
- fireEvent.click(screen.getByRole("button", { name: "Agree" }));
887
-
888
- // Static strings now render in Hindi (from the i18n dictionary).
889
- expect(screen.getByText("मोबाइल नंबर")).toBeInTheDocument();
890
- // CTA now reads the localized "Send OTP".
891
- expect(
892
- screen.getByRole("button", { name: "ओटीपी (OTP) भेजें" }),
893
- ).toBeInTheDocument();
894
- // The inline verify panel heading is localized too.
895
- expect(
896
- screen.getByText("ओटीपी (OTP) से सत्यापित करें"),
897
- ).toBeInTheDocument();
898
- // English static text is gone.
899
- expect(screen.queryByText("Mobile number")).not.toBeInTheDocument();
900
- });
901
- });
902
-
903
- describe("makeStaticT", () => {
904
- it("returns the translation for a known language and interpolates vars", () => {
905
- const t = makeStaticT("hi");
906
- expect(t("getOtp")).toBe("ओटीपी (OTP) प्राप्त करें");
907
- expect(t("enterNDigitOtp", { count: 6 })).toBe(
908
- "6-अंकों का ओटीपी (OTP) दर्ज करें",
909
- );
910
- });
911
-
912
- it("falls back to English for an unknown language code", () => {
913
- const t = makeStaticT("zz");
914
- expect(t("getOtp")).toBe("Get OTP");
915
- expect(t("otpDigit", { index: 3 })).toBe("OTP digit 3");
916
- });
917
-
918
- it("resolves the single-screen CTA + panel keys", () => {
919
- const en = makeStaticT("en");
920
- expect(en("sendOtp")).toBe("Send OTP");
921
- expect(en("confirmOtp")).toBe("Confirm OTP");
922
- expect(en("verifyWithOtp")).toBe("Verify with OTP");
923
- expect(en("editSelections")).toBe("Edit selections");
924
- // Localized (Hindi) — must not fall back to English.
925
- const hi = makeStaticT("hi");
926
- expect(hi("sendOtp")).toBe("ओटीपी (OTP) भेजें");
927
- expect(hi("confirmOtp")).toBe("ओटीपी (OTP) की पुष्टि करें");
928
- });
929
-
930
- it("leaves unmatched placeholders untouched", () => {
931
- const t = makeStaticT("en");
932
- expect(t("enterNDigitOtp")).toBe("Enter {count}-digit OTP");
933
- });
934
- });