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

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 (42) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/privacy-center.d.mts +2 -0
  3. package/dist/privacy-center.d.ts +2 -0
  4. package/dist/privacy-center.js +177 -49
  5. package/dist/privacy-center.mjs +177 -49
  6. package/package.json +1 -1
  7. package/src/RedactoPrivacyCenter/api/actions.ts +14 -1
  8. package/src/RedactoPrivacyCenter/components/AppShell/AppShell.tsx +28 -0
  9. package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +25 -18
  10. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +7 -0
  11. package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +19 -2
  12. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +1 -1
  13. package/src/RedactoPrivacyCenter/components/SelectUser.tsx +15 -1
  14. package/src/RedactoPrivacyCenter/lib/types.ts +9 -0
  15. package/src/RedactoPrivacyCenter/locales/as/translation.json +4 -1
  16. package/src/RedactoPrivacyCenter/locales/bho/translation.json +4 -1
  17. package/src/RedactoPrivacyCenter/locales/bn/translation.json +4 -1
  18. package/src/RedactoPrivacyCenter/locales/brx/translation.json +4 -1
  19. package/src/RedactoPrivacyCenter/locales/doi/translation.json +4 -1
  20. package/src/RedactoPrivacyCenter/locales/en/translation.json +4 -1
  21. package/src/RedactoPrivacyCenter/locales/gom/translation.json +4 -1
  22. package/src/RedactoPrivacyCenter/locales/gu/translation.json +4 -1
  23. package/src/RedactoPrivacyCenter/locales/hi/translation.json +4 -1
  24. package/src/RedactoPrivacyCenter/locales/kn/translation.json +4 -1
  25. package/src/RedactoPrivacyCenter/locales/ks/translation.json +4 -1
  26. package/src/RedactoPrivacyCenter/locales/mai/translation.json +4 -1
  27. package/src/RedactoPrivacyCenter/locales/ml/translation.json +4 -1
  28. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +4 -1
  29. package/src/RedactoPrivacyCenter/locales/mr/translation.json +4 -1
  30. package/src/RedactoPrivacyCenter/locales/ne/translation.json +4 -1
  31. package/src/RedactoPrivacyCenter/locales/or/translation.json +4 -1
  32. package/src/RedactoPrivacyCenter/locales/pa/translation.json +4 -1
  33. package/src/RedactoPrivacyCenter/locales/sa/translation.json +4 -1
  34. package/src/RedactoPrivacyCenter/locales/sat/translation.json +4 -1
  35. package/src/RedactoPrivacyCenter/locales/sd/translation.json +4 -1
  36. package/src/RedactoPrivacyCenter/locales/ta/translation.json +4 -1
  37. package/src/RedactoPrivacyCenter/locales/te/translation.json +4 -1
  38. package/src/RedactoPrivacyCenter/locales/ur/translation.json +4 -1
  39. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +11 -0
  40. package/tests/ManageConsentErrorMapping.test.tsx +85 -0
  41. package/tests/RevocablePurpose.test.tsx +146 -0
  42. package/tests/RevokeWarningMessage.test.tsx +151 -0
@@ -5,7 +5,8 @@ import { PCAvatar, PCPopover } from '../../ui';
5
5
  import { useBranding } from '../../context/BrandingContext';
6
6
  import { useOrganization } from '../../context/OrganizationContext';
7
7
  import { useProfileSwitch } from '../../context/ProfileSwitchContext';
8
- import { getVar } from '../../lib/utils';
8
+ import { useAuth } from '../../context/AuthContext';
9
+ import { decodeJwtToken, getVar } from '../../lib/utils';
9
10
  import { usePrivacyCenterExitAction } from './usePrivacyCenterExitAction';
10
11
 
