@capillarytech/creatives-library 9.0.36 → 9.0.37-alpha.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 (70) hide show
  1. package/AppRoot.js +124 -0
  2. package/app-config.js +61 -0
  3. package/app.js +10 -175
  4. package/bootstrap.js +185 -0
  5. package/entry.js +67 -1
  6. package/mfe-exposed-components.js +2 -4
  7. package/package.json +2 -2
  8. package/services/api.js +9 -1
  9. package/styles/containers/layout/_layoutPage.scss +8 -6
  10. package/translations/en.json +2 -2
  11. package/utils/getDataLayer.js +15 -0
  12. package/utils/gtmTrackers/gtmEvents/creativeDetails.js +2 -1
  13. package/utils/mfeDetect.js +1 -0
  14. package/utils/mfeFirstPaintReady.js +29 -0
  15. package/utils/mfeHistory.js +62 -0
  16. package/utils/permissions.js +59 -0
  17. package/utils/rcsPayloadUtils.js +5 -2
  18. package/utils/tests/permissions.test.js +116 -0
  19. package/utils/tests/rcsPayloadUtils.test.js +54 -2
  20. package/v2Components/AccessForbidden/_accessForbidden.scss +14 -0
  21. package/v2Components/AccessForbidden/index.js +5 -3
  22. package/v2Components/AccessForbidden/messages.js +3 -2
  23. package/v2Components/CapActionButton/constants.js +1 -0
  24. package/v2Components/CapActionButton/index.js +77 -9
  25. package/v2Components/CapActionButton/index.scss +13 -0
  26. package/v2Components/CapActionButton/messages.js +13 -0
  27. package/v2Components/CapActionButton/tests/index.test.js +32 -1
  28. package/v2Components/CommonTestAndPreview/index.js +44 -15
  29. package/v2Components/CommonTestAndPreview/tests/index.test.js +78 -0
  30. package/v2Components/CommonTestAndPreview/utils.js +34 -0
  31. package/v2Components/NavigationBar/index.js +9 -7
  32. package/v2Components/NavigationBar/mfeModuleHeader.config.js +16 -0
  33. package/v2Components/NavigationBar/tests/index.test.js +39 -19
  34. package/v2Components/NoPermissionWrapper/_noPermissionWrapper.scss +12 -0
  35. package/v2Components/NoPermissionWrapper/index.js +26 -0
  36. package/v2Components/SmsFallback/index.js +6 -0
  37. package/v2Containers/App/constants.js +5 -0
  38. package/v2Containers/Assets/Gallery/_gallery.scss +6 -0
  39. package/v2Containers/Assets/Gallery/index.js +34 -10
  40. package/v2Containers/Cap/constants.js +1 -0
  41. package/v2Containers/Cap/index.js +62 -29
  42. package/v2Containers/Cap/messages.js +4 -0
  43. package/v2Containers/Cap/tests/__snapshots__/index.test.js.snap +2 -2
  44. package/v2Containers/CreativesContainer/index.js +11 -8
  45. package/v2Containers/CreativesContainer/tests/index.test.js +53 -0
  46. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +6 -6
  47. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +4 -4
  48. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +32 -32
  49. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +36 -36
  50. package/v2Containers/Rcs/components/CarouselCard.js +5 -0
  51. package/v2Containers/Rcs/components/CarouselCardButtons.js +20 -0
  52. package/v2Containers/Rcs/index.js +154 -79
  53. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +55 -4
  54. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +404 -118
  55. package/v2Containers/Rcs/tests/carouselUtils.test.js +13 -17
  56. package/v2Containers/Rcs/tests/index.test.js +141 -6
  57. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +110 -0
  58. package/v2Containers/Rcs/tests/utils.test.js +36 -0
  59. package/v2Containers/Rcs/utils.js +16 -4
  60. package/v2Containers/SmsTrai/Create/tests/__snapshots__/index.test.js.snap +8 -8
  61. package/v2Containers/SmsTrai/Edit/index.js +35 -24
  62. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +18 -18
  63. package/v2Containers/Templates/ChannelTypeIllustration.js +10 -1
  64. package/v2Containers/Templates/_templates.scss +7 -1
  65. package/v2Containers/Templates/index.js +141 -99
  66. package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +7 -0
  67. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +173 -115
  68. package/v2Containers/TemplatesV2/TemplatesV2.style.js +8 -3
  69. package/v2Containers/TemplatesV2/index.js +38 -14
  70. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +268 -268
