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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/NOTICE.md +17 -0
  2. package/README.md +141 -3
  3. package/dist/chunk-FEGTW73Q.mjs +182 -0
  4. package/dist/{chunk-6A665OPR.mjs → chunk-OGL6OC3A.mjs} +4 -0
  5. package/dist/fonts.d.mts +35 -0
  6. package/dist/fonts.d.ts +35 -0
  7. package/dist/fonts.js +183 -0
  8. package/dist/fonts.mjs +10 -0
  9. package/dist/index.d.mts +41 -4
  10. package/dist/index.d.ts +41 -4
  11. package/dist/index.js +881 -416
  12. package/dist/index.mjs +502 -253
  13. package/dist/montserrat.generated-NBTECYRW.mjs +22 -0
  14. package/dist/privacy-center.d.mts +51 -11
  15. package/dist/privacy-center.d.ts +51 -11
  16. package/dist/privacy-center.js +252 -136
  17. package/dist/privacy-center.mjs +251 -137
  18. package/dist/types-6Aj7faxL.d.mts +57 -0
  19. package/dist/types-6Aj7faxL.d.ts +57 -0
  20. package/package.json +10 -1
  21. package/.turbo/turbo-build.log +0 -22
  22. package/.turbo/turbo-test.log +0 -203
  23. package/.turbo/turbo-verify-pack.log +0 -6
  24. package/CHANGELOG.md +0 -343
  25. package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.test.tsx +0 -934
  26. package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.tsx +0 -2160
  27. package/src/RedactoNoticeAssisted/api/index.ts +0 -275
  28. package/src/RedactoNoticeAssisted/api/types.ts +0 -182
  29. package/src/RedactoNoticeAssisted/assets/redacto-logo.png +0 -0
  30. package/src/RedactoNoticeAssisted/constants.ts +0 -51
  31. package/src/RedactoNoticeAssisted/i18n.ts +0 -1408
  32. package/src/RedactoNoticeAssisted/index.ts +0 -1
  33. package/src/RedactoNoticeAssisted/injectStyles.ts +0 -251
  34. package/src/RedactoNoticeAssisted/styles.ts +0 -730
  35. package/src/RedactoNoticeAssisted/types.ts +0 -26
  36. package/src/RedactoNoticeAssisted/useMediaQuery.ts +0 -36
  37. package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +0 -1152
  38. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +0 -4258
  39. package/src/RedactoNoticeConsent/api/index.ts +0 -512
  40. package/src/RedactoNoticeConsent/api/types.ts +0 -469
  41. package/src/RedactoNoticeConsent/assets/redacto-logo.png +0 -0
  42. package/src/RedactoNoticeConsent/components/DigiLockerCallback.tsx +0 -602
  43. package/src/RedactoNoticeConsent/components/index.ts +0 -7
  44. package/src/RedactoNoticeConsent/constants.ts +0 -20
  45. package/src/RedactoNoticeConsent/index.ts +0 -1
  46. package/src/RedactoNoticeConsent/injectStyles.ts +0 -151
  47. package/src/RedactoNoticeConsent/language-codes.ts +0 -95
  48. package/src/RedactoNoticeConsent/services/storage.ts +0 -122
  49. package/src/RedactoNoticeConsent/styles.ts +0 -409
  50. package/src/RedactoNoticeConsent/types.ts +0 -54
  51. package/src/RedactoNoticeConsent/useMediaQuery.ts +0 -36
  52. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.test.tsx +0 -369
  53. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +0 -687
  54. package/src/RedactoNoticeConsentInline/api/index.ts +0 -106
  55. package/src/RedactoNoticeConsentInline/api/types.ts +0 -183
  56. package/src/RedactoNoticeConsentInline/assets/redacto-logo.png +0 -0
  57. package/src/RedactoNoticeConsentInline/index.ts +0 -1
  58. package/src/RedactoNoticeConsentInline/injectStyles.ts +0 -40
  59. package/src/RedactoNoticeConsentInline/styles.ts +0 -397
  60. package/src/RedactoNoticeConsentInline/types.ts +0 -51
  61. package/src/RedactoNoticeConsentInline/useMediaQuery.ts +0 -36
  62. package/src/RedactoPrivacyCenter/NotificationsPage.tsx +0 -182
  63. package/src/RedactoPrivacyCenter/PrivacyCenterActivityPage.tsx +0 -19
  64. package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +0 -156
  65. package/src/RedactoPrivacyCenter/PrivacyCenterConsentManagerPage.tsx +0 -17
  66. package/src/RedactoPrivacyCenter/PrivacyCenterFormPage.tsx +0 -79
  67. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +0 -36
  68. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +0 -177
  69. package/src/RedactoPrivacyCenter/PrivacyCenterMyReceiptPage.tsx +0 -26
  70. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +0 -58
  71. package/src/RedactoPrivacyCenter/api/actions.ts +0 -695
  72. package/src/RedactoPrivacyCenter/api/client.ts +0 -911
  73. package/src/RedactoPrivacyCenter/api/config.ts +0 -33
  74. package/src/RedactoPrivacyCenter/api/fetcher.ts +0 -260
  75. package/src/RedactoPrivacyCenter/api/orgBranding.ts +0 -16
  76. package/src/RedactoPrivacyCenter/api/upload.ts +0 -93
  77. package/src/RedactoPrivacyCenter/assets/character.svg +0 -31
  78. package/src/RedactoPrivacyCenter/assets/fileAttach.svg +0 -9
  79. package/src/RedactoPrivacyCenter/assets/greenTick.svg +0 -4
  80. package/src/RedactoPrivacyCenter/assets/mail.svg +0 -3
  81. package/src/RedactoPrivacyCenter/assets/poweredByRedacto.svg +0 -9
  82. package/src/RedactoPrivacyCenter/assets/sentImg.svg +0 -9
  83. package/src/RedactoPrivacyCenter/assets/uploadFileIcon.svg +0 -9
  84. package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +0 -252
  85. package/src/RedactoPrivacyCenter/components/ActivityTimeline.tsx +0 -239
  86. package/src/RedactoPrivacyCenter/components/AppShell/AppShell.tsx +0 -268
  87. package/src/RedactoPrivacyCenter/components/AppShell/MobileNavDrawer.tsx +0 -218
  88. package/src/RedactoPrivacyCenter/components/AppShell/NavItem.tsx +0 -205
  89. package/src/RedactoPrivacyCenter/components/AppShell/PageHeader.tsx +0 -107
  90. package/src/RedactoPrivacyCenter/components/AppShell/SidebarNav.tsx +0 -100
  91. package/src/RedactoPrivacyCenter/components/AppShell/TopAppBar.tsx +0 -339
  92. package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +0 -245
  93. package/src/RedactoPrivacyCenter/components/AppShell/hooks/useIsTruncated.ts +0 -28
  94. package/src/RedactoPrivacyCenter/components/AppShell/hooks/useKeyboardShortcut.ts +0 -40
  95. package/src/RedactoPrivacyCenter/components/AppShell/hooks/useSidebarState.ts +0 -62
  96. package/src/RedactoPrivacyCenter/components/AppShell/usePrivacyCenterExitAction.ts +0 -36
  97. package/src/RedactoPrivacyCenter/components/CaseDetails/AttachmentCard.tsx +0 -38
  98. package/src/RedactoPrivacyCenter/components/CaseDetails/ChatBubble.tsx +0 -31
  99. package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +0 -79
  100. package/src/RedactoPrivacyCenter/components/CaseDetails/FinalizedTooltipWrapper.tsx +0 -25
  101. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +0 -96
  102. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +0 -97
  103. package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +0 -156
  104. package/src/RedactoPrivacyCenter/components/CaseDetails/RequestDetailsTab.tsx +0 -171
  105. package/src/RedactoPrivacyCenter/components/CaseDetails/StatusEvent.tsx +0 -23
  106. package/src/RedactoPrivacyCenter/components/CaseDetails/useFileUpload.ts +0 -116
  107. package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +0 -43
  108. package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +0 -71
  109. package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +0 -469
  110. package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +0 -426
  111. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +0 -1206
  112. package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +0 -218
  113. package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +0 -138
  114. package/src/RedactoPrivacyCenter/components/ConsentManagerConstants.ts +0 -7
  115. package/src/RedactoPrivacyCenter/components/ConsentManagerExpandedRow.tsx +0 -179
  116. package/src/RedactoPrivacyCenter/components/ConsentManagerGroupedColumns.tsx +0 -318
  117. package/src/RedactoPrivacyCenter/components/ConsentManagerProductCards.tsx +0 -333
  118. package/src/RedactoPrivacyCenter/components/ConsentManagerPurposeCards.tsx +0 -102
  119. package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +0 -159
  120. package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +0 -189
  121. package/src/RedactoPrivacyCenter/components/DataRequest/GrievanceDataSection.tsx +0 -118
  122. package/src/RedactoPrivacyCenter/components/DataRequest/NominationDataSection.tsx +0 -66
  123. package/src/RedactoPrivacyCenter/components/DataRequest/ProductSelectionSection.tsx +0 -222
  124. package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +0 -331
  125. package/src/RedactoPrivacyCenter/components/Form.tsx +0 -1796
  126. package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +0 -174
  127. package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +0 -337
  128. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +0 -334
  129. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationBell.tsx +0 -173
  130. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationCTAButton.tsx +0 -18
  131. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdown.tsx +0 -54
  132. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdownItem.tsx +0 -57
  133. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationIcon.tsx +0 -19
  134. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationListPanel.tsx +0 -92
  135. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRefreshButton.tsx +0 -20
  136. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRow.tsx +0 -58
  137. package/src/RedactoPrivacyCenter/components/NotificationHub/StatCard.tsx +0 -10
  138. package/src/RedactoPrivacyCenter/components/NotificationHub/useNotificationActions.ts +0 -112
  139. package/src/RedactoPrivacyCenter/components/NotificationHub/utils.ts +0 -27
  140. package/src/RedactoPrivacyCenter/components/Pagination.tsx +0 -140
  141. package/src/RedactoPrivacyCenter/components/PrivacyCenterEmptyState.tsx +0 -76
  142. package/src/RedactoPrivacyCenter/components/PrivacyCenterSessionExpired.tsx +0 -97
  143. package/src/RedactoPrivacyCenter/components/ReceiptsList.tsx +0 -454
  144. package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +0 -270
  145. package/src/RedactoPrivacyCenter/components/SelectUser.tsx +0 -95
  146. package/src/RedactoPrivacyCenter/components/SelectUserGate.tsx +0 -187
  147. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +0 -25
  148. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +0 -21
  149. package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +0 -159
  150. package/src/RedactoPrivacyCenter/components/constant.ts +0 -52
  151. package/src/RedactoPrivacyCenter/components/groupHelpers.ts +0 -119
  152. package/src/RedactoPrivacyCenter/components/receiptColumns.tsx +0 -203
  153. package/src/RedactoPrivacyCenter/components/types.ts +0 -123
  154. package/src/RedactoPrivacyCenter/components/utils.tsx +0 -10
  155. package/src/RedactoPrivacyCenter/config/routes.ts +0 -9
  156. package/src/RedactoPrivacyCenter/config/table.ts +0 -25
  157. package/src/RedactoPrivacyCenter/context/AuthContext.tsx +0 -160
  158. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +0 -47
  159. package/src/RedactoPrivacyCenter/context/BrandingContext.tsx +0 -52
  160. package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.session-expired.test.tsx +0 -92
  161. package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.tsx +0 -181
  162. package/src/RedactoPrivacyCenter/context/LocaleContext.tsx +0 -11
  163. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +0 -96
  164. package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +0 -30
  165. package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +0 -80
  166. package/src/RedactoPrivacyCenter/context/ProfileSwitchContext.tsx +0 -15
  167. package/src/RedactoPrivacyCenter/context/types.ts +0 -19
  168. package/src/RedactoPrivacyCenter/lib/api.ts +0 -759
  169. package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +0 -67
  170. package/src/RedactoPrivacyCenter/lib/constants.ts +0 -276
  171. package/src/RedactoPrivacyCenter/lib/i18n.ts +0 -81
  172. package/src/RedactoPrivacyCenter/lib/locales.ts +0 -102
  173. package/src/RedactoPrivacyCenter/lib/queries.ts +0 -141
  174. package/src/RedactoPrivacyCenter/lib/types.ts +0 -876
  175. package/src/RedactoPrivacyCenter/lib/useCurrentLanguage.ts +0 -37
  176. package/src/RedactoPrivacyCenter/lib/utils.refresh-circuit.test.ts +0 -127
  177. package/src/RedactoPrivacyCenter/lib/utils.refresh-storage.test.ts +0 -85
  178. package/src/RedactoPrivacyCenter/lib/utils.session-expired.test.ts +0 -109
  179. package/src/RedactoPrivacyCenter/lib/utils.ts +0 -783
  180. package/src/RedactoPrivacyCenter/locales/as/translation.json +0 -342
  181. package/src/RedactoPrivacyCenter/locales/bn/translation.json +0 -342
  182. package/src/RedactoPrivacyCenter/locales/brx/translation.json +0 -342
  183. package/src/RedactoPrivacyCenter/locales/doi/translation.json +0 -342
  184. package/src/RedactoPrivacyCenter/locales/en/translation.json +0 -357
  185. package/src/RedactoPrivacyCenter/locales/gom/translation.json +0 -342
  186. package/src/RedactoPrivacyCenter/locales/gu/translation.json +0 -342
  187. package/src/RedactoPrivacyCenter/locales/hi/translation.json +0 -345
  188. package/src/RedactoPrivacyCenter/locales/kn/translation.json +0 -342
  189. package/src/RedactoPrivacyCenter/locales/ks/translation.json +0 -342
  190. package/src/RedactoPrivacyCenter/locales/mai/translation.json +0 -342
  191. package/src/RedactoPrivacyCenter/locales/ml/translation.json +0 -342
  192. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +0 -342
  193. package/src/RedactoPrivacyCenter/locales/mr/translation.json +0 -342
  194. package/src/RedactoPrivacyCenter/locales/ne/translation.json +0 -342
  195. package/src/RedactoPrivacyCenter/locales/or/translation.json +0 -342
  196. package/src/RedactoPrivacyCenter/locales/pa/translation.json +0 -342
  197. package/src/RedactoPrivacyCenter/locales/sa/translation.json +0 -342
  198. package/src/RedactoPrivacyCenter/locales/sat/translation.json +0 -342
  199. package/src/RedactoPrivacyCenter/locales/sd/translation.json +0 -342
  200. package/src/RedactoPrivacyCenter/locales/ta/translation.json +0 -342
  201. package/src/RedactoPrivacyCenter/locales/te/translation.json +0 -342
  202. package/src/RedactoPrivacyCenter/locales/ur/translation.json +0 -342
  203. package/src/RedactoPrivacyCenter/styles/applyBrandingTheme.ts +0 -66
  204. package/src/RedactoPrivacyCenter/styles/base.css +0 -117
  205. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +0 -3108
  206. package/src/RedactoPrivacyCenter/styles/pcStyles.ts +0 -831
  207. package/src/RedactoPrivacyCenter/styles/responsive.ts +0 -138
  208. package/src/RedactoPrivacyCenter/styles/variables.css +0 -96
  209. package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +0 -387
  210. package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +0 -468
  211. package/src/RedactoPrivacyCenter/table/index.ts +0 -2
  212. package/src/RedactoPrivacyCenter/types/images.d.ts +0 -29
  213. package/src/RedactoPrivacyCenter/ui/Footer.tsx +0 -69
  214. package/src/RedactoPrivacyCenter/ui/FormBlockWrapper.tsx +0 -54
  215. package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +0 -93
  216. package/src/RedactoPrivacyCenter/ui/PCActionIcon.tsx +0 -47
  217. package/src/RedactoPrivacyCenter/ui/PCAvatar.test.tsx +0 -83
  218. package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +0 -93
  219. package/src/RedactoPrivacyCenter/ui/PCBadge.tsx +0 -48
  220. package/src/RedactoPrivacyCenter/ui/PCButton.tsx +0 -77
  221. package/src/RedactoPrivacyCenter/ui/PCCheckbox.tsx +0 -55
  222. package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +0 -107
  223. package/src/RedactoPrivacyCenter/ui/PCEmpty.tsx +0 -35
  224. package/src/RedactoPrivacyCenter/ui/PCInput.tsx +0 -132
  225. package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +0 -31
  226. package/src/RedactoPrivacyCenter/ui/PCModal.tsx +0 -106
  227. package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +0 -206
  228. package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +0 -433
  229. package/src/RedactoPrivacyCenter/ui/PCTable.tsx +0 -149
  230. package/src/RedactoPrivacyCenter/ui/PCText.tsx +0 -41
  231. package/src/RedactoPrivacyCenter/ui/PCTextarea.tsx +0 -43
  232. package/src/RedactoPrivacyCenter/ui/PCTitle.tsx +0 -37
  233. package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +0 -121
  234. package/src/RedactoPrivacyCenter/ui/SimpleBar.tsx +0 -25
  235. package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +0 -53
  236. package/src/RedactoPrivacyCenter/ui/index.ts +0 -18
  237. package/src/RedactoPrivacyCenter/utils/cn.ts +0 -28
  238. package/src/RedactoPrivacyCenter/utils/date.ts +0 -48
  239. package/src/RedactoPrivacyCenter/utils/status.ts +0 -111
  240. package/src/RedactoPrivacyCenter/utils/toast.ts +0 -130
  241. package/src/index.ts +0 -19
  242. package/src/privacy-center.ts +0 -113
  243. package/src/shared/api-errors.ts +0 -180
  244. package/src/shared/notice-consent-status.ts +0 -190
  245. package/src/types/css.d.ts +0 -4
  246. package/src/types/images.d.ts +0 -19
  247. package/src/types/react-jsx.d.ts +0 -26
  248. package/tests/DataAvailabilityContext.test.tsx +0 -123
  249. package/tests/Form.test.tsx +0 -1287
  250. package/tests/ManageConsentErrorMapping.test.tsx +0 -85
  251. package/tests/ProductSelectionSection.test.tsx +0 -328
  252. package/tests/RequestSubmit.test.tsx +0 -135
  253. package/tests/RevocablePurpose.test.tsx +0 -146
  254. package/tests/RevokeWarningMessage.test.tsx +0 -151
  255. package/tests/consentClient.test.ts +0 -217
  256. package/tests/manageConsentServer.test.ts +0 -38
  257. package/tests/mocks.ts +0 -257
  258. package/tests/resolveContactFromJwt.test.ts +0 -44
  259. package/tests/setup.ts +0 -16
  260. package/tsconfig.json +0 -16
  261. package/tsup.config.ts +0 -31
  262. 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
- });