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

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 (66) hide show
  1. package/.turbo/turbo-build.log +22 -25
  2. package/CHANGELOG.md +20 -0
  3. package/dist/index.d.mts +13 -6
  4. package/dist/index.d.ts +13 -6
  5. package/dist/index.js +6 -8
  6. package/dist/index.mjs +6 -8
  7. package/dist/privacy-center.d.mts +62 -27
  8. package/dist/privacy-center.d.ts +62 -27
  9. package/dist/privacy-center.js +1940 -1365
  10. package/dist/privacy-center.mjs +1864 -1284
  11. package/package.json +1 -1
  12. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +1 -0
  13. package/src/RedactoNoticeConsent/api/index.ts +4 -4
  14. package/src/RedactoNoticeConsent/api/types.ts +3 -2
  15. package/src/RedactoNoticeConsent/types.ts +1 -1
  16. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +1 -0
  17. package/src/RedactoNoticeConsentInline/api/index.ts +4 -4
  18. package/src/RedactoNoticeConsentInline/api/types.ts +2 -1
  19. package/src/RedactoNoticeConsentInline/types.ts +1 -1
  20. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +8 -5
  21. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +1 -1
  22. package/src/RedactoPrivacyCenter/api/actions.ts +24 -5
  23. package/src/RedactoPrivacyCenter/api/client.ts +101 -21
  24. package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +64 -35
  25. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +7 -0
  26. package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +19 -2
  27. package/src/RedactoPrivacyCenter/components/Form.tsx +7 -3
  28. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +1 -1
  29. package/src/RedactoPrivacyCenter/components/SelectUser.tsx +83 -0
  30. package/src/RedactoPrivacyCenter/components/SelectUserGate.tsx +193 -0
  31. package/src/RedactoPrivacyCenter/components/constant.ts +11 -0
  32. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +1 -1
  33. package/src/RedactoPrivacyCenter/context/ProfileSwitchContext.tsx +15 -0
  34. package/src/RedactoPrivacyCenter/context/types.ts +1 -1
  35. package/src/RedactoPrivacyCenter/lib/types.ts +63 -2
  36. package/src/RedactoPrivacyCenter/locales/as/translation.json +9 -1
  37. package/src/RedactoPrivacyCenter/locales/bho/translation.json +9 -1
  38. package/src/RedactoPrivacyCenter/locales/bn/translation.json +9 -1
  39. package/src/RedactoPrivacyCenter/locales/brx/translation.json +9 -1
  40. package/src/RedactoPrivacyCenter/locales/doi/translation.json +9 -1
  41. package/src/RedactoPrivacyCenter/locales/en/translation.json +9 -1
  42. package/src/RedactoPrivacyCenter/locales/gom/translation.json +9 -1
  43. package/src/RedactoPrivacyCenter/locales/gu/translation.json +9 -1
  44. package/src/RedactoPrivacyCenter/locales/hi/translation.json +9 -1
  45. package/src/RedactoPrivacyCenter/locales/kn/translation.json +9 -1
  46. package/src/RedactoPrivacyCenter/locales/ks/translation.json +9 -1
  47. package/src/RedactoPrivacyCenter/locales/mai/translation.json +9 -1
  48. package/src/RedactoPrivacyCenter/locales/ml/translation.json +9 -1
  49. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +9 -1
  50. package/src/RedactoPrivacyCenter/locales/mr/translation.json +9 -1
  51. package/src/RedactoPrivacyCenter/locales/ne/translation.json +9 -1
  52. package/src/RedactoPrivacyCenter/locales/or/translation.json +9 -1
  53. package/src/RedactoPrivacyCenter/locales/pa/translation.json +9 -1
  54. package/src/RedactoPrivacyCenter/locales/sa/translation.json +9 -1
  55. package/src/RedactoPrivacyCenter/locales/sat/translation.json +9 -1
  56. package/src/RedactoPrivacyCenter/locales/sd/translation.json +9 -1
  57. package/src/RedactoPrivacyCenter/locales/ta/translation.json +9 -1
  58. package/src/RedactoPrivacyCenter/locales/te/translation.json +9 -1
  59. package/src/RedactoPrivacyCenter/locales/ur/translation.json +9 -1
  60. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +105 -0
  61. package/tests/Form.test.tsx +53 -7
  62. package/tests/ManageConsentErrorMapping.test.tsx +85 -0
  63. package/tests/RevocablePurpose.test.tsx +146 -0
  64. package/tests/RevokeWarningMessage.test.tsx +151 -0
  65. package/tests/consentClient.test.ts +171 -0
  66. package/tests/manageConsentServer.test.ts +38 -0
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "প্রোফাইল হোংদোক-উ",
335
+ "chooseYourProfile": "নহাক্কী প্রোফাইল খনবিয়ু",
336
+ "emailLinkedToMultipleProfiles": "{{email}} অসি প্রোফাইল অমদগী হেন্না লোইননবা লৈ। নহাক্না য়েংনিংবা অদু খনবিয়ু।",
337
+ "contactLinkedToMultipleProfiles": "মসি কন্টেক্ট অসি প্রোফাইল অমদগী হেন্না লোইননবা লৈ। নহাক্না য়েংনিংবা অদু খনবিয়ু।",
338
+ "signingInEllipsis": "সাইন ইন তৌরি…",
339
+ "couldNotSwitchProfile": "প্রোফাইল অদুদা হোংদোকপা ঙমদ্রে। চানবীদুনা অমুক হন্না হোৎনবিয়ু।",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "प्रोफाइल बदला",
335
+ "chooseYourProfile": "तुमची प्रोफाइल निवडा",
336
+ "emailLinkedToMultipleProfiles": "{{email}} एकापेक्षा जास्त प्रोफाइलशी जोडलेला आहे. तुम्हाला जी पाहायची आहे ती निवडा.",
337
+ "contactLinkedToMultipleProfiles": "हा संपर्क एकापेक्षा जास्त प्रोफाइलशी जोडलेला आहे. तुम्हाला जी पाहायची आहे ती निवडा.",
338
+ "signingInEllipsis": "साइन इन करत आहे…",
339
+ "couldNotSwitchProfile": "त्या प्रोफाइलवर स्विच करता आले नाही. कृपया पुन्हा प्रयत्न करा.",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "प्रोफाइल बदल्नुहोस्",
335
+ "chooseYourProfile": "आफ्नो प्रोफाइल छान्नुहोस्",
336
+ "emailLinkedToMultipleProfiles": "{{email}} एकभन्दा बढी प्रोफाइलसँग जोडिएको छ। तपाईंले हेर्न चाहनुभएको छान्नुहोस्।",
337
+ "contactLinkedToMultipleProfiles": "यो सम्पर्क एकभन्दा बढी प्रोफाइलसँग जोडिएको छ। तपाईंले हेर्न चाहनुभएको छान्नुहोस्।",
338
+ "signingInEllipsis": "साइन इन हुँदैछ…",
339
+ "couldNotSwitchProfile": "त्यो प्रोफाइलमा स्विच गर्न सकिएन। कृपया फेरि प्रयास गर्नुहोस्।",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "ପ୍ରୋଫାଇଲ୍ ବଦଳାନ୍ତୁ",
335
+ "chooseYourProfile": "ଆପଣଙ୍କ ପ୍ରୋଫାଇଲ୍ ବାଛନ୍ତୁ",
336
+ "emailLinkedToMultipleProfiles": "{{email}} ଏକାଧିକ ପ୍ରୋଫାଇଲ୍ ସହିତ ଯୋଡ଼ାଯାଇଛି। ଆପଣ ଦେଖିବାକୁ ଚାହୁଁଥିବାଟି ବାଛନ୍ତୁ।",
337
+ "contactLinkedToMultipleProfiles": "ଏହି ଯୋଗାଯୋଗ ଏକାଧିକ ପ୍ରୋଫାଇଲ୍ ସହିତ ଯୋଡ଼ାଯାଇଛି। ଆପଣ ଦେଖିବାକୁ ଚାହୁଁଥିବାଟି ବାଛନ୍ତୁ।",
338
+ "signingInEllipsis": "ସାଇନ୍ ଇନ୍ ହେଉଛି…",
339
+ "couldNotSwitchProfile": "ସେହି ପ୍ରୋଫାଇଲକୁ ସ୍ୱିଚ୍ କରାଯାଇପାରିଲା ନାହିଁ। ଦୟାକରି ପୁନର୍ବାର ଚେଷ୍ଟା କରନ୍ତୁ।",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "ਪ੍ਰੋਫਾਈਲ ਬਦਲੋ",
335
+ "chooseYourProfile": "ਆਪਣੀ ਪ੍ਰੋਫਾਈਲ ਚੁਣੋ",
336
+ "emailLinkedToMultipleProfiles": "{{email}} ਇੱਕ ਤੋਂ ਵੱਧ ਪ੍ਰੋਫਾਈਲਾਂ ਨਾਲ ਜੁੜਿਆ ਹੋਇਆ ਹੈ। ਜੋ ਤੁਸੀਂ ਵੇਖਣਾ ਚਾਹੁੰਦੇ ਹੋ ਉਹ ਚੁਣੋ।",
337
+ "contactLinkedToMultipleProfiles": "ਇਹ ਸੰਪਰਕ ਇੱਕ ਤੋਂ ਵੱਧ ਪ੍ਰੋਫਾਈਲਾਂ ਨਾਲ ਜੁੜਿਆ ਹੋਇਆ ਹੈ। ਜੋ ਤੁਸੀਂ ਵੇਖਣਾ ਚਾਹੁੰਦੇ ਹੋ ਉਹ ਚੁਣੋ।",
338
+ "signingInEllipsis": "ਸਾਈਨ ਇਨ ਹੋ ਰਿਹਾ ਹੈ…",
339
+ "couldNotSwitchProfile": "ਉਸ ਪ੍ਰੋਫਾਈਲ 'ਤੇ ਸਵਿੱਚ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ। ਕਿਰਪਾ ਕਰਕੇ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "प्रोफ़ाइलं परिवर्तयतु",
335
+ "chooseYourProfile": "स्वप्रोफ़ाइलं चिनोतु",
336
+ "emailLinkedToMultipleProfiles": "{{email}} एकाधिकैः प्रोफ़ाइलैः सह सम्बद्धः अस्ति। यत् द्रष्टुम् इच्छति तत् चिनोतु।",
337
+ "contactLinkedToMultipleProfiles": "अयं सम्पर्कः एकाधिकैः प्रोफ़ाइलैः सह सम्बद्धः अस्ति। यत् द्रष्टुम् इच्छति तत् चिनोतु।",
338
+ "signingInEllipsis": "प्रवेशः क्रियते…",
339
+ "couldNotSwitchProfile": "तां प्रोफ़ाइलं प्रति परिवर्तनं न शक्यते स्म। कृपया पुनः प्रयत्नं करोतु।",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "ᱯᱨᱳᱯᱷᱟᱭᱤᱞ ᱵᱚᱫᱚᱞ ᱢᱮ",
335
+ "chooseYourProfile": "ᱟᱢᱟᱜ ᱯᱨᱳᱯᱷᱟᱭᱤᱞ ᱵᱟᱪᱷᱟᱣ ᱢᱮ",
336
+ "emailLinkedToMultipleProfiles": "{{email}} ᱫᱚ ᱢᱤᱫ ᱠᱷᱚᱱ ᱰᱷᱮᱨ ᱯᱨᱳᱯᱷᱟᱭᱤᱞ ᱥᱟᱶ ᱡᱩᱰᱟᱹᱣ ᱟᱠᱟᱱᱟ᱾ ᱟᱢ ᱧᱮᱞ ᱥᱟᱱᱟᱢ ᱚᱱᱟ ᱵᱟᱪᱷᱟᱣ ᱢᱮ᱾",
337
+ "contactLinkedToMultipleProfiles": "ᱱᱳᱶᱟ ᱠᱚᱱᱴᱮᱠᱴ ᱫᱚ ᱢᱤᱫ ᱠᱷᱚᱱ ᱰᱷᱮᱨ ᱯᱨᱳᱯᱷᱟᱭᱤᱞ ᱥᱟᱶ ᱡᱩᱰᱟᱹᱣ ᱟᱠᱟᱱᱟ᱾ ᱟᱢ ᱧᱮᱞ ᱥᱟᱱᱟᱢ ᱚᱱᱟ ᱵᱟᱪᱷᱟᱣ ᱢᱮ᱾",
338
+ "signingInEllipsis": "ᱥᱟᱭᱤᱱ ᱤᱱ ᱮᱫᱟᱭ…",
339
+ "couldNotSwitchProfile": "ᱚᱱᱟ ᱯᱨᱳᱯᱷᱟᱭᱤᱞ ᱛᱮ ᱥᱩᱭᱪ ᱵᱟᱝ ᱦᱩᱭ ᱞᱮᱱᱟ᱾ ᱫᱟᱭᱟ ᱠᱟᱛᱮ ᱫᱩᱠᱟᱹᱛᱮ ᱠᱩᱨᱩᱢᱩᱴᱩ ᱢᱮ᱾",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "پروفائل تبديل ڪريو",
335
+ "chooseYourProfile": "پنهنجو پروفائل چونڊيو",
336
+ "emailLinkedToMultipleProfiles": "{{email}} هڪ کان وڌيڪ پروفائلن سان ڳنڍيل آهي. جيڪو توهان ڏسڻ چاهيو ٿا اهو چونڊيو.",
337
+ "contactLinkedToMultipleProfiles": "هي رابطو هڪ کان وڌيڪ پروفائلن سان ڳنڍيل آهي. جيڪو توهان ڏسڻ چاهيو ٿا اهو چونڊيو.",
338
+ "signingInEllipsis": "سائن ان ٿي رهيو آهي…",
339
+ "couldNotSwitchProfile": "ان پروفائل تي سوئچ نه ٿي سگهيو. مهرباني ڪري ٻيهر ڪوشش ڪريو.",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "சுயவிவரத்தை மாற்று",
335
+ "chooseYourProfile": "உங்கள் சுயவிவரத்தைத் தேர்ந்தெடுக்கவும்",
336
+ "emailLinkedToMultipleProfiles": "{{email}} ஒன்றுக்கு மேற்பட்ட சுயவிவரங்களுடன் இணைக்கப்பட்டுள்ளது. நீங்கள் பார்க்க விரும்புவதைத் தேர்ந்தெடுக்கவும்.",
337
+ "contactLinkedToMultipleProfiles": "இந்தத் தொடர்பு ஒன்றுக்கு மேற்பட்ட சுயவிவரங்களுடன் இணைக்கப்பட்டுள்ளது. நீங்கள் பார்க்க விரும்புவதைத் தேர்ந்தெடுக்கவும்.",
338
+ "signingInEllipsis": "உள்நுழைகிறது…",
339
+ "couldNotSwitchProfile": "அந்தச் சுயவிவரத்திற்கு மாற முடியவில்லை. மீண்டும் முயற்சிக்கவும்.",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "ప్రొఫైల్‌ను మార్చండి",
335
+ "chooseYourProfile": "మీ ప్రొఫైల్‌ను ఎంచుకోండి",
336
+ "emailLinkedToMultipleProfiles": "{{email}} ఒకటి కంటే ఎక్కువ ప్రొఫైల్‌లతో అనుసంధానించబడింది. మీరు చూడాలనుకుంటున్నదాన్ని ఎంచుకోండి.",
337
+ "contactLinkedToMultipleProfiles": "ఈ సంప్రదింపు ఒకటి కంటే ఎక్కువ ప్రొఫైల్‌లతో అనుసంధానించబడింది. మీరు చూడాలనుకుంటున్నదాన్ని ఎంచుకోండి.",
338
+ "signingInEllipsis": "సైన్ ఇన్ అవుతోంది…",
339
+ "couldNotSwitchProfile": "ఆ ప్రొఫైల్‌కు మారడం సాధ్యం కాలేదు. దయచేసి మళ్లీ ప్రయత్నించండి.",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "پروفائل تبدیل کریں",
335
+ "chooseYourProfile": "اپنی پروفائل منتخب کریں",
336
+ "emailLinkedToMultipleProfiles": "{{email}} ایک سے زیادہ پروفائلز سے منسلک ہے۔ جسے آپ دیکھنا چاہتے ہیں اسے منتخب کریں۔",
337
+ "contactLinkedToMultipleProfiles": "یہ رابطہ ایک سے زیادہ پروفائلز سے منسلک ہے۔ جسے آپ دیکھنا چاہتے ہیں اسے منتخب کریں۔",
338
+ "signingInEllipsis": "سائن ان ہو رہا ہے…",
339
+ "couldNotSwitchProfile": "اس پروفائل پر سوئچ نہیں کیا جا سکا۔ براہ کرم دوبارہ کوشش کریں۔",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -664,6 +664,111 @@ export const injectPCStyles = () => {
664
664
  color: var(--pc-text-secondary);
665
665
  line-height: 1.25;
666
666
  }
