@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
@@ -554,15 +554,13 @@ describe('buildCarouselCardsForPreview', () => {
554
554
  expect(
555
555
  buildCarouselCardsForPreview(undefined, {
556
556
  isFullMode: false,
557
- rcsVarRegex,
558
- resolveTemplateWithMap: jest.fn(),
557
+ resolveCarouselTemplateWithMap: jest.fn(),
559
558
  }),
560
559
  ).toEqual([]);
561
560
  expect(
562
561
  buildCarouselCardsForPreview([], {
563
562
  isFullMode: false,
564
- rcsVarRegex,
565
- resolveTemplateWithMap: jest.fn(),
563
+ resolveCarouselTemplateWithMap: jest.fn(),
566
564
  }),
567
565
  ).toEqual([]);
568
566
  });
@@ -571,19 +569,18 @@ describe('buildCarouselCardsForPreview', () => {
571
569
  expect(
572
570
  buildCarouselCardsForPreview(null, {
573
571
  isFullMode: false,
574
- rcsVarRegex,
575
- resolveTemplateWithMap: jest.fn(),
572
+ resolveCarouselTemplateWithMap: jest.fn(),
576
573
  }),
577
574
  ).toEqual([]);
578
575
  });
579
576
 
580
- it('passes title/description through unresolved in full mode without calling resolveTemplateWithMap', () => {
581
- const resolveTemplateWithMap = jest.fn();
577
+ it('passes title/description through unresolved in full mode without calling resolveCarouselTemplateWithMap', () => {
578
+ const resolveCarouselTemplateWithMap = jest.fn();
582
579
  const out = buildCarouselCardsForPreview(
583
580
  [{ title: 'Hi {{a}}', description: 'Bye {{b}}' }],
584
- { isFullMode: true, rcsVarRegex, resolveTemplateWithMap },
581
+ { isFullMode: true, resolveCarouselTemplateWithMap },
585
582
  );
586
- expect(resolveTemplateWithMap).not.toHaveBeenCalled();
583
+ expect(resolveCarouselTemplateWithMap).not.toHaveBeenCalled();
587
584
  expect(out[0].title).toBe('Hi {{a}}');
588
585
  expect(out[0].bodyText).toBe('Bye {{b}}');
589
586
  });
@@ -595,7 +592,7 @@ describe('buildCarouselCardsForPreview', () => {
595
592
  { title: 'Hi {{a}}', description: 'Bye {{b}} {{c}}' },
596
593
  { title: 'Solo {{d}}', description: 'none' },
597
594
  ],
598
- { isFullMode: false, rcsVarRegex, resolveTemplateWithMap },
595
+ { isFullMode: false, resolveCarouselTemplateWithMap },
599
596
  );
600
597
  expect(resolveCarouselTemplateWithMap).toHaveBeenNthCalledWith(1, 'Hi {{a}}', 0, false);
601
598
  expect(resolveCarouselTemplateWithMap).toHaveBeenNthCalledWith(2, 'Bye {{b}} {{c}}', 0, true);
@@ -610,8 +607,7 @@ describe('buildCarouselCardsForPreview', () => {
610
607
  it('defaults missing card fields, including a default {} for a missing array item', () => {
611
608
  const out = buildCarouselCardsForPreview([undefined], {
612
609
  isFullMode: true,
613
- rcsVarRegex,
614
- resolveTemplateWithMap: jest.fn(),
610
+ resolveCarouselTemplateWithMap: jest.fn(),
615
611
  });
616
612
  expect(out[0]).toEqual({
617
613
  mediaType: '',
@@ -635,7 +631,7 @@ describe('buildCarouselCardsForPreview', () => {
635
631
  description: '',
636
632
  },
637
633
  ],
638
- { isFullMode: true, rcsVarRegex, resolveTemplateWithMap: jest.fn() },
634
+ { isFullMode: true, resolveCarouselTemplateWithMap: jest.fn() },
639
635
  );
640
636
  expect(out[0].mediaType).toBe('image');
641
637
  expect(out[0].imageSrc).toBe('img.png');
@@ -651,7 +647,7 @@ describe('buildCarouselCardsForPreview', () => {
651
647
  videoAsset: { videoSrc: 'v.mp4', videoThumbnail: 'other-thumb.png' },
652
648
  },
653
649
  ],
654
- { isFullMode: true, rcsVarRegex, resolveTemplateWithMap: jest.fn() },
650
+ { isFullMode: true, resolveCarouselTemplateWithMap: jest.fn() },
655
651
  );
656
652
  expect(out[0].videoPreviewImg).toBe('thumb.png');
657
653
  expect(out[0].videoSrc).toBe('v.mp4');
@@ -665,7 +661,7 @@ describe('buildCarouselCardsForPreview', () => {
665
661
  videoAsset: { videoSrc: 'v.mp4', videoThumbnail: 'other-thumb.png' },
666
662
  },
667
663
  ],
668
- { isFullMode: true, rcsVarRegex, resolveTemplateWithMap: jest.fn() },
664
+ { isFullMode: true, resolveCarouselTemplateWithMap: jest.fn() },
669
665
  );
670
666
  expect(out[0].videoPreviewImg).toBe('other-thumb.png');
671
667
  });
@@ -673,7 +669,7 @@ describe('buildCarouselCardsForPreview', () => {
673
669
  it('defaults videoPreviewImg and videoSrc to empty string when nothing is set', () => {
674
670
  const out = buildCarouselCardsForPreview(
675
671
  [{ mediaType: RCS_MEDIA_TYPES.IMAGE }],
676
- { isFullMode: true, rcsVarRegex, resolveTemplateWithMap: jest.fn() },
672
+ { isFullMode: true, resolveCarouselTemplateWithMap: jest.fn() },
677
673
  );
678
674
  expect(out[0].videoPreviewImg).toBe('');
679
675
  expect(out[0].videoSrc).toBe('');
@@ -570,8 +570,8 @@ describe('RCS carousel label resolution in non-full mode', () => {
570
570
  expect(card0NameAreas.length).toBe(2);
571
571
 
572
572
  act(() => {
573
- const slotId = nameVarAreas.at(0).prop('id') || nameVarAreas.at(0).key();
574
- nameVarAreas.at(0).props().onChange({ target: { id: slotId, value: 'madhavi' } });
573
+ const slotId = card0NameAreas.at(0).prop('id') || card0NameAreas.at(0).key();
574
+ card0NameAreas.at(0).props().onChange({ target: { id: slotId, value: 'madhavi' } });
575
575
  });
576
576
  wrapper.update();
577
577
 
@@ -1189,6 +1189,130 @@ describe('RCS createPayload', () => {
1189
1189
  expect(Object.prototype.hasOwnProperty.call(card, 'cardVarMapped')).toBe(true);
1190
1190
  });
1191
1191
 
1192
+ it('disables Done in non-full mode when a CTA button URL still holds the unresolved {{1}} placeholder', () => {
1193
+ const templateData = {
1194
+ name: 'PromoCard',
1195
+ type: 'RCS',
1196
+ versions: {
1197
+ base: {
1198
+ content: {
1199
+ RCS: {
1200
+ rcsContent: {
1201
+ cardType: 'STANDALONE',
1202
+ cardSettings: { cardOrientation: 'VERTICAL' },
1203
+ cardContent: [
1204
+ {
1205
+ title: 'Title',
1206
+ description: 'Desc',
1207
+ mediaType: 'VIDEO',
1208
+ media: {
1209
+ mediaUrl: 'https://cdn.example.com/video.mp4',
1210
+ thumbnailUrl: 'https://cdn.example.com/video-thumb.jpg',
1211
+ },
1212
+ suggestions: [
1213
+ { type: 'CTA', text: 'Visit here', url: 'https://example.com{{1}}' },
1214
+ ],
1215
+ },
1216
+ ],
1217
+ contentType: 'RICHCARD',
1218
+ },
1219
+ },
1220
+ },
1221
+ },
1222
+ },
1223
+ };
1224
+
1225
+ const wrapper = mountWithIntl(
1226
+ <Provider store={store}>
1227
+ <Rcs
1228
+ actions={{
1229
+ clearCreateResponse: jest.fn(),
1230
+ getTemplateDetails: jest.fn(),
1231
+ uploadRcsAsset: jest.fn(),
1232
+ clearRcsMediaAsset: jest.fn(),
1233
+ editTemplate: jest.fn(),
1234
+ clearEditResponse: jest.fn(),
1235
+ }}
1236
+ globalActions={{ fetchSchemaForEntity }}
1237
+ onCreateComplete={onCreateComplete}
1238
+ handleClose={handleClose}
1239
+ intl={{ formatMessage }}
1240
+ location={{ pathname: '/rcs/create', query: { type: false, module: 'default' }, search: '' }}
1241
+ params={params}
1242
+ templateData={templateData}
1243
+ rcsData={{}}
1244
+ isFullMode={false}
1245
+ isEditFlow={false}
1246
+ loadingTags={false}
1247
+ metaEntities={[]}
1248
+ isDltEnabled={false}
1249
+ smsRegister={'DLT'}
1250
+ getFormData={jest.fn()}
1251
+ />
1252
+ </Provider>,
1253
+ );
1254
+
1255
+ const doneBtn = wrapper.find('CapButton.rcs-done-btn').at(0);
1256
+ expect(doneBtn.exists()).toBe(true);
1257
+ expect(doneBtn.prop('disabled')).toBe(true);
1258
+ });
1259
+
1260
+ it('disables Send for Approval in full-mode creation when a CTA button URL still holds the unresolved {{1}} placeholder', () => {
1261
+ const wrapper = mountWithIntl(
1262
+ <Provider store={store}>
1263
+ <Rcs
1264
+ actions={{
1265
+ clearCreateResponse: jest.fn(),
1266
+ getTemplateDetails: jest.fn(),
1267
+ uploadRcsAsset: jest.fn(),
1268
+ clearRcsMediaAsset: jest.fn(),
1269
+ editTemplate: jest.fn(),
1270
+ clearEditResponse: jest.fn(),
1271
+ }}
1272
+ globalActions={{ fetchSchemaForEntity }}
1273
+ onCreateComplete={onCreateComplete}
1274
+ handleClose={handleClose}
1275
+ intl={{ formatMessage }}
1276
+ location={{ pathname: '/rcs/create', query: { type: false, module: 'default' }, search: '' }}
1277
+ params={params}
1278
+ templateData={{}}
1279
+ rcsData={{}}
1280
+ isFullMode
1281
+ isEditFlow={false}
1282
+ loadingTags={false}
1283
+ metaEntities={[]}
1284
+ isDltEnabled={false}
1285
+ smsRegister={'DLT'}
1286
+ getFormData={jest.fn()}
1287
+ />
1288
+ </Provider>,
1289
+ );
1290
+
1291
+ act(() => {
1292
+ wrapper.find('#rcs_template_name_input').at(0).simulate('change', { target: { value: 'PromoCard' } });
1293
+ });
1294
+ wrapper.update();
1295
+
1296
+ act(() => {
1297
+ wrapper.find('#rcs_template_message_textarea').at(0).simulate('change', { target: { value: 'Hello there' } });
1298
+ });
1299
+ wrapper.update();
1300
+
1301
+ const actionButton = wrapper.find('.cap-action-button-mock').at(0);
1302
+ expect(actionButton.exists()).toBe(true);
1303
+ act(() => {
1304
+ actionButton.prop('updateButtonChange')(
1305
+ { type: 'CTA', text: 'Visit here', url: 'https://example.com{{1}}' },
1306
+ 0,
1307
+ );
1308
+ });
1309
+ wrapper.update();
1310
+
1311
+ const doneBtn = wrapper.find('CapButton.rcs-done-btn').at(0);
1312
+ expect(doneBtn.exists()).toBe(true);
1313
+ expect(doneBtn.prop('disabled')).toBe(true);
1314
+ });
1315
+
1192
1316
  it('should sync duplicate placeholders via cardVarMapped in non-full mode (typing once updates all occurrences)', () => {
1193
1317
  const templateData = {
1194
1318
  name: 'DupVarsTemplate',
@@ -1286,7 +1410,12 @@ describe('RCS createPayload', () => {
1286
1410
  cardContent: [
1287
1411
  {
1288
1412
  title: 'Hello {{user_name}} and {{user_name}}',
1289
- description: 'Hi {{user_name}}',
1413
+ // Distinct from the title's var name: VarSegmentMessageEditor ids are
1414
+ // `${token}_${localSegmentIndex}`, not scoped per-field, so reusing
1415
+ // {{user_name}} here would collide with the title's own id and get
1416
+ // picked up by the `{{user_name}}_` filter below once the tag-select
1417
+ // renames the title's first occurrence.
1418
+ description: 'Hi {{other_var}}',
1290
1419
  mediaType: 'IMAGE',
1291
1420
  media: { mediaUrl: '', thumbnailUrl: '' },
1292
1421
  cardVarMapped: {},
@@ -1352,12 +1481,18 @@ describe('RCS createPayload', () => {
1352
1481
  });
1353
1482
  wrapper.update();
1354
1483
 
1355
- const updatedTitleVarAreas = wrapper.find('CapInputTextArea').filterWhere((n) => {
1484
+ // The focused occurrence's own token is replaced with the picked tag, while the untouched
1485
+ // duplicate keeps its original {{user_name}} token but syncs to the same resolved value.
1486
+ const renamedVarArea = wrapper.find('CapInputTextArea').filterWhere((n) => {
1487
+ const id = n.prop('id') || '';
1488
+ return id.includes('{{first_name}}_');
1489
+ });
1490
+ const duplicateVarArea = wrapper.find('CapInputTextArea').filterWhere((n) => {
1356
1491
  const id = n.prop('id') || '';
1357
1492
  return id.includes('{{user_name}}_');
1358
1493
  });
1359
- expect(updatedTitleVarAreas.at(0).prop('value')).toBe('{{first_name}}');
1360
- expect(updatedTitleVarAreas.at(1).prop('value')).toBe('{{first_name}}');
1494
+ expect(renamedVarArea.at(0).prop('value')).toBe('{{first_name}}');
1495
+ expect(duplicateVarArea.at(0).prop('value')).toBe('{{first_name}}');
1361
1496
  });
1362
1497
 
1363
1498
  it('should keep two tags + freetext inside the variable textarea in non-full mode edit (not merged into static text)', () => {
@@ -364,6 +364,14 @@ describe('rcsLibraryHydrationUtils', () => {
364
364
  expect(out).toEqual({ 1: 'x', user_name: 'y' });
365
365
  });
366
366
 
367
+ it('keeps function-style tag keys with parentheses (voucher/offer/badge tags)', () => {
368
+ const input = {
369
+ 'voucher(1190738)': 'SAVE20',
370
+ 'badges_enroll_no_expiry(123)': 'Gold',
371
+ };
372
+ expect(pickRcsCardVarMappedEntries(input)).toEqual(input);
373
+ });
374
+
367
375
  it('returns {} for an empty object', () => {
368
376
  expect(pickRcsCardVarMappedEntries({})).toEqual({});
369
377
  });
@@ -439,6 +447,7 @@ describe('rcsLibraryHydrationUtils', () => {
439
447
  template: 'Hello',
440
448
  templateName: 'T1',
441
449
  registeredSenderIds: ['S1'],
450
+ templateVariableMapping: { '{#a#}_0': { data: 'x', count: 1 } },
442
451
  },
443
452
  },
444
453
  });
@@ -497,6 +506,7 @@ describe('rcsLibraryHydrationUtils', () => {
497
506
  expect(out.smsFallBackContent.templateConfigs).toEqual({
498
507
  templateName: 'ConfigName',
499
508
  registeredSenderIds: ['H1'],
509
+ templateVariableMapping: { a: { data: '1', count: 1 } },
500
510
  });
501
511
  expect(out.smsFallBackContent.rcsSmsFallbackVarMapped).toEqual({ a: '1' });
502
512
  });
@@ -537,6 +547,25 @@ describe('rcsLibraryHydrationUtils', () => {
537
547
  });
538
548
  });
539
549
 
550
+ it('non-full mode: DLT campaign with registeredSenderIds carries the var map into templateConfigs.templateVariableMapping (so it survives normalizeRcsMessageContentForApi stripping rcsSmsFallbackVarMapped)', () => {
551
+ enableTraiDlt();
552
+ const out = buildSmsFallBackContentForPayload({
553
+ smsFallbackData: {
554
+ message: 'Hi {#var#} your OTP is {#var#}.Capillary Tech.',
555
+ templateName: 'TEST_FALLBACK',
556
+ registeredSenderIds: ['CPLRYT'],
557
+ rcsSmsFallbackVarMapped: { '{#var#}_1': 'test1', '{#var#}_3': 'test2' },
558
+ },
559
+ templateData: {},
560
+ isFullMode: false,
561
+ smsRegister: 'DLT',
562
+ });
563
+ expect(out.smsFallBackContent.templateConfigs.templateVariableMapping).toEqual({
564
+ '{#var#}_1': { data: 'test1', count: 1 },
565
+ '{#var#}_3': { data: 'test2', count: 2 },
566
+ });
567
+ });
568
+
540
569
  it('non-full mode: DLT campaign but no derivable templateConfigs omits templateConfigs', () => {
541
570
  enableTraiDlt();
542
571
  const out = buildSmsFallBackContentForPayload({
@@ -656,6 +685,45 @@ describe('rcsLibraryHydrationUtils', () => {
656
685
  expect(warnSpy).toHaveBeenCalled();
657
686
  });
658
687
 
688
+ it('falls back to templateConfigs.templateName when root-level name fields are absent', () => {
689
+ const out = resolveSmsFallbackHydrationFromDetails({
690
+ smsFallBackContent: {
691
+ templateConfigs: { templateName: 'ConfigTemplateName' },
692
+ },
693
+ });
694
+ expect(out.nextSmsState.templateName).toBe('ConfigTemplateName');
695
+ });
696
+
697
+ it('falls back to templateConfigs.name when no other name field is present anywhere', () => {
698
+ const out = resolveSmsFallbackHydrationFromDetails({
699
+ smsFallBackContent: {
700
+ templateConfigs: { name: 'ConfigName' },
701
+ },
702
+ });
703
+ expect(out.nextSmsState.templateName).toBe('ConfigName');
704
+ });
705
+
706
+ it('falls back to the root-level templateName when templateConfigs has no name field', () => {
707
+ const out = resolveSmsFallbackHydrationFromDetails({
708
+ smsFallBackContent: {
709
+ templateName: 'RootName',
710
+ templateConfigs: { registeredSenderIds: ['123'] },
711
+ },
712
+ });
713
+ expect(out.nextSmsState.templateName).toBe('RootName');
714
+ });
715
+
716
+ it('prefers the root templateName over a conflicting nested templateConfigs.name', () => {
717
+ const out = resolveSmsFallbackHydrationFromDetails({
718
+ smsFallBackContent: {
719
+ templateName: 'RootName',
720
+ smsTemplateName: '',
721
+ templateConfigs: { name: 'NestedName', header: ['H1'] },
722
+ },
723
+ });
724
+ expect(out.nextSmsState.templateName).toBe('RootName');
725
+ });
726
+
659
727
  it('is meaningful from var map alone, with no template name or message text', () => {
660
728
  const out = resolveSmsFallbackHydrationFromDetails({
661
729
  smsFallBackContent: { rcsSmsFallbackVarMapped: { '{#a#}_0': 'v' } },
@@ -705,6 +773,48 @@ describe('rcsLibraryHydrationUtils', () => {
705
773
  ).toBe('Direct Template');
706
774
  });
707
775
 
776
+ it('prefers templateConfigs.template (raw, {#var#} intact) over smsContent/message for DLT', () => {
777
+ const out = resolveSmsFallbackHydrationFromDetails({
778
+ smsFallBackContent: {
779
+ smsContent: 'hey user this is a test',
780
+ message: 'hey user this is a test',
781
+ templateConfigs: {
782
+ templateName: 'RCSDLT2',
783
+ template: 'hey{#var#}test',
784
+ templateVariableMapping: {
785
+ '{#var#}_1': { data: ' user this is a ', count: 1 },
786
+ },
787
+ },
788
+ },
789
+ });
790
+ expect(out.nextSmsState.content).toBe('hey{#var#}test');
791
+ expect(out.nextSmsState.templateContent).toBe('hey{#var#}test');
792
+ expect(out.nextSmsState.rcsSmsFallbackVarMapped).toEqual({ '{#var#}_1': ' user this is a ' });
793
+ });
794
+
795
+ it('merges templateVariableMapping-derived slots with an explicit rcsSmsFallbackVarMapped (explicit wins)', () => {
796
+ const out = resolveSmsFallbackHydrationFromDetails({
797
+ smsFallBackContent: {
798
+ smsContent: 'hey user this is a test',
799
+ rcsSmsFallbackVarMapped: { '{#var#}_1': 'explicit override' },
800
+ templateConfigs: {
801
+ template: 'hey{#var#}test',
802
+ templateVariableMapping: {
803
+ '{#var#}_1': { data: ' user this is a ', count: 1 },
804
+ },
805
+ },
806
+ },
807
+ });
808
+ expect(out.nextSmsState.rcsSmsFallbackVarMapped).toEqual({ '{#var#}_1': 'explicit override' });
809
+ });
810
+
811
+ it('falls back to smsContent when templateConfigs.template is absent (non-DLT)', () => {
812
+ const out = resolveSmsFallbackHydrationFromDetails({
813
+ smsFallBackContent: { smsContent: 'Hi {{first_name}}, your OTP is 1234' },
814
+ });
815
+ expect(out.nextSmsState.content).toBe('Hi {{first_name}}, your OTP is 1234');
816
+ });
817
+
708
818
  it('joins an updated-sms-editor array into the fallback message', () => {
709
819
  const out = resolveSmsFallbackHydrationFromDetails({
710
820
  smsFallBackContent: {
@@ -781,6 +781,42 @@ describe('RCS utils', () => {
781
781
  expect(rcsTitle).toBe('');
782
782
  expect(rcsDesc).toBe('Visit Store for {{loyalty_points}} discount');
783
783
  });
784
+
785
+ it('carousel card 1+: cardSlotOffset param resolves against its own global slots, not card 0\'s', () => {
786
+ // Full, correctly-populated carousel-wide map: slot 1 = card 0 title (user_id_b64), slot 2 =
787
+ // card 0 desc (voucher(1190738)), slot 3 = card 1 title (user_id_b64), slot 4 = card 1 desc
788
+ // (first_name).
789
+ const fullCarouselWideMap = {
790
+ 1: '{{user_id_b64}}',
791
+ 2: '{{voucher(1190738)}}',
792
+ 3: '{{user_id_b64}}',
793
+ 4: '{{first_name}}',
794
+ };
795
+ // Card 1's own title/description, called with cardSlotOffset=2 (card 0 contributed 2 slots) —
796
+ // the same way mapRcsCardContentForConsumerWithResolvedTags now calls it per card.
797
+ const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
798
+ 'This is a park {{3}}',
799
+ 'This park has a cat and a bird {{4}}',
800
+ fullCarouselWideMap,
801
+ true,
802
+ false,
803
+ false,
804
+ 2,
805
+ );
806
+ expect(rcsTitle).toBe('This is a park {{user_id_b64}}');
807
+ expect(rcsDesc).toBe('This park has a cat and a bird {{first_name}}');
808
+ });
809
+
810
+ it('omitting cardSlotOffset defaults to 0 (single-card / standalone callers unaffected)', () => {
811
+ const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
812
+ 'Hi {{1}}',
813
+ 'Pts {{2}}',
814
+ { 1: '{{first_name}}', 2: '{{loyalty_points}}' },
815
+ true,
816
+ );
817
+ expect(rcsTitle).toBe('Hi {{first_name}}');
818
+ expect(rcsDesc).toBe('Pts {{loyalty_points}}');
819
+ });
784
820
  });
785
821
 
786
822
  describe('sanitizeCardVarMappedValue', () => {
@@ -6,6 +6,7 @@ import {
6
6
  RCS_NUMERIC_VAR_NAME_REGEX,
7
7
  REGEX_SPECIAL_CHARS_ESCAPE_PATTERN,
8
8
  RCS_STRIP_MUSTACHE_DELIMITERS_REGEX,
9
+ RCS_MUSTACHE_WRAPPED_REGEX,
9
10
  } from './constants';
10
11
  import './index.scss';
11
12
  // import { formatMessage } from '../../../utils/intl';
@@ -341,6 +342,8 @@ export function resolveRcsCardPreviewStrings(
341
342
  cardVarMapped,
342
343
  isLibraryMode = false,
343
344
  textOnlyCard = false,
345
+ preserveLiteralValueTokens = false,
346
+ cardSlotOffset = 0,
344
347
  ) {
345
348
  const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
346
349
  const getVarNameFromToken = (token = '') =>
@@ -389,8 +392,8 @@ export function resolveRcsCardPreviewStrings(
389
392
  ? 0
390
393
  : (effectiveTitle.match(rcsVarRegex) || []).length;
391
394
  return {
392
- rcsTitle: textOnlyCard ? '' : resolveTemplateWithMap(effectiveTitle, 0),
393
- rcsDesc: resolveTemplateWithMap(String(description || ''), titleVarCount),
395
+ rcsTitle: textOnlyCard ? '' : resolveTemplateWithMap(effectiveTitle, cardSlotOffset),
396
+ rcsDesc: resolveTemplateWithMap(String(description || ''), cardSlotOffset + titleVarCount),
394
397
  };
395
398
  }
396
399
 
@@ -411,6 +414,7 @@ export function mapRcsCardContentForConsumerWithResolvedTags(
411
414
  : {};
412
415
  const list = Array.isArray(cardContentArray) ? cardContentArray : [];
413
416
  const isLibraryMode = isFullMode !== true;
417
+ let carouselSlotOffset = 0;
414
418
  return list.map((card) => {
415
419
  if (!card || typeof card !== 'object') return card;
416
420
  const nested =
@@ -421,13 +425,21 @@ export function mapRcsCardContentForConsumerWithResolvedTags(
421
425
  const nestedClean = pickRcsCardVarMappedEntries(nested);
422
426
  const merged = { ...rootClean, ...nestedClean };
423
427
  const textOnly = isRcsTextOnlyCardMediaType(card.mediaType);
428
+ const cardTitle = card.title ?? '';
429
+ const cardDescription = card.description ?? '';
424
430
  const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
425
- card.title ?? '',
426
- card.description ?? '',
431
+ cardTitle,
432
+ cardDescription,
427
433
  merged,
428
434
  isLibraryMode,
429
435
  textOnly,
436
+ true,
437
+ carouselSlotOffset,
430
438
  );
439
+ const effectiveTitleForCount = textOnly ? '' : String(cardTitle);
440
+ const titleVarCount = (effectiveTitleForCount.match(rcsVarRegex) || []).length;
441
+ const descVarCount = (String(cardDescription).match(rcsVarRegex) || []).length;
442
+ carouselSlotOffset += titleVarCount + descVarCount;
431
443
  const { cardVarMapped: _drop, ...cardRest } = card;
432
444
 
433
445
  return {
@@ -112,8 +112,8 @@ exports[`Creatives SmsTraiCreate test/> done button should be disabled if blank
112
112
  "app.v2containers.SmsTrai.Edit.traiEditTitle": "Message",
113
113
  "app.v2containers.SmsTrai.Edit.unicodeLabel": "Allow unicode characters",
114
114
  "app.v2containers.SmsTrai.Edit.unsupportedTagsValidationError": "Unsupported tags: {unsupportedTags}. Please remove them from this message.",
115
- "creatives.components.AccessForbidden.forbiddenDesc": "This page access has not been provided to you.",
116
- "creatives.components.AccessForbidden.forbiddenHeader": "Access Forbidden",
115
+ "creatives.components.AccessForbidden.forbiddenDesc": "Please contact your organisation administrator to request access.",
116
+ "creatives.components.AccessForbidden.forbiddenHeader": "You don't have permission to view this page.",
117
117
  "creatives.components.BreadCrumbs.header": "This is the BreadCrumbs component !",
118
118
  "creatives.components.CapTagList.Cancel": "Cancel",
119
119
  "creatives.components.CapTagList.Ok": "Ok",
@@ -3273,8 +3273,8 @@ exports[`Creatives SmsTraiCreate test/> invalid file drop 1`] = `
3273
3273
  "app.v2containers.SmsTrai.Edit.traiEditTitle": "Message",
3274
3274
  "app.v2containers.SmsTrai.Edit.unicodeLabel": "Allow unicode characters",
3275
3275
  "app.v2containers.SmsTrai.Edit.unsupportedTagsValidationError": "Unsupported tags: {unsupportedTags}. Please remove them from this message.",
3276
- "creatives.components.AccessForbidden.forbiddenDesc": "This page access has not been provided to you.",
3277
- "creatives.components.AccessForbidden.forbiddenHeader": "Access Forbidden",
3276
+ "creatives.components.AccessForbidden.forbiddenDesc": "Please contact your organisation administrator to request access.",
3277
+ "creatives.components.AccessForbidden.forbiddenHeader": "You don't have permission to view this page.",
3278
3278
  "creatives.components.BreadCrumbs.header": "This is the BreadCrumbs component !",
3279
3279
  "creatives.components.CapTagList.Cancel": "Cancel",
3280
3280
  "creatives.components.CapTagList.Ok": "Ok",
@@ -6417,8 +6417,8 @@ exports[`Creatives SmsTraiCreate test/> renders UI 1`] = `
6417
6417
  "app.v2containers.SmsTrai.Edit.traiEditTitle": "Message",
6418
6418
  "app.v2containers.SmsTrai.Edit.unicodeLabel": "Allow unicode characters",
6419
6419
  "app.v2containers.SmsTrai.Edit.unsupportedTagsValidationError": "Unsupported tags: {unsupportedTags}. Please remove them from this message.",
6420
- "creatives.components.AccessForbidden.forbiddenDesc": "This page access has not been provided to you.",
6421
- "creatives.components.AccessForbidden.forbiddenHeader": "Access Forbidden",
6420
+ "creatives.components.AccessForbidden.forbiddenDesc": "Please contact your organisation administrator to request access.",
6421
+ "creatives.components.AccessForbidden.forbiddenHeader": "You don't have permission to view this page.",
6422
6422
  "creatives.components.BreadCrumbs.header": "This is the BreadCrumbs component !",
6423
6423
  "creatives.components.CapTagList.Cancel": "Cancel",
6424
6424
  "creatives.components.CapTagList.Ok": "Ok",
@@ -9578,8 +9578,8 @@ exports[`Creatives SmsTraiCreate test/> valid file drop 1`] = `
9578
9578
  "app.v2containers.SmsTrai.Edit.traiEditTitle": "Message",
9579
9579
  "app.v2containers.SmsTrai.Edit.unicodeLabel": "Allow unicode characters",
9580
9580
  "app.v2containers.SmsTrai.Edit.unsupportedTagsValidationError": "Unsupported tags: {unsupportedTags}. Please remove them from this message.",
9581
- "creatives.components.AccessForbidden.forbiddenDesc": "This page access has not been provided to you.",
9582
- "creatives.components.AccessForbidden.forbiddenHeader": "Access Forbidden",
9581
+ "creatives.components.AccessForbidden.forbiddenDesc": "Please contact your organisation administrator to request access.",
9582
+ "creatives.components.AccessForbidden.forbiddenHeader": "You don't have permission to view this page.",
9583
9583
  "creatives.components.BreadCrumbs.header": "This is the BreadCrumbs component !",
9584
9584
  "creatives.components.CapTagList.Cancel": "Cancel",
9585
9585
  "creatives.components.CapTagList.Ok": "Ok",