@@ -79,7 +79,6 @@
79
79
  flex: 1;
80
80
  height: calc(100vh - 50px);
81
81
  overflow: auto;
82
- padding: 8px 16px 0;
83
82
  color: #333333;
84
83
  }
85
84
 
@@ -154,16 +153,19 @@
154
153
  }
155
154
 
156
155
  .cap-loader-box {
157
- position: fixed;
156
+ position: absolute;
158
157
  top: 0;
158
+ left: 0;
159
159
  width: 100%;
160
- height: 100%;
161
- background: rgb(255,255,255);
162
- text-align: center;
160
+ min-height: calc(100vh - 48px);
161
+ background: #ffffff;
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: center;
163
165
 
164
166
  .loader-image {
165
167
  width: 80px;
166
- margin-top: 20%;
168
+ height: auto;
167
169
  }
168
170
  }
169
171
 
@@ -63,8 +63,8 @@
63
63
  "app.v2containers.SmsTrai.Edit.traiEditTitle": "Message",
64
64
  "app.v2containers.SmsTrai.Edit.unicodeLabel": "Allow unicode characters",
65
65
  "app.v2containers.SmsTrai.Edit.unsupportedTagsValidationError": "Unsupported tags: {unsupportedTags}. Please remove them from this message.",
66
- "creatives.components.AccessForbidden.forbiddenDesc": "This page access has not been provided to you.",
67
- "creatives.components.AccessForbidden.forbiddenHeader": "Access Forbidden",
66
+ "creatives.components.AccessForbidden.forbiddenDesc": "Please contact your organisation administrator to request access.",
67
+ "creatives.components.AccessForbidden.forbiddenHeader": "You don't have permission to view this page.",
68
68
  "creatives.components.BreadCrumbs.header": "This is the BreadCrumbs component !",
69
69
  "creatives.components.CapTagList.Cancel": "Cancel",
70
70
  "creatives.components.CapTagList.Ok": "Ok",
@@ -0,0 +1,15 @@
1
+ import { isMFEMode } from './mfeDetect';
2
+ import appConfig from '../app-config';
3
+
4
+ const getDataLayerName = () => {
5
+ if (isMFEMode()) {
6
+ return `dataLayer_${appConfig.appName.replace(/-/g, '_')}`;
7
+ }
8
+ return 'dataLayer';
9
+ };
10
+
11
+ export const getDataLayer = () => {
12
+ const name = getDataLayerName();
13
+ window[name] = window[name] || [];
14
+ return window[name];
15
+ };
@@ -1,4 +1,5 @@
1
1
  import { CREATIVES } from "../../../v2Containers/App/constants";
2
+ import { getDataLayer } from '../../getDataLayer';
2
3
 