667
+ .pc-select-user {
668
+ display: flex;
669
+ flex-direction: column;
670
+ justify-content: center;
671
+ gap: var(--pc-spacing-xl);
672
+ width: 100%;
673
+ max-width: 460px;
674
+ margin: 0 auto;
675
+ min-height: min(62vh, 540px);
676
+ padding-top: var(--pc-spacing-2xl);
677
+ padding-bottom: var(--pc-spacing-2xl);
678
+ }
679
+ .pc-select-user__header {
680
+ display: flex;
681
+ flex-direction: column;
682
+ align-items: center;
683
+ text-align: center;
684
+ gap: var(--pc-spacing-sm);
685
+ }
686
+ .pc-select-user__header > * {
687
+ margin: 0;
688
+ }
689
+ .pc-select-user__list {
690
+ display: flex;
691
+ flex-direction: column;
692
+ gap: 0.625rem;
693
+ }
694
+ .pc-select-user__item {
695
+ display: flex;
696
+ align-items: center;
697
+ justify-content: space-between;
698
+ gap: 0.75rem;
699
+ width: 100%;
700
+ box-sizing: border-box;
701
+ padding: 0.75rem 1rem;
702
+ border: 1px solid var(--pc-border-color);
703
+ border-radius: 0.625rem;
704
+ background-color: var(--pc-bg-primary);
705
+ color: var(--pc-text-primary);
706
+ cursor: pointer;
707
+ text-align: left;
708
+ appearance: none;
709
+ -webkit-appearance: none;
710
+ font-family: inherit;
711
+ transition: border-color 0.2s ease, background-color 0.2s ease;
712
+ }
713
+ .pc-select-user__item:hover:not(:disabled) {
714
+ border-color: var(--pc-primary);
715
+ background-color: color-mix(in srgb, var(--pc-primary) 6%, var(--pc-bg-primary));
716
+ }
717
+ .pc-select-user__item:disabled {
718
+ cursor: default;
719
+ opacity: 0.7;
720
+ }
721
+ .pc-select-user__item:focus-visible {
722
+ outline: 2px solid var(--pc-border-color-focus);
723
+ outline-offset: 2px;
724
+ }
725
+ .pc-select-user__item-text {
726
+ display: flex;
727
+ flex-direction: column;
728
+ align-items: flex-start;
729
+ gap: 0.125rem;
730
+ min-width: 0;
731
+ }
732
+ .pc-select-user__item-primary {
733
+ font-size: 0.875rem;
734
+ font-weight: 600;
735
+ line-height: 1.25;
736
+ overflow: hidden;
737
+ text-overflow: ellipsis;
738
+ white-space: nowrap;
739
+ max-width: 100%;
740
+ }
741
+ .pc-select-user__item-secondary {
742
+ font-size: 0.75rem;
743
+ color: var(--pc-text-secondary);
744
+ line-height: 1.25;
745
+ overflow: hidden;
746
+ text-overflow: ellipsis;
747
+ white-space: nowrap;
748
+ max-width: 100%;
749
+ }
750
+ .pc-select-user__item-status {
751
+ font-size: 0.75rem;
752
+ font-weight: 600;
753
+ color: var(--pc-primary);
754
+ flex-shrink: 0;
755
+ }
756
+ .pc-select-user__item-chevron {
757
+ color: var(--pc-primary);
758
+ flex-shrink: 0;
759
+ }
760
+ .pc-select-user__error {
761
+ margin: 0.75rem 0 0;
762
+ font-size: 0.8125rem;
763
+ color: var(--pc-error);
764
+ text-align: center;
765
+ }
766
+ .pc-select-user-loading {
767
+ display: flex;
768
+ align-items: center;
769
+ justify-content: center;
770
+ min-height: 60vh;
771
+ }
667
772
  .pc-product-purpose-panel {
668
773
  display: flex;
669
774
  flex-direction: column;
@@ -140,13 +140,16 @@ vi.mock('../src/RedactoPrivacyCenter/context/AuthContext', () => {
140
140
  return { useAuth: () => auth };
141
141
  });
142
142
 
143
- vi.mock('../src/RedactoPrivacyCenter/context/BaseUrlContext', () => {
144
- const base = {
145
- consentClient: { get: () => Promise.resolve(null), post: () => Promise.resolve(null) },
146
- baseUrl: 'https://consent.test',
147
- };
148
- return { useBaseUrl: () => base };
149
- });
143
+ // Mutable so individual tests can drop `ledgerBaseUrl` to exercise the legacy
144
+ // (no-ledger) mode; beforeEach restores the ledger default.
145
+ const baseUrlStub = vi.hoisted(() => ({
146
+ consentClient: { get: () => Promise.resolve(null), post: () => Promise.resolve(null) },
147
+ baseUrl: 'https://consent.test',
148
+ ledgerBaseUrl: 'https://ledger.test/consent-ledger' as string | undefined,
149
+ }));
150
+ vi.mock('../src/RedactoPrivacyCenter/context/BaseUrlContext', () => ({
151
+ useBaseUrl: () => baseUrlStub,
152
+ }));
150
153
 
151
154
  vi.mock('../src/RedactoPrivacyCenter/context/OrganizationContext', () => {
152
155
  const org = {
@@ -481,6 +484,8 @@ describe('Form (P2-SDK-3)', () => {
481
484
  beforeEach(() => {
482
485
  createCaseRequest.mockReset();
483
486
  getFormData.mockReset();
487
+ fetchUserConsents.mockReset();
488
+ baseUrlStub.ledgerBaseUrl = 'https://ledger.test/consent-ledger';
484
489
  sessionStorage.clear();
485
490
  });
486
491
 
@@ -519,6 +524,47 @@ describe('Form (P2-SDK-3)', () => {
519
524
  expect(submitButton.disabled).toBe(true);
520
525
  });
521
526
 
527
+ it('skips the ledger picker overlay when no ledgerBaseUrl is configured', async () => {
528
+ // Without a ledger, consents still live in Python and the form/data
529
+ // groups are authoritative — the overlay must not fire a user-consents
530
+ // read (it would hit the legacy endpoint and mis-map its shape).
531
+ baseUrlStub.ledgerBaseUrl = undefined;
532
+ setupSession();
533
+ getFormData.mockResolvedValue(
534
+ makeFormData({
535
+ direct_groups: [
536
+ makeGroup(),
537
+ makeGroup({
538
+ product_uuid: 'prod-direct-2',
539
+ product_name: 'Acme Rewards',
540
+ purposes: [basePurpose('purp-2', 'Analytics')],
541
+ }),
542
+ ],
543
+ }),
544
+ );
545
+
546
+ render(<Form onCreateRequest={vi.fn()} />);
547
+ await waitFor(() => expect(getFormData).toHaveBeenCalled());
548
+
549
+ pickRequestType(RequestTypeEnum.ERASURE);
550
+ await waitFor(() => {
551
+ expect(screen.getByText('Acme Loyalty')).toBeInTheDocument();
552
+ });
553
+ expect(fetchUserConsents).not.toHaveBeenCalled();
554
+ });
555
+
556
+ it('runs the ledger picker overlay when ledgerBaseUrl is configured', async () => {
557
+ setupSession();
558
+ getFormData.mockResolvedValue(
559
+ makeFormData({ direct_groups: [makeGroup()] }),
560
+ );
561
+
562
+ render(<Form onCreateRequest={vi.fn()} />);
563
+ await waitFor(() => expect(getFormData).toHaveBeenCalled());
564
+
565
+ await waitFor(() => expect(fetchUserConsents).toHaveBeenCalled());
566
+ });
567
+
522
568
  it('renders the product step for DSAR types when direct_groups is present', async () => {
523
569
  setupSession();
524
570
  const direct = makeGroup({
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ import { manageConsentServer, ConsentApiError } from '../src/RedactoPrivacyCenter/api/actions';
4
+ import { ApiError } from '../src/RedactoPrivacyCenter/api/fetcher';
5
+ import type { createConsentClient } from '../src/RedactoPrivacyCenter/api/client';
6
+
7
+ type ConsentClient = ReturnType<typeof createConsentClient>;
8
+
9
+ // Build a mock consent client whose `manageConsent` rejects with the supplied
10
+ // error. Only `manageConsent` is exercised by `manageConsentServer`, so the
11
+ // rest of the surface is a cast-through stub.
12
+ const makeClient = (error: unknown): ConsentClient =>
13
+ ({
14
+ manageConsent: () => Promise.reject(error),
15
+ } as unknown as ConsentClient);
16
+
17
+ const baseParams = {
18
+ organizationId: 'org-1',
19
+ workspaceId: 'ws-1',
20
+ purposeId: 'purpose-1',
21
+ contact: 'user@example.com',
22
+ action: 'revoke' as const,
23
+ authToken: 'token-abc',
24
+ };
25
+
26
+ describe('manageConsentServer error mapping — purpose_not_revocable', () => {
27
+ // The consent-server 400 is rendered by a Ninja wrapper as
28
+ // { code: 400, status, detail: { message, code: 'purpose_not_revocable' } }.
29
+ // `privacyCenterApiFetch` preserves the parsed `detail` on the ApiError and
30
+ // sets ApiError.code only from the TOP-LEVEL string code — which here is the
31
+ // integer 400 (dropped, so ApiError.code is undefined). The app-level
32
+ // discriminator therefore lives in `error.detail.code`.
33
+ const realBackody400 = () =>
34
+ new ApiError(
35
+ `[400] ${JSON.stringify(
36
+ { message: 'This purpose cannot be revoked.', code: 'purpose_not_revocable' },
37
+ null,
38
+ 2,
39
+ )}`,
40
+ { status: 400, code: undefined, detail: { message: 'This purpose cannot be revoked.', code: 'purpose_not_revocable' } },
41
+ );
42
+
43
+ it('maps the real backend 400 body (code nested in detail) to PURPOSE_NOT_REVOCABLE', async () => {
44
+ await expect(
45
+ manageConsentServer({ ...baseParams, consentClient: makeClient(realBackody400()) }),
46
+ ).rejects.toMatchObject({
47
+ name: 'ConsentApiError',
48
+ code: 'PURPOSE_NOT_REVOCABLE',
49
+ });
50
+ });
51
+
52
+ it('throws a ConsentApiError instance carrying the original message', async () => {
53
+ const error = realBackody400();
54
+ try {
55
+ await manageConsentServer({ ...baseParams, consentClient: makeClient(error) });
56
+ throw new Error('expected manageConsentServer to reject');
57
+ } catch (caught) {
58
+ expect(caught).toBeInstanceOf(ConsentApiError);
59
+ expect((caught as ConsentApiError).code).toBe('PURPOSE_NOT_REVOCABLE');
60
+ expect((caught as ConsentApiError).message).toContain('purpose_not_revocable');
61
+ }
62
+ });
63
+
64
+ it('still maps when the string code is surfaced at the top level (error.code)', async () => {
65
+ // Defensive: if a backend/wrapper ever exposes the string code at the top
66
+ // level, the original `error?.code === 'purpose_not_revocable'` arm must
67
+ // continue to fire.
68
+ const error = new ApiError('[400] purpose is not revocable', {
69
+ status: 400,
70
+ code: 'purpose_not_revocable',
71
+ });
72
+ await expect(
73
+ manageConsentServer({ ...baseParams, consentClient: makeClient(error) }),
74
+ ).rejects.toMatchObject({ code: 'PURPOSE_NOT_REVOCABLE' });
75
+ });
76
+
77
+ it('does not misclassify an unrelated error as PURPOSE_NOT_REVOCABLE', async () => {
78
+ const error = new ApiError('[500] internal server error', { status: 500 });
79
+ // Unrelated non-auth errors are swallowed to null by handleServerActionError,
80
+ // so the call resolves rather than throwing a ConsentApiError.
81
+ await expect(
82
+ manageConsentServer({ ...baseParams, consentClient: makeClient(error) }),
83
+ ).resolves.toBeNull();
84
+ });
85
+ });
@@ -0,0 +1,146 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { describe, it, expect, vi } from 'vitest';
3
+
4
+ // react-i18next is an optional peer dep that is not installed in this
5
+ // package's node_modules. Stub it with a minimal `useTranslation` that
6
+ // resolves the keys used by the components under test, honouring the inline
7
+ // English fallback passed as the 2nd argument (mirrors the real i18next
8
+ // behaviour when a key is missing).
9
+ vi.mock('react-i18next', () => {
10
+ const en: Record<string, string> = {
11
+ modifyButton: 'Modify',
12
+ modifyConsent: 'Modify Consent',
13
+ closeModal: 'Close',
14
+ cancel: 'Cancel',
15
+ revokeConsent: 'Revoke Consent',
16
+ revokeConsentWarning: 'If you revoke this consent, you may lose access.',
17
+ aboutToModifyConsent: 'You are about to modify this consent.',
18
+ noDataElementsAvailable: 'No data elements available',
19
+ purposeNotRevocable: "This purpose can't be withdrawn directly.",
20
+ noActionsAvailable: 'No actions available for this record.',
21
+ };
22
+ const interpolate = (template: string, vars?: Record<string, unknown>) =>
23
+ template.replace(/\{\{(\w+)\}\}/g, (_match, key: string) => {
24
+ const value = vars?.[key];
25
+ return value === undefined || value === null ? '' : String(value);
26
+ });
27
+ // Supports the `t(key, fallbackString)` and `t(key, opts)` call shapes used
28
+ // across the components. When the key is unknown, fall back to the inline
29
+ // English string if one was supplied, otherwise echo the key.
30
+ const t = (key: string, second?: unknown, opts?: Record<string, unknown>) => {
31
+ const fallback = typeof second === 'string' ? second : undefined;
32
+ const interpolationOpts =
33
+ typeof second === 'object' && second !== null
34
+ ? (second as Record<string, unknown>)
35
+ : opts;
36
+ const template = en[key] ?? fallback ?? key;
37
+ return interpolate(template, interpolationOpts);
38
+ };
39
+ return {
40
+ useTranslation: () => ({ t, i18n: { language: 'en' } }),
41
+ };
42
+ });
43
+
44
+ // Icon peer deps — stub the icons used by the modal.
45
+ vi.mock('lucide-react', () => ({
46
+ ChevronDownIcon: () => null,
47
+ X: () => null,
48
+ Smartphone: () => null,
49
+ LaptopMinimal: () => null,
50
+ }));
51
+
52
+ vi.mock('react-icons/pi', () => ({
53
+ PiWarningCircleBold: () => null,
54
+ }));
55
+
56
+ import { renderModifyButton } from '../src/RedactoPrivacyCenter/components/ConsentManagerCells';
57
+ import { ModifyConsentModal } from '../src/RedactoPrivacyCenter/components/ModifyConsentModal';
58
+ import { NavigationProvider } from '../src/RedactoPrivacyCenter/context/NavigationContext';
59
+ import { ConsentStatusEnum } from '../src/RedactoPrivacyCenter/lib/types';
60
+ import type { ConsentModalRecord } from '../src/RedactoPrivacyCenter/lib/types';
61
+
62
+ const makeRecord = (
63
+ overrides: Partial<ConsentModalRecord> = {},
64
+ ): ConsentModalRecord => ({
65
+ purpose_uuid: 'purpose-1',
66
+ name: 'Marketing analytics',
67
+ description: 'Improve product recommendations',
68
+ status: ConsentStatusEnum.ACTIVE,
69
+ given_date: '2026-01-01T00:00:00Z',
70
+ valid_till: null,
71
+ method: 'WEB',
72
+ data_elements: [],
73
+ link_reason: null,
74
+ product_uuid: 'prod-1',
75
+ product_name: 'Acme Loyalty',
76
+ product_description: null,
77
+ nominator: null,
78
+ ...overrides,
79
+ });
80
+
81
+ // A TFunction-compatible stub for the pure render helper (which receives `t`
82
+ // directly rather than via the react-i18next mock). Resolves the handful of
83
+ // keys it uses and honours the inline English fallback like real i18next.
84
+ const tStubKeys: Record<string, string> = {
85
+ modifyButton: 'Modify',
86
+ };
87
+ const tStub = ((key: string, second?: unknown) =>
88
+ tStubKeys[key] ??
89
+ (typeof second === 'string' ? second : key)) as unknown as Parameters<
90
+ typeof renderModifyButton
91
+ >[2];
92
+
93
+ describe('non-revocable purpose', () => {
94
+ it('renders the modify/withdraw control disabled for a non-revocable ACTIVE purpose', () => {
95
+ const record = makeRecord({ revocable: false });
96
+ render(<>{renderModifyButton(record, vi.fn(), tStub)}</>);
97
+
98
+ const button = screen.getByRole('button', { name: 'Modify' });
99
+ expect(button).toBeDisabled();
100
+ expect(button).toHaveAttribute(
101
+ 'title',
102
+ "This purpose can't be withdrawn directly.",
103
+ );
104
+ });
105
+
106
+ it('does not invoke onModify when the disabled non-revocable control is clicked', () => {
107
+ const onModify = vi.fn();
108
+ const record = makeRecord({ revocable: false });
109
+ render(<>{renderModifyButton(record, onModify, tStub)}</>);
110
+
111
+ fireEvent.click(screen.getByRole('button', { name: 'Modify' }));
112
+ expect(onModify).not.toHaveBeenCalled();
113
+ });
114
+ });
115
+
116
+ describe('revocable purpose (regression)', () => {
117
+ it('renders the modify control enabled when revocable is absent (older backends)', () => {
118
+ const record = makeRecord(); // no `revocable` field
119
+ const onModify = vi.fn();
120
+ render(<>{renderModifyButton(record, onModify, tStub)}</>);
121
+
122
+ const button = screen.getByRole('button', { name: 'Modify' });
123
+ expect(button).not.toBeDisabled();
124
+ fireEvent.click(button);
125
+ expect(onModify).toHaveBeenCalledTimes(1);
126
+ });
127
+
128
+ it('offers the revoke action in the modal for a revocable ACTIVE record', () => {
129
+ const record = makeRecord({ revocable: true });
130
+ render(
131
+ <NavigationProvider>
132
+ <ModifyConsentModal
133
+ isOpen
134
+ onClose={vi.fn()}
135
+ selectedRecord={record}
136
+ isLoading={false}
137
+ onManageConsent={vi.fn()}
138
+ />
139
+ </NavigationProvider>,
140
+ );
141
+
142
+ expect(
143
+ screen.getByRole('button', { name: 'Revoke Consent' }),
144
+ ).toBeInTheDocument();
145
+ });
146
+ });