11
12
  type UserMenuProps = {
@@ -35,6 +36,7 @@ export function UserMenu({ onBack }: UserMenuProps) {
35
36
  const { t } = useTranslation();
36
37
  const branding = useBranding();
37
38
  const { organization } = useOrganization();
39
+ const { accessToken } = useAuth();
38
40
  const [email, setEmail] = useState('');
39
41
 
40
42
  useEffect(() => {
@@ -45,6 +47,12 @@ export function UserMenu({ onBack }: UserMenuProps) {
45
47
  usePrivacyCenterExitAction(onBack);
46
48
  const { canSwitch, switchProfile } = useProfileSwitch();
47
49
 
50
+ // The signed-in principal's org_user_id — unique per profile, unlike the
51
+ // email a shared contact reuses across profiles — so the menu can show which
52
+ // profile is active. Read from the (live) token, which re-scopes on switch.
53
+ const orgUserId =
54
+ (decodeJwtToken(accessToken)?.user_data?.org_user_id as string) || '';
55
+
48
56
  const displayName = branding?.brand_name || organization?.name || '';
49
57
  const triggerAvatarName = email ? initialsFromEmail(email) : displayName || '?';
50
58
 
@@ -155,7 +163,7 @@ export function UserMenu({ onBack }: UserMenuProps) {
155
163
  </div>
156
164
 
157
165
  {/* Identity card */}
158
- {email && (
166
+ {(email || orgUserId) && (
159
167
  <div
160
168
  style={{
161
169
  padding: '12px 16px',
@@ -168,22 +176,21 @@ export function UserMenu({ onBack }: UserMenuProps) {
168
176
  : 'none',
169
177
  }}
170
178
  >
171
- <PCAvatar name={initialsFromEmail(email)} size="sm" color="info" />
172
- <div
173
- title={email}
174
- style={{
175
- fontSize: 13,
176
- fontWeight: 600,
177
- color: getVar('--pc-text-primary'),
178
- lineHeight: 1.3,
179
- overflow: 'hidden',
180
- textOverflow: 'ellipsis',
181
- whiteSpace: 'nowrap',
182
- minWidth: 0,
183
- flex: 1,
184
- }}
185
- >
186
- {email}
179
+ <PCAvatar name={triggerAvatarName} size="sm" color="info" />
180
+ <div className="pc-shell-user-menu-identity">
181
+ {email ? (
182
+ <div title={email} className="pc-shell-user-menu-identity-email">
183
+ {email}
184
+ </div>
185
+ ) : null}
186
+ {orgUserId ? (
187
+ <div
188
+ title={orgUserId}
189
+ className="pc-shell-user-menu-identity-id"
190
+ >
191
+ {t('userIdLabel', { id: orgUserId })}
192
+ </div>
193
+ ) : null}
187
194
  </div>
188
195
  </div>
189
196
  )}
@@ -790,6 +790,13 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
790
790
  toast.error(t('nominatorNotFound'));
791
791
  } else if (error?.code === 'NOMINEE_MUST_SPECIFY_NOMINATOR') {
792
792
  toast.error(t('nomineeMustSpecifyNominator'));
793
+ } else if (error?.code === 'PURPOSE_NOT_REVOCABLE') {
794
+ toast.error(
795
+ t(
796
+ 'purposeNotRevocableError',
797
+ 'This purpose can\'t be withdrawn directly. Please raise a request to remove it.'
798
+ )
799
+ );
793
800
  } else {
794
801
  toast.error(errorMessages[action]);
795
802
  }
@@ -176,8 +176,14 @@ export const renderModifyButton = (
176
176
  onModify: (consent: ConsentModalRecord) => void,
177
177
  t: TFunction
178
178
  ) => {
179
- const isDisabled = consent.status === ConsentStatusEnum.DECLINED;
180
- return (
179
+ // An ACTIVE purpose flagged non-revocable can't be self-service withdrawn.
180
+ // Gate strictly on the explicit `false` so older responses (field absent)
181
+ // stay revocable.
182
+ const isNonRevocable =
183
+ consent.status === ConsentStatusEnum.ACTIVE && consent.revocable === false;
184
+ const isDisabled = consent.status === ConsentStatusEnum.DECLINED || isNonRevocable;
185
+ const nonRevocableMessage = t('purposeNotRevocable', "This purpose can't be withdrawn directly.");
186
+ const button = (
181
187
  <button
182
188
  type="button"
183
189
  style={{
@@ -194,8 +200,19 @@ export const renderModifyButton = (
194
200
  if (!isDisabled) onModify(consent);
195
201
  }}
196
202
  disabled={isDisabled}
203
+ title={isNonRevocable ? nonRevocableMessage : undefined}
197
204
  >
198
205
  {t('modifyButton')}
199
206
  </button>
200
207
  );
208
+
209
+ if (isNonRevocable) {
210
+ return (
211
+ <PCTooltip content={nonRevocableMessage} placement="top">
212
+ {button}
213
+ </PCTooltip>
214
+ );
215
+ }
216
+
217
+ return button;
201
218
  };
@@ -32,7 +32,7 @@ export const ModifyConsentModal = ({
32
32
  action: 'revoke' as ConsentAction,
33
33
  buttonLabel: t('revokeConsent'),
34
34
  buttonVariant: 'danger' as const,
35
- warningMessage: t('revokeConsentWarning'),
35
+ warningMessage: selectedRecord.revoke_warning_message || t('revokeConsentWarning'),
36
36
  description: nominatorLabel
37
37
  ? t('aboutToModifyConsentOnBehalf', { nominator: nominatorLabel })
38
38
  : t('aboutToModifyConsent'),
@@ -40,8 +40,17 @@ export const SelectUser = ({
40
40
  const principalUuid = candidate.uuid || '';
41
41
  const primaryLabel =
42
42
  candidate.primary_email || candidate.primary_mobile || candidate.uuid;
43
- const secondaryLabel =
43
+ const secondaryContact =
44
44
  candidate.primary_mobile || candidate.primary_email || '';
45
+ // Don't echo the same value on both lines — an email-only principal
46
+ // otherwise renders its email twice, which is exactly the "identical
47
+ // rows" problem. The org_user_id below is the real differentiator.
48
+ const secondaryLabel =
49
+ secondaryContact !== primaryLabel ? secondaryContact : '';
50
+ // org_user_id is unique per principal (ledger-enforced within the
51
+ // org/workspace), so it's the reliable way to tell shared-contact
52
+ // profiles apart when the email/mobile are identical.
53
+ const orgUserId = candidate.org_user_id || '';
45
54
  const isBusy = busyOrgUserId === principalUuid;
46
55
  return (
47
56
  <button
@@ -60,6 +69,11 @@ export const SelectUser = ({
60
69
  {secondaryLabel}
61
70
  </span>
62
71
  ) : null}
72
+ {orgUserId ? (
73
+ <span className="pc-select-user__item-id">
74
+ {t('userIdLabel', { id: orgUserId })}
75
+ </span>
76
+ ) : null}
63
77
  </span>
64
78
  {isBusy ? (
65
79
  <span className="pc-select-user__item-status">
@@ -256,6 +256,15 @@ export type PurposeItem = {
256
256
  method: string;
257
257
  data_elements: ConsentDataElement[];
258
258
  link_reason: string | null;
259
+ // When explicitly false, the purpose cannot be self-service withdrawn/revoked
260
+ // in the Privacy Center — the user must raise a data-subject request instead.
261
+ // Optional for backwards-compat: older backends omit it (treated as revocable).
262
+ revocable?: boolean;
263
+ // Per-purpose override for the revoke-confirmation warning message. When set,
264
+ // the revoke modal shows this text instead of the localized default; when
265
+ // null/empty/absent, the default `revokeConsentWarning` copy is used.
266
+ // Optional for backwards-compat: older backends omit it.
267
+ revoke_warning_message?: string | null;
259
268
  // UUID of the notice version this purpose was consented under. Served by the
260
269
  // Go ledger's user-consents response; required to revoke/regrant via the
261
270
  // ledger's submit-consent endpoint (which has no manage-consent route).
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} একাধিক প্ৰ'ফাইলৰ সৈতে সংযুক্ত। আপুনি চাব বিচৰাটো বাছনি কৰক।",
337
337
  "contactLinkedToMultipleProfiles": "এই যোগাযোগটো একাধিক প্ৰ'ফাইলৰ সৈতে সংযুক্ত। আপুনি চাব বিচৰাটো বাছনি কৰক।",
338
338
  "signingInEllipsis": "ছাইন ইন হৈ আছে…",
339
- "couldNotSwitchProfile": "সেই প্ৰ'ফাইললৈ সলনি কৰিব পৰা নগ'ল। অনুগ্ৰহ কৰি পুনৰ চেষ্টা কৰক।"
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.",
342
+ "userIdLabel": "ব্যৱহাৰকাৰী আইডি: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} एक से ढेर प्रोफाइल से जुड़ल बा। जवन रउआ देखल चाहत बानी ओकरा के चुनीं।",
337
337
  "contactLinkedToMultipleProfiles": "ई संपर्क एक से ढेर प्रोफाइल से जुड़ल बा। जवन रउआ देखल चाहत बानी ओकरा के चुनीं।",
338
338
  "signingInEllipsis": "साइन इन हो रहल बा…",
339
- "couldNotSwitchProfile": "ओह प्रोफाइल पर स्विच ना हो सकल। कृपया फेर से कोसिस करीं।"
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.",
342
+ "userIdLabel": "उपयोगकर्ता आईडी: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} একাধিক প্রোফাইলের সাথে যুক্ত। আপনি যেটি দেখতে চান সেটি বেছে নিন।",
337
337
  "contactLinkedToMultipleProfiles": "এই যোগাযোগটি একাধিক প্রোফাইলের সাথে যুক্ত। আপনি যেটি দেখতে চান সেটি বেছে নিন।",
338
338
  "signingInEllipsis": "সাইন ইন হচ্ছে…",
339
- "couldNotSwitchProfile": "সেই প্রোফাইলে স্যুইচ করা যায়নি। অনুগ্রহ করে আবার চেষ্টা করুন।"
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.",
342
+ "userIdLabel": "ব্যবহারকারীর আইডি: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} आ मोनसेनिख्रुइ बांसिन प्रफाइलजों नांजाब दं। नोंथाङा नायनो लुबैयोब्ला बेखौ सायख।",
337
337
  "contactLinkedToMultipleProfiles": "बे सोमोन्दोआ मोनसेनिख्रुइ बांसिन प्रफाइलजों नांजाब दं। नोंथाङा नायनो लुबैयोब्ला बेखौ सायख।",
338
338
  "signingInEllipsis": "साइन इन जायगासिनो दं…",
339
- "couldNotSwitchProfile": "बै प्रफाइलआव स्विच खालामनो हायाखै। अननानै फिन नाजा।"
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.",
342
+ "userIdLabel": "यूजर आईडी: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} इक कोला मता प्रोफाइलें कन्नै जुड़्या दा ऐ। जेह्ड़ी तुसें दिक्खनी ऐ ओह् चुनो।",
337
337
  "contactLinkedToMultipleProfiles": "एह् संपर्क इक कोला मता प्रोफाइलें कन्नै जुड़्या दा ऐ। जेह्ड़ी तुसें दिक्खनी ऐ ओह् चुनो।",
338
338
  "signingInEllipsis": "साइन इन होआ करदा ऐ…",
339
- "couldNotSwitchProfile": "ओह् प्रोफाइल पर स्विच नेईं होई सकेआ। कृपा करियै दुबारा कोशिश करो।"
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.",
342
+ "userIdLabel": "प्रयोगकर्ता आईडी: {{id}}"
340
343
  }
@@ -350,5 +350,8 @@
350
350
  "emailLinkedToMultipleProfiles": "{{email}} is linked to more than one profile. Choose the one you want to view.",
351
351
  "contactLinkedToMultipleProfiles": "This contact is linked to more than one profile. Choose the one you want to view.",
352
352
  "signingInEllipsis": "Signing in…",
353
- "couldNotSwitchProfile": "Could not switch to that profile. Please try again."
353
+ "couldNotSwitchProfile": "Could not switch to that profile. Please try again.",
354
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
355
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it.",
356
+ "userIdLabel": "User ID: {{id}}"
354
357
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} एकापरस चड प्रोफायलांक जोडलां. तुमकां जें पळोवपाक जाय तें निवडात.",
337
337
  "contactLinkedToMultipleProfiles": "हो संपर्क एकापरस चड प्रोफायलांक जोडला. तुमकां जें पळोवपाक जाय तें निवडात.",
338
338
  "signingInEllipsis": "साइन इन जावन आसा…",
339
- "couldNotSwitchProfile": "त्या प्रोफायलाचेर स्विच करूंक जालें ना. उपकार करून परत यत्न करात."
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.",
342
+ "userIdLabel": "वापरपी आयडी: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} એક કરતાં વધુ પ્રોફાઇલ સાથે જોડાયેલ છે. તમે જે જોવા માંગો છો તે પસંદ કરો.",
337
337
  "contactLinkedToMultipleProfiles": "આ સંપર્ક એક કરતાં વધુ પ્રોફાઇલ સાથે જોડાયેલ છે. તમે જે જોવા માંગો છો તે પસંદ કરો.",
338
338
  "signingInEllipsis": "સાઇન ઇન થઈ રહ્યું છે…",
339
- "couldNotSwitchProfile": "તે પ્રોફાઇલ પર સ્વિચ કરી શકાયું નહીં. કૃપા કરીને ફરી પ્રયાસ કરો."
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.",
342
+ "userIdLabel": "વપરાશકર્તા આઈડી: {{id}}"
340
343
  }
@@ -339,5 +339,8 @@
339
339
  "emailLinkedToMultipleProfiles": "{{email}} एक से अधिक प्रोफ़ाइल से जुड़ा है। जिसे आप देखना चाहते हैं उसे चुनें।",
340
340
  "contactLinkedToMultipleProfiles": "यह संपर्क एक से अधिक प्रोफ़ाइल से जुड़ा है। जिसे आप देखना चाहते हैं उसे चुनें।",
341
341
  "signingInEllipsis": "साइन इन हो रहा है…",
342
- "couldNotSwitchProfile": "उस प्रोफ़ाइल पर स्विच नहीं किया जा सका। कृपया पुनः प्रयास करें।"
342
+ "couldNotSwitchProfile": "उस प्रोफ़ाइल पर स्विच नहीं किया जा सका। कृपया पुनः प्रयास करें।",
343
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
344
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it.",
345
+ "userIdLabel": "उपयोगकर्ता आईडी: {{id}}"
343
346
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} ಒಂದಕ್ಕಿಂತ ಹೆಚ್ಚು ಪ್ರೊಫೈಲ್‌ಗಳಿಗೆ ಲಿಂಕ್ ಆಗಿದೆ. ನೀವು ವೀಕ್ಷಿಸಲು ಬಯಸುವುದನ್ನು ಆಯ್ಕೆಮಾಡಿ.",
337
337
  "contactLinkedToMultipleProfiles": "ಈ ಸಂಪರ್ಕವು ಒಂದಕ್ಕಿಂತ ಹೆಚ್ಚು ಪ್ರೊಫೈಲ್‌ಗಳಿಗೆ ಲಿಂಕ್ ಆಗಿದೆ. ನೀವು ವೀಕ್ಷಿಸಲು ಬಯಸುವುದನ್ನು ಆಯ್ಕೆಮಾಡಿ.",
338
338
  "signingInEllipsis": "ಸೈನ್ ಇನ್ ಆಗುತ್ತಿದೆ…",
339
- "couldNotSwitchProfile": "ಆ ಪ್ರೊಫೈಲ್‌ಗೆ ಬದಲಾಯಿಸಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ. ದಯವಿಟ್ಟು ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ."
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.",
342
+ "userIdLabel": "ಬಳಕೆದಾರ ಐಡಿ: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} چھُ اکہ کھۄتہ زیادٕ پروفائلن سٟتؠ جوڑنہ آمُت۔ یُس توہہِ وُچھُن چھِو یژھان سُ ژارٕو۔",
337
337
  "contactLinkedToMultipleProfiles": "یہ رابطہ چھُ اکہ کھۄتہ زیادٕ پروفائلن سٟتؠ جوڑنہ آمُت۔ یُس توہہِ وُچھُن چھِو یژھان سُ ژارٕو۔",
338
338
  "signingInEllipsis": "سائن اِن گژھان چھُ…",
339
- "couldNotSwitchProfile": "تہِ پروفائلس منٛز سوِچ کرُن آو نہ۔ مہربانی کرِتھ دوبارٕ کوشش کریو۔"
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.",
342
+ "userIdLabel": "صارف آئی ڈی: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} एकाधिक प्रोफाइलसँ जुड़ल अछि। जे अहाँ देखय चाहैत छी ओकरा चुनू।",
337
337
  "contactLinkedToMultipleProfiles": "ई संपर्क एकाधिक प्रोफाइलसँ जुड़ल अछि। जे अहाँ देखय चाहैत छी ओकरा चुनू।",
338
338
  "signingInEllipsis": "साइन इन भ' रहल अछि…",
339
- "couldNotSwitchProfile": "ओ प्रोफाइल पर स्विच नहि भ' सकल। कृपया फेर सँ प्रयास करू।"
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.",
342
+ "userIdLabel": "उपयोगकर्ता आइडी: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} ഒന്നിലധികം പ്രൊഫൈലുകളുമായി ബന്ധിപ്പിച്ചിരിക്കുന്നു. നിങ്ങൾ കാണാൻ ആഗ്രഹിക്കുന്നത് തിരഞ്ഞെടുക്കുക.",
337
337
  "contactLinkedToMultipleProfiles": "ഈ കോൺടാക്റ്റ് ഒന്നിലധികം പ്രൊഫൈലുകളുമായി ബന്ധിപ്പിച്ചിരിക്കുന്നു. നിങ്ങൾ കാണാൻ ആഗ്രഹിക്കുന്നത് തിരഞ്ഞെടുക്കുക.",
338
338
  "signingInEllipsis": "സൈൻ ഇൻ ചെയ്യുന്നു…",
339
- "couldNotSwitchProfile": "ആ പ്രൊഫൈലിലേക്ക് മാറാൻ കഴിഞ്ഞില്ല. വീണ്ടും ശ്രമിക്കുക."
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.",
342
+ "userIdLabel": "ഉപയോക്തൃ ഐഡി: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} অসি প্রোফাইল অমদগী হেন্না লোইননবা লৈ। নহাক্না য়েংনিংবা অদু খনবিয়ু।",
337
337
  "contactLinkedToMultipleProfiles": "মসি কন্টেক্ট অসি প্রোফাইল অমদগী হেন্না লোইননবা লৈ। নহাক্না য়েংনিংবা অদু খনবিয়ু।",
338
338
  "signingInEllipsis": "সাইন ইন তৌরি…",
339
- "couldNotSwitchProfile": "প্রোফাইল অদুদা হোংদোকপা ঙমদ্রে। চানবীদুনা অমুক হন্না হোৎনবিয়ু।"
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.",
342
+ "userIdLabel": "ꯃꯤꯑꯣꯏꯕ ꯑꯥꯏꯗꯤ: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} एकापेक्षा जास्त प्रोफाइलशी जोडलेला आहे. तुम्हाला जी पाहायची आहे ती निवडा.",
337
337
  "contactLinkedToMultipleProfiles": "हा संपर्क एकापेक्षा जास्त प्रोफाइलशी जोडलेला आहे. तुम्हाला जी पाहायची आहे ती निवडा.",
338
338
  "signingInEllipsis": "साइन इन करत आहे…",
339
- "couldNotSwitchProfile": "त्या प्रोफाइलवर स्विच करता आले नाही. कृपया पुन्हा प्रयत्न करा."
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.",
342
+ "userIdLabel": "वापरकर्ता आयडी: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} एकभन्दा बढी प्रोफाइलसँग जोडिएको छ। तपाईंले हेर्न चाहनुभएको छान्नुहोस्।",
337
337
  "contactLinkedToMultipleProfiles": "यो सम्पर्क एकभन्दा बढी प्रोफाइलसँग जोडिएको छ। तपाईंले हेर्न चाहनुभएको छान्नुहोस्।",
338
338
  "signingInEllipsis": "साइन इन हुँदैछ…",
339
- "couldNotSwitchProfile": "त्यो प्रोफाइलमा स्विच गर्न सकिएन। कृपया फेरि प्रयास गर्नुहोस्।"
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.",
342
+ "userIdLabel": "प्रयोगकर्ता आईडी: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} ଏକାଧିକ ପ୍ରୋଫାଇଲ୍ ସହିତ ଯୋଡ଼ାଯାଇଛି। ଆପଣ ଦେଖିବାକୁ ଚାହୁଁଥିବାଟି ବାଛନ୍ତୁ।",
337
337
  "contactLinkedToMultipleProfiles": "ଏହି ଯୋଗାଯୋଗ ଏକାଧିକ ପ୍ରୋଫାଇଲ୍ ସହିତ ଯୋଡ଼ାଯାଇଛି। ଆପଣ ଦେଖିବାକୁ ଚାହୁଁଥିବାଟି ବାଛନ୍ତୁ।",
338
338
  "signingInEllipsis": "ସାଇନ୍ ଇନ୍ ହେଉଛି…",
339
- "couldNotSwitchProfile": "ସେହି ପ୍ରୋଫାଇଲକୁ ସ୍ୱିଚ୍ କରାଯାଇପାରିଲା ନାହିଁ। ଦୟାକରି ପୁନର୍ବାର ଚେଷ୍ଟା କରନ୍ତୁ।"
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.",
342
+ "userIdLabel": "ବ୍ୟବହାରକାରୀ ଆଇଡି: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} ਇੱਕ ਤੋਂ ਵੱਧ ਪ੍ਰੋਫਾਈਲਾਂ ਨਾਲ ਜੁੜਿਆ ਹੋਇਆ ਹੈ। ਜੋ ਤੁਸੀਂ ਵੇਖਣਾ ਚਾਹੁੰਦੇ ਹੋ ਉਹ ਚੁਣੋ।",
337
337
  "contactLinkedToMultipleProfiles": "ਇਹ ਸੰਪਰਕ ਇੱਕ ਤੋਂ ਵੱਧ ਪ੍ਰੋਫਾਈਲਾਂ ਨਾਲ ਜੁੜਿਆ ਹੋਇਆ ਹੈ। ਜੋ ਤੁਸੀਂ ਵੇਖਣਾ ਚਾਹੁੰਦੇ ਹੋ ਉਹ ਚੁਣੋ।",
338
338
  "signingInEllipsis": "ਸਾਈਨ ਇਨ ਹੋ ਰਿਹਾ ਹੈ…",
339
- "couldNotSwitchProfile": "ਉਸ ਪ੍ਰੋਫਾਈਲ 'ਤੇ ਸਵਿੱਚ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ। ਕਿਰਪਾ ਕਰਕੇ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।"
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.",
342
+ "userIdLabel": "ਉਪਯੋਗਕਰਤਾ ਆਈਡੀ: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} एकाधिकैः प्रोफ़ाइलैः सह सम्बद्धः अस्ति। यत् द्रष्टुम् इच्छति तत् चिनोतु।",
337
337
  "contactLinkedToMultipleProfiles": "अयं सम्पर्कः एकाधिकैः प्रोफ़ाइलैः सह सम्बद्धः अस्ति। यत् द्रष्टुम् इच्छति तत् चिनोतु।",
338
338
  "signingInEllipsis": "प्रवेशः क्रियते…",
339
- "couldNotSwitchProfile": "तां प्रोफ़ाइलं प्रति परिवर्तनं न शक्यते स्म। कृपया पुनः प्रयत्नं करोतु।"
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.",
342
+ "userIdLabel": "उपयोक्तृ-आयडी: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} ᱫᱚ ᱢᱤᱫ ᱠᱷᱚᱱ ᱰᱷᱮᱨ ᱯᱨᱳᱯᱷᱟᱭᱤᱞ ᱥᱟᱶ ᱡᱩᱰᱟᱹᱣ ᱟᱠᱟᱱᱟ᱾ ᱟᱢ ᱧᱮᱞ ᱥᱟᱱᱟᱢ ᱚᱱᱟ ᱵᱟᱪᱷᱟᱣ ᱢᱮ᱾",
337
337
  "contactLinkedToMultipleProfiles": "ᱱᱳᱶᱟ ᱠᱚᱱᱴᱮᱠᱴ ᱫᱚ ᱢᱤᱫ ᱠᱷᱚᱱ ᱰᱷᱮᱨ ᱯᱨᱳᱯᱷᱟᱭᱤᱞ ᱥᱟᱶ ᱡᱩᱰᱟᱹᱣ ᱟᱠᱟᱱᱟ᱾ ᱟᱢ ᱧᱮᱞ ᱥᱟᱱᱟᱢ ᱚᱱᱟ ᱵᱟᱪᱷᱟᱣ ᱢᱮ᱾",
338
338
  "signingInEllipsis": "ᱥᱟᱭᱤᱱ ᱤᱱ ᱮᱫᱟᱭ…",
339
- "couldNotSwitchProfile": "ᱚᱱᱟ ᱯᱨᱳᱯᱷᱟᱭᱤᱞ ᱛᱮ ᱥᱩᱭᱪ ᱵᱟᱝ ᱦᱩᱭ ᱞᱮᱱᱟ᱾ ᱫᱟᱭᱟ ᱠᱟᱛᱮ ᱫᱩᱠᱟᱹᱛᱮ ᱠᱩᱨᱩᱢᱩᱴᱩ ᱢᱮ᱾"
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.",
342
+ "userIdLabel": "ᱵᱮᱵᱷᱟᱨᱤᱭᱟᱹ ᱟᱭᱰᱤ: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} هڪ کان وڌيڪ پروفائلن سان ڳنڍيل آهي. جيڪو توهان ڏسڻ چاهيو ٿا اهو چونڊيو.",
337
337
  "contactLinkedToMultipleProfiles": "هي رابطو هڪ کان وڌيڪ پروفائلن سان ڳنڍيل آهي. جيڪو توهان ڏسڻ چاهيو ٿا اهو چونڊيو.",
338
338
  "signingInEllipsis": "سائن ان ٿي رهيو آهي…",
339
- "couldNotSwitchProfile": "ان پروفائل تي سوئچ نه ٿي سگهيو. مهرباني ڪري ٻيهر ڪوشش ڪريو."
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.",
342
+ "userIdLabel": "يوزر آئيڊي: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} ஒன்றுக்கு மேற்பட்ட சுயவிவரங்களுடன் இணைக்கப்பட்டுள்ளது. நீங்கள் பார்க்க விரும்புவதைத் தேர்ந்தெடுக்கவும்.",
337
337
  "contactLinkedToMultipleProfiles": "இந்தத் தொடர்பு ஒன்றுக்கு மேற்பட்ட சுயவிவரங்களுடன் இணைக்கப்பட்டுள்ளது. நீங்கள் பார்க்க விரும்புவதைத் தேர்ந்தெடுக்கவும்.",
338
338
  "signingInEllipsis": "உள்நுழைகிறது…",
339
- "couldNotSwitchProfile": "அந்தச் சுயவிவரத்திற்கு மாற முடியவில்லை. மீண்டும் முயற்சிக்கவும்."
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.",
342
+ "userIdLabel": "பயனர் ஐடி: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} ఒకటి కంటే ఎక్కువ ప్రొఫైల్‌లతో అనుసంధానించబడింది. మీరు చూడాలనుకుంటున్నదాన్ని ఎంచుకోండి.",
337
337
  "contactLinkedToMultipleProfiles": "ఈ సంప్రదింపు ఒకటి కంటే ఎక్కువ ప్రొఫైల్‌లతో అనుసంధానించబడింది. మీరు చూడాలనుకుంటున్నదాన్ని ఎంచుకోండి.",
338
338
  "signingInEllipsis": "సైన్ ఇన్ అవుతోంది…",
339
- "couldNotSwitchProfile": "ఆ ప్రొఫైల్‌కు మారడం సాధ్యం కాలేదు. దయచేసి మళ్లీ ప్రయత్నించండి."
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.",
342
+ "userIdLabel": "వినియోగదారు ఐడి: {{id}}"
340
343
  }
@@ -336,5 +336,8 @@
336
336
  "emailLinkedToMultipleProfiles": "{{email}} ایک سے زیادہ پروفائلز سے منسلک ہے۔ جسے آپ دیکھنا چاہتے ہیں اسے منتخب کریں۔",
337
337
  "contactLinkedToMultipleProfiles": "یہ رابطہ ایک سے زیادہ پروفائلز سے منسلک ہے۔ جسے آپ دیکھنا چاہتے ہیں اسے منتخب کریں۔",
338
338
  "signingInEllipsis": "سائن ان ہو رہا ہے…",
339
- "couldNotSwitchProfile": "اس پروفائل پر سوئچ نہیں کیا جا سکا۔ براہ کرم دوبارہ کوشش کریں۔"
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.",
342
+ "userIdLabel": "صارف آئی ڈی: {{id}}"
340
343
  }
@@ -747,6 +747,17 @@ export const injectPCStyles = () => {
747
747
  white-space: nowrap;
748
748
  max-width: 100%;
749
749
  }
750
+ .pc-select-user__item-id {
751
+ margin-top: 0.125rem;
752
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
753
+ font-size: 0.6875rem;
754
+ color: var(--pc-text-secondary);
755
+ line-height: 1.25;
756
+ overflow: hidden;
757
+ text-overflow: ellipsis;
758
+ white-space: nowrap;
759
+ max-width: 100%;
760
+ }
750
761
  .pc-select-user__item-status {
751
762
  font-size: 0.75rem;
752
763
  font-weight: 600;
@@ -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
+ });