3
4
  const creativeDetails = ({
4
5
  name,
@@ -23,7 +24,7 @@ const creativeDetails = ({
23
24
  videoAdded,
24
25
  stickersAdded
25
26
  };
26
- window.dataLayer.push({
27
+ getDataLayer().push({
27
28
  creativeDetails: parsedObj,
28
29
  event: 'creativeDetails',
29
30
  });
@@ -0,0 +1 @@
1
+ export const isMFEMode = () => Boolean(window.MFE_HOST);
@@ -0,0 +1,29 @@
1
+ import { MFEEventBus } from '@capillarytech/cap-ui-utils';
2
+ import appConfig from '../app-config';
3
+ import { isMFEMode } from './mfeDetect';
4
+
5
+ // Routes whose containers emit the data-settled 'lcp' themselves —
6
+ // the first-paint emit must NOT fire for these.
7
+ // TemplatesV2 renders at relative '/v2' and '/v2/loyalty' (routes.js); in MFE mode
8
+ // the shared history is rebased on publicPath, so the real browser pathname carries
9
+ // the '/creatives/ui' prefix. Match the prefixed paths — this util reads
10
+ // window.location.pathname, not the rebased router location. (v1 standalone is
11
+ // non-MFE and irrelevant here.)
12
+ const DATA_LANDING_MATCHERS = ['/creatives/ui/v2', '/creatives/ui/v2/loyalty'];
13
+
14
+ export const isDataLandingPath = (pathname) => {
15
+ const p = (pathname || '').replace(/\/+$/, '') || '/';
16
+ return DATA_LANDING_MATCHERS.some((m) => (m instanceof RegExp ? m.test(p) : m === p));
17
+ };
18
+
19
+ export const emitFirstPaintReadyIfNotDataLanding = () => {
20
+ if (!isMFEMode() || isDataLandingPath(window.location.pathname)) return;
21
+ requestAnimationFrame(() => {
22
+ MFEEventBus.emit('mfe:segment', {
23
+ action: 'stop',
24
+ appId: appConfig.appName,
25
+ type: 'lcp',
26
+ timestamp: performance.now(),
27
+ });
28
+ });
29
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Re-base the host's shared root history for this MFE remote.
3
+ *
4
+ * The MFE host owns ONE root history instance and hands it to every remote, so host + all
5
+ * remotes stay perfectly in sync: a push on the shared instance notifies every mounted
6
+ * router directly (no popstate hack, no drift on fast remote switching).
7
+ *
8
+ * This remote's routes / <Link>s / history.push calls are written RELATIVE to its serving
9
+ * prefix. This adapter presents a basename'd VIEW of the shared history — stripping the
10
+ * prefix when the router reads the location, and prepending it when the router navigates —
11
+ * while delegating all actual navigation to the shared instance. Net effect: relative
12
+ * routing keeps working unchanged, and navigation still flows through the single shared
13
+ * history. Standalone (no host history) is unaffected; the remote keeps creating its own.
14
+ */
15
+ const addBase = (base, to) => {
16
+ if (typeof to === 'string') {
17
+ if (!to.startsWith('/')) return to; // already relative to current location
18
+ return to === '/' ? base : `${base}${to}`;
19
+ }
20
+ if (to && typeof to === 'object') {
21
+ return { ...to, pathname: addBase(base, to.pathname || '/') };
22
+ }
23
+ return to;
24
+ };
25
+
26
+ const stripBase = (base, pathname) => {
27
+ if (!pathname) return pathname;
28
+ if (pathname === base) return '/';
29
+ if (pathname.startsWith(`${base}/`)) return pathname.slice(base.length);
30
+ return pathname;
31
+ };
32
+
33
+ export default function rebaseHistory(history, rawBase) {
34
+ const base = String(rawBase || '').replace(/\/+$/, ''); // normalise: no trailing slash
35
+ if (!base) return history;
36
+ const view = loc => ({ ...loc, pathname: stripBase(base, loc.pathname) });
37
+ return {
38
+ get length() {
39
+ return history.length;
40
+ },
41
+ get action() {
42
+ return history.action;
43
+ },
44
+ get location() {
45
+ return view(history.location);
46
+ },
47
+ push: (to, state) => history.push(addBase(base, to), state),
48
+ replace: (to, state) => history.replace(addBase(base, to), state),
49
+ go: n => history.go(n),
50
+ goBack: () => history.goBack(),
51
+ goForward: () => history.goForward(),
52
+ block: (...args) => history.block(...args),
53
+ listen: listener =>
54
+ history.listen((loc, action) => listener(view(loc), action)),
55
+ createHref: to =>
56
+ history.createHref(
57
+ typeof to === 'string'
58
+ ? addBase(base, to)
59
+ : { ...to, pathname: addBase(base, (to && to.pathname) || '/') },
60
+ ),
61
+ };
62
+ }
@@ -0,0 +1,59 @@
1
+ import {
2
+ CREATIVE_VIEW,
3
+ CREATIVE_CREATE,
4
+ CREATIVE_EDIT,
5
+ CREATIVE_DELETE,
6
+ CREATIVES_UI_VIEW,
7
+ } from '../v2Containers/App/constants';
8
+
9
+ export const PERMISSIONS = {
10
+ CREATIVE_VIEW,
11
+ CREATIVE_CREATE,
12
+ CREATIVE_EDIT,
13
+ CREATIVE_DELETE,
14
+ };
15
+
16
+ export const PERMISSION_IDS = {
17
+ [CREATIVE_VIEW]: 1508,
18
+ [CREATIVE_CREATE]: 1509,
19
+ [CREATIVE_EDIT]: 1510,
20
+ [CREATIVE_DELETE]: 1511,
21
+ };
22
+
23
+ /**
24
+ * Mode-aware access resolver for the tri-modal Creatives app.
25
+ * Standalone: the `cap` slice is passed as a prop by the connected container.
26
+ * Library / MFE: the host passes a `cap` (or legacy `Global`) prop.
27
+ * Reads the prop first, then falls back — mirrors TemplatesV2's existing
28
+ * `cap.user || Global.user` resolution.
29
+ */
30
+ export const getUserAccess = (props = {}) => {
31
+ const { cap = {}, Global = {} } = props;
32
+ const user = (cap && cap.user) || (Global && Global.user) || {};
33
+ const isLoaded = !!(
34
+ user &&
35
+ typeof user === 'object' &&
36
+ Object.keys(user).length > 0
37
+ );
38
+ return {
39
+ accessiblePermissions: (isLoaded && user.accessiblePermissions) || [],
40
+ isLoaded,
41
+ };
42
+ };
43
+
44
+ export const hasPermission = (userAccess, code) => {
45
+ if (!userAccess) return false;
46
+ const granted = userAccess.accessiblePermissions || [];
47
+ const id = PERMISSION_IDS[code];
48
+ return (
49
+ granted.includes(code) ||
50
+ granted.includes(id) ||
51
+ granted.includes(String(id))
52
+ );
53
+ };
54
+
55
+ // Zero-impact view gate: honour the new CREATIVE_VIEW OR the legacy CREATIVES_UI_VIEW.
56
+ export const canViewCreatives = userAccess =>
57
+ hasPermission(userAccess, CREATIVE_VIEW) ||
58
+ (userAccess &&
59
+ (userAccess.accessiblePermissions || []).includes(CREATIVES_UI_VIEW));
@@ -59,9 +59,12 @@ export const normalizeRcsMessageContentForApi = messageContentItem => {
59
59
  rcsCardPayloadOnly.cardContent.forEach(card => {
60
60
  if (Array.isArray(card?.suggestions)) {
61
61
  card.suggestions.forEach(suggestion => {
62
- // isSaved is UI-only editor state: campaigns' send-for-approval API rejects it, and it
63
- // isn't part of the persisted payload shape.
62
+ // isSaved and urlType are UI-only editor state: campaigns' send-for-approval API
63
+ // rejects them, and they aren't part of the persisted payload shape. The dynamic tag
64
+ // (if any) already lives inline in `url` (e.g. "https://x.com{{first_name}}") — no
65
+ // separate dynamicUrlPayload field is sent for RCS.
64
66
  delete suggestion?.isSaved;
67
+ delete suggestion?.urlType;
65
68
  });
66
69
  }
67
70
  });
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Test suite for RBAC permission helpers (app/utils/permissions.js).
3
+ * Covers getUserAccess (loaded/unloaded, cap vs Global, missing perms),
4
+ * hasPermission (null guard, code/id/string-id match, empty), and
5
+ * canViewCreatives (new perm, legacy perm, null guard).
6
+ */
7
+ import {
8
+ PERMISSIONS,
9
+ PERMISSION_IDS,
10
+ getUserAccess,
11
+ hasPermission,
12
+ canViewCreatives,
13
+ } from '../permissions';
14
+ import {
15
+ CREATIVE_VIEW,
16
+ CREATIVE_CREATE,
17
+ CREATIVE_EDIT,
18
+ CREATIVE_DELETE,
19
+ CREATIVES_UI_VIEW,
20
+ } from '../../v2Containers/App/constants';
21
+
22
+ describe('permissions - exported maps', () => {
23
+ it('exposes the four creative permission codes', () => {
24
+ expect(PERMISSIONS).toEqual({
25
+ CREATIVE_VIEW,
26
+ CREATIVE_CREATE,
27
+ CREATIVE_EDIT,
28
+ CREATIVE_DELETE,
29
+ });
30
+ });
31
+
32
+ it('maps each permission code to its id', () => {
33
+ expect(PERMISSION_IDS[CREATIVE_VIEW]).toBe(1508);
34
+ expect(PERMISSION_IDS[CREATIVE_CREATE]).toBe(1509);
35
+ expect(PERMISSION_IDS[CREATIVE_EDIT]).toBe(1510);
36
+ expect(PERMISSION_IDS[CREATIVE_DELETE]).toBe(1511);
37
+ });
38
+ });
39
+
40
+ describe('getUserAccess', () => {
41
+ it('returns unloaded access with empty perms when no props are passed', () => {
42
+ expect(getUserAccess()).toEqual({ accessiblePermissions: [], isLoaded: false });
43
+ });
44
+
45
+ it('returns unloaded access for an empty props object', () => {
46
+ expect(getUserAccess({})).toEqual({ accessiblePermissions: [], isLoaded: false });
47
+ });
48
+
49
+ it('reads accessiblePermissions from cap.user when loaded', () => {
50
+ const result = getUserAccess({ cap: { user: { accessiblePermissions: [1508, 1509] } } });
51
+ expect(result).toEqual({ accessiblePermissions: [1508, 1509], isLoaded: true });
52
+ });
53
+
54
+ it('falls back to Global.user when cap.user is absent', () => {
55
+ const result = getUserAccess({ Global: { user: { accessiblePermissions: ['CREATIVE_VIEW'] } } });
56
+ expect(result).toEqual({ accessiblePermissions: ['CREATIVE_VIEW'], isLoaded: true });
57
+ });
58
+
59
+ it('is not loaded when the user object is empty', () => {
60
+ expect(getUserAccess({ cap: { user: {} } })).toEqual({ accessiblePermissions: [], isLoaded: false });
61
+ });
62
+
63
+ it('defaults accessiblePermissions to [] when a loaded user has none', () => {
64
+ const result = getUserAccess({ cap: { user: { id: 42 } } });
65
+ expect(result).toEqual({ accessiblePermissions: [], isLoaded: true });
66
+ });
67
+ });
68
+
69
+ describe('hasPermission', () => {
70
+ it('returns false when userAccess is null or undefined', () => {
71
+ expect(hasPermission(null, CREATIVE_VIEW)).toBe(false);
72
+ expect(hasPermission(undefined, CREATIVE_VIEW)).toBe(false);
73
+ });
74
+
75
+ it('returns false when accessiblePermissions is missing', () => {
76
+ expect(hasPermission({}, CREATIVE_VIEW)).toBe(false);
77
+ });
78
+
79
+ it('matches by permission code', () => {
80
+ expect(hasPermission({ accessiblePermissions: [CREATIVE_CREATE] }, CREATIVE_CREATE)).toBe(true);
81
+ });
82
+
83
+ it('matches by numeric permission id', () => {
84
+ expect(hasPermission({ accessiblePermissions: [1509] }, CREATIVE_CREATE)).toBe(true);
85
+ });
86
+
87
+ it('matches by string permission id', () => {
88
+ expect(hasPermission({ accessiblePermissions: ['1509'] }, CREATIVE_CREATE)).toBe(true);
89
+ });
90
+
91
+ it('returns false when the permission is not granted', () => {
92
+ expect(hasPermission({ accessiblePermissions: [CREATIVE_VIEW] }, CREATIVE_DELETE)).toBe(false);
93
+ expect(hasPermission({ accessiblePermissions: [] }, CREATIVE_VIEW)).toBe(false);
94
+ });
95
+ });
96
+
97
+ describe('canViewCreatives', () => {
98
+ it('allows when the new CREATIVE_VIEW permission is present', () => {
99
+ expect(canViewCreatives({ accessiblePermissions: [CREATIVE_VIEW] })).toBe(true);
100
+ expect(canViewCreatives({ accessiblePermissions: [1508] })).toBe(true);
101
+ });
102
+
103
+ it('allows via the legacy CREATIVES_UI_VIEW permission', () => {
104
+ expect(canViewCreatives({ accessiblePermissions: [CREATIVES_UI_VIEW] })).toBe(true);
105
+ });
106
+
107
+ it('returns a falsy value when userAccess is null', () => {
108
+ // short-circuits on `userAccess &&`, so returns null (falsy) rather than false
109
+ expect(canViewCreatives(null)).toBeFalsy();
110
+ });
111
+
112
+ it('returns false when neither view permission is present', () => {
113
+ expect(canViewCreatives({ accessiblePermissions: [CREATIVE_CREATE] })).toBe(false);
114
+ expect(canViewCreatives({})).toBe(false);
115
+ });
116
+ });
@@ -209,7 +209,7 @@ describe('normalizeRcsMessageContentForApi', () => {
209
209
  expect(item.rcsContent.cardContent).toEqual([{ title: 'Card' }]);
210
210
  });
211
211
 
212
- it('strips isSaved from each suggestion in cardContent', () => {
212
+ it('strips isSaved and urlType from each suggestion in cardContent', () => {
213
213
  const item = {
214
214
  rcsContent: {
215
215
  cardContent: [
@@ -217,7 +217,9 @@ describe('normalizeRcsMessageContentForApi', () => {
217
217
  title: 'Card 1',
218
218
  suggestions: [
219
219
  { type: 'QUICK_REPLY', text: 'Yes', isSaved: true },
220
- { type: 'CTA', text: 'Go', url: 'https://example.com', isSaved: false },
220
+ {
221
+ type: 'CTA', text: 'Go', url: 'https://example.com', urlType: 'STATIC', isSaved: false,
222
+ },
221
223
  ],
222
224
  },
223
225
  ],
@@ -230,6 +232,56 @@ describe('normalizeRcsMessageContentForApi', () => {
230
232
  ]);
231
233
  });
232
234
 
235
+ it('keeps type "CTA" and the inline tag in url, but drops urlType, for a dynamic-URL suggestion', () => {
236
+ const item = {
237
+ rcsContent: {
238
+ cardContent: [
239
+ {
240
+ title: 'Card 1',
241
+ suggestions: [
242
+ {
243
+ type: 'CTA',
244
+ text: 'Visit here',
245
+ url: 'https://example.com/{{first_name}}',
246
+ urlType: 'DYNAMIC',
247
+ isSaved: true,
248
+ },
249
+ ],
250
+ },
251
+ ],
252
+ },
253
+ };
254
+ normalizeRcsMessageContentForApi(item);
255
+ expect(item.rcsContent.cardContent[0].suggestions).toEqual([
256
+ {
257
+ type: 'CTA',
258
+ text: 'Visit here',
259
+ url: 'https://example.com/{{first_name}}',
260
+ },
261
+ ]);
262
+ });
263
+
264
+ it('leaves phone number and quick reply suggestions untouched by the urlType stripping', () => {
265
+ const item = {
266
+ rcsContent: {
267
+ cardContent: [
268
+ {
269
+ title: 'Card 1',
270
+ suggestions: [
271
+ {
272
+ type: 'PHONE_NUMBER', text: 'Call', phoneNumber: '918987678', isSaved: true,
273
+ },
274
+ ],
275
+ },
276
+ ],
277
+ },
278
+ };
279
+ normalizeRcsMessageContentForApi(item);
280
+ expect(item.rcsContent.cardContent[0].suggestions).toEqual([
281
+ { type: 'PHONE_NUMBER', text: 'Call', phoneNumber: '918987678' },
282
+ ]);
283
+ });
284
+
233
285
  it('handles cardContent with no suggestions gracefully', () => {
234
286
  const item = {
235
287
  rcsContent: {
@@ -0,0 +1,14 @@
1
+ // Access-denied screen: rbac-lock icon + heading + subtext (matches the shared
2
+ // "no access" screen used across Loyalty+ / Org Settings).
3
+ .no-access-icon {
4
+ font-size: 48px;
5
+ color: #8c8c8c;
6
+ }
7
+ .no-access-title {
8
+ margin-top: 16px;
9
+ }
10
+ .no-access-text {
11
+ margin-top: 8px;
12
+ color: #6b778c;
13
+ max-width: 420px;
14
+ }
@@ -1,14 +1,16 @@
1
1
  import React from 'react';
2
2
  import { FormattedMessage } from 'react-intl';
3
- import { CapRow, CapHeading } from '@capillarytech/cap-ui-library';
3
+ import { CapRow, CapHeading, CapIcon } from '@capillarytech/cap-ui-library';
4
4
  import messages from './messages';
5
+ import './_accessForbidden.scss';
5
6
 
6
7
  const AccessForbidden = () => (
7
8
  <CapRow type='flex' vertical justify='center' align='middle' className="margin-t-32 text-align-center">
8
- <CapHeading type="h2">
9
+ <CapIcon type="rbac-lock" className="no-access-icon" />
10
+ <CapHeading type="h3" className="no-access-title">
9
11
  <FormattedMessage {...messages.forbiddenHeader} />
10
12
  </CapHeading>
11
- <CapHeading type="h3" className="margin-t-24">
13
+ <CapHeading type="h6" className="no-access-text">
12
14
  <FormattedMessage {...messages.forbiddenDesc} />
13
15
  </CapHeading>
14
16
  </CapRow>
@@ -5,10 +5,11 @@ const prefix = 'creatives.components.AccessForbidden';
5
5
  export default defineMessages({
6
6
  forbiddenHeader: {
7
7
  id: `${prefix}.forbiddenHeader`,
8
- defaultMessage: 'Access Forbidden',
8
+ defaultMessage: "You don't have permission to view this page.",
9
9
  },
10
10
  forbiddenDesc: {
11
11
  id: `${prefix}.forbiddenDesc`,
12
- defaultMessage: 'This page access has not been provided to you.',
12
+ defaultMessage:
13
+ 'Please contact your organisation administrator to request access.',
13
14
  },
14
15
  });
@@ -43,6 +43,7 @@ export const RCS_CTA_URL_TYPE = {
43
43
  export const BTN_MAX_LENGTH = 20;
44
44
  export const PHONE_NUMBER_MAX_LENGTH = 15;
45
45
  export const URL_MAX_LENGTH = 2000;
46
+ export const DYNAMIC_URL_SUFFIX = '{{1}}';
46
47
 
47
48
  export const CTA_TYPE_RADIO_OPTIONS = [
48
49
  { label: <FormattedMessage {...messages.ctaPhoneNo} />, value: RCS_BUTTON_TYPES.PHONE_NUMBER },