@capillarytech/creatives-library 9.0.47 → 9.0.48

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.
@@ -0,0 +1,100 @@
1
+ import {
2
+ DLT_VAR_TYPE_KEYS,
3
+ DLT_VAR_TYPE_META,
4
+ DLT_LEGACY_VAR_TOKEN,
5
+ DLT_LEGACY_VAR_REGEX,
6
+ ANY_DLT_HASH_TOKEN_PROBE_REGEX,
7
+ isAnyDltVarToken,
8
+ getDltVarTypeKey,
9
+ getDltVarTypeMeta,
10
+ getDltVarCharLimit,
11
+ } from '../dltVarTypes';
12
+
13
+ describe('dltVarTypes', () => {
14
+ it('exposes the six DLT types with short + long syntax + char limit', () => {
15
+ expect(DLT_VAR_TYPE_KEYS).toEqual([
16
+ 'numeric', 'alphanumeric', 'url', 'urlott', 'cbn', 'email',
17
+ ]);
18
+ DLT_VAR_TYPE_KEYS.forEach((key) => {
19
+ const meta = DLT_VAR_TYPE_META[key];
20
+ expect(meta.short).toMatch(/^\{#[^#]+#\}$/);
21
+ expect(meta.long).toMatch(/^\{#[^#]+#\}$/);
22
+ expect(typeof meta.charLimit).toBe('number');
23
+ });
24
+ });
25
+
26
+ describe('isAnyDltVarToken', () => {
27
+ it('accepts short and long DLT token forms', () => {
28
+ expect(isAnyDltVarToken('{#num#}')).toBe(true);
29
+ expect(isAnyDltVarToken('{#numeric#}')).toBe(true);
30
+ expect(isAnyDltVarToken('{#var#}')).toBe(true);
31
+ expect(isAnyDltVarToken('{# alp #}')).toBe(true);
32
+ });
33
+
34
+ it('rejects non-DLT strings and non-strings', () => {
35
+ expect(isAnyDltVarToken('{{mustache}}')).toBe(false);
36
+ expect(isAnyDltVarToken('plain text')).toBe(false);
37
+ expect(isAnyDltVarToken('')).toBe(false);
38
+ expect(isAnyDltVarToken(null)).toBe(false);
39
+ expect(isAnyDltVarToken(undefined)).toBe(false);
40
+ expect(isAnyDltVarToken(42)).toBe(false);
41
+ });
42
+ });
43
+
44
+ describe('getDltVarTypeKey', () => {
45
+ it('resolves short and long spellings + legacy {#var#} to a type key', () => {
46
+ expect(getDltVarTypeKey('{#num#}')).toBe('numeric');
47
+ expect(getDltVarTypeKey('{#numeric#}')).toBe('numeric');
48
+ expect(getDltVarTypeKey('{#ALP#}')).toBe('alphanumeric');
49
+ expect(getDltVarTypeKey('{# url #}')).toBe('url');
50
+ expect(getDltVarTypeKey(DLT_LEGACY_VAR_TOKEN)).toBe('alphanumeric');
51
+ });
52
+
53
+ it('returns null for unknown / non-string tokens', () => {
54
+ expect(getDltVarTypeKey('{#foobar#}')).toBe(null);
55
+ expect(getDltVarTypeKey('')).toBe(null);
56
+ expect(getDltVarTypeKey(null)).toBe(null);
57
+ expect(getDltVarTypeKey(undefined)).toBe(null);
58
+ });
59
+ });
60
+
61
+ describe('getDltVarTypeMeta', () => {
62
+ it('returns the full meta object for a valid token', () => {
63
+ const meta = getDltVarTypeMeta('{#url#}');
64
+ expect(meta.charLimit).toBe(120);
65
+ expect(meta.placeholderKey).toBe('dltVarPlaceholderUrl');
66
+ });
67
+
68
+ it('returns null for an unknown token', () => {
69
+ expect(getDltVarTypeMeta('{#nope#}')).toBe(null);
70
+ });
71
+ });
72
+
73
+ describe('getDltVarCharLimit', () => {
74
+ it('returns the type-specific char limit for known tokens', () => {
75
+ expect(getDltVarCharLimit('{#num#}')).toBe(40);
76
+ expect(getDltVarCharLimit('{#url#}')).toBe(120);
77
+ expect(getDltVarCharLimit('{#cbn#}')).toBe(14);
78
+ });
79
+
80
+ it('falls back to the supplied default when the token is unknown', () => {
81
+ expect(getDltVarCharLimit('{#nope#}', 40)).toBe(40);
82
+ expect(getDltVarCharLimit('{#nope#}', 99)).toBe(99);
83
+ expect(getDltVarCharLimit(null)).toBe(40);
84
+ });
85
+ });
86
+
87
+ describe('exported regexes', () => {
88
+ it('DLT_LEGACY_VAR_REGEX matches only the legacy generic token', () => {
89
+ expect(DLT_LEGACY_VAR_REGEX.test('body {#var#} tail')).toBe(true);
90
+ expect(DLT_LEGACY_VAR_REGEX.test('body {#VAR#} tail')).toBe(true);
91
+ expect(DLT_LEGACY_VAR_REGEX.test('body {#num#} tail')).toBe(false);
92
+ });
93
+
94
+ it('ANY_DLT_HASH_TOKEN_PROBE_REGEX detects any hash token safely across calls', () => {
95
+ expect(ANY_DLT_HASH_TOKEN_PROBE_REGEX.test('hi {#alp#} there')).toBe(true);
96
+ expect(ANY_DLT_HASH_TOKEN_PROBE_REGEX.test('hi {#url#} again')).toBe(true);
97
+ expect(ANY_DLT_HASH_TOKEN_PROBE_REGEX.test('plain')).toBe(false);
98
+ });
99
+ });
100
+ });
@@ -1480,6 +1480,29 @@
1480
1480
  cursor: not-allowed;
1481
1481
  }
1482
1482
 
1483
+ .sms-service-explicit-retired-banner.ant-alert {
1484
+ margin: 0 $CAP_SPACE_16 $CAP_SPACE_16 $CAP_SPACE_16;
1485
+ padding: $CAP_SPACE_08 $CAP_SPACE_12;
1486
+ align-items: center;
1487
+ }
1488
+
1489
+ .sms-template-category-tag {
1490
+ margin-bottom: $CAP_SPACE_12;
1491
+ padding: 0 $CAP_SPACE_12;
1492
+ border-radius: 62.5rem;
1493
+ }
1494
+
1495
+
1496
+ .sms-template-legacy-warning.ant-alert {
1497
+ margin-top: $CAP_SPACE_12;
1498
+
1499
+ .ant-alert-title,
1500
+ .ant-alert-title span {
1501
+ font-weight: 400;
1502
+ white-space: normal;
1503
+ }
1504
+ }
1505
+
1483
1506
  .template-action-dropdown {
1484
1507
  min-width: 14.286rem !important;
1485
1508
  .template-action-menu {
@@ -142,7 +142,10 @@ import {CREATIVE} from '../Facebook/constants';
142
142
  import videoPlay from '../../assets/videoPlay.svg';
143
143
  import whatsappImageEmptyPreview from '../../v2Components/TemplatePreview/assets/images/empty_image_preview.svg';
144
144
  import whatsappVideoEmptyPreview from '../../v2Components/TemplatePreview/assets/images/empty_video_preview.svg';
145
- import { CAP_SPACE_16, CAP_G08, CAP_G05, CAP_SPACE_08, CAP_SPACE_12 } from '@capillarytech/cap-ui-library/styled/variables';
145
+ import {
146
+ CAP_SPACE_16, CAP_G08, CAP_G05, CAP_SPACE_08, CAP_SPACE_12, CAP_YELLOW01,
147
+ } from '@capillarytech/cap-ui-library/styled/variables';
148
+ import { SMS_CATEGORY_TAG_STYLES, SMS_CATEGORY_LABELS } from '../../v2Components/SmsFallback/constants';
146
149
  import { GA } from '@capillarytech/cap-ui-utils';
147
150
  import { MAPP_SDK } from '../InApp/constants';
148
151
  import injectReducer from '../../utils/injectReducer';
@@ -158,6 +161,7 @@ import { v2MobilePushSagas } from '../MobilePushNew/sagas';
158
161
  import { AUTO_CAROUSEL, BIG_PICTURE, FILMSTRIP_CAROUSEL, MANUAL_CAROUSEL } from '../MobilePushNew/constants';
159
162
  import CapPageSpinner from '../../v2Components/CapPageSpinner';
160
163
  import webPushSagas from '../WebPush/sagas';
164
+ import { DLT_LEGACY_VAR_REGEX } from '../SmsTrai/Edit/dltVarTypes';
161
165
  const withMobilePushNewSaga = injectSaga({ key: 'mobilePushNew', saga: v2MobilePushSagas, mode: DAEMON });
162
166
  const withWebPushSaga = injectSaga({ key: 'webPush', saga: webPushSagas, mode: DAEMON });
163
167
 
@@ -454,9 +458,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
454
458
  }
455
459
 
456
460
  checkDLTfeatureEnable() {
457
- const {smsRegister, isFullMode} = this.props;
458
- const isTraiDlt = commonUtil.isTraiDLTEnable(isFullMode, smsRegister);
459
- return isTraiDlt;
461
+ const { smsRegister, isFullMode, localTemplatesConfig } = this.props;
462
+ if (commonUtil.isTraiDLTEnable(isFullMode, smsRegister)) return true;
463
+ // Local-templates picker (RCS SMS fallback, campaigns): apply DLT UI whenever the org has
464
+ // the feature, even if the parent context wasn't marked `smsRegister === 'DLT'`.
465
+ return !!localTemplatesConfig?.useLocalTemplates && commonUtil.hasTraiDltFeature();
460
466
  }
461
467
 
462
468
  componentDidMount() {
@@ -2045,11 +2051,21 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2045
2051
  const isCardArchiveEligible = isArchivalEnabled && this.isChannelArchiveEligible(currentChannel, cardWhatsappStatus, cardRcsStatus);
2046
2052
  const isArchivedMode = isArchivalEnabled && get(this.props, 'Templates.isArchivedMode', false);
2047
2053
  const isAnyArchiveInProgress = isArchivalEnabled && !!(get(this.props, 'Templates.archiveInProgress') || get(this.props, 'Templates.unarchiveInProgress') || get(this.props, 'Templates.bulkArchiveInProgress') || get(this.props, 'Templates.bulkUnarchiveInProgress'));
2054
+ const smsBaseForLegacyCheck = template?.versions?.base || {};
2055
+ const updatedSmsEditorForLegacy = smsBaseForLegacyCheck['updated-sms-editor'];
2056
+ const updatedSmsEditorForLegacyJoined = Array.isArray(updatedSmsEditorForLegacy)
2057
+ ? updatedSmsEditorForLegacy.join('')
2058
+ : updatedSmsEditorForLegacy;
2059
+ const smsBodyForLegacyCheck = `${updatedSmsEditorForLegacyJoined || ''}${smsBaseForLegacyCheck['sms-editor'] || ''}`;
2060
+ const isDltLegacyTemplate = currentChannel === SMS
2061
+ && isTraiDltFeature
2062
+ && !this.props.isFullMode
2063
+ && DLT_LEGACY_VAR_REGEX.test(smsBodyForLegacyCheck);
2048
2064
  const templateData = {
2049
2065
  key: `${currentChannel}-card-${template?.name}`,
2050
2066
  title: (
2051
2067
  <span className="template-card-title" title={template?.name}>
2052
- {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress })}
2068
+ {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress || isDltLegacyTemplate })}
2053
2069
  <CapLabel.CapLabelInline type="label1" title={template?.name} className="template-card-name">
2054
2070
  {template?.name}
2055
2071
  {currentChannel === INAPP && (
@@ -2109,20 +2125,39 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2109
2125
  })()
2110
2126
  ],
2111
2127
  hoverOption: isArchivedMode || !this.canPerform(PERMISSIONS.CREATIVE_EDIT) ? null : (
2112
- <CapButton
2113
- className={
2114
- this.props.isFullMode
2115
- ? `edit-${channelLowerCase}`
2116
- : `select-${channelLowerCase}`
2117
- }
2118
- onClick={e =>
2119
- handlers.handleEditClick(e, template, undefined, undefined, {
2120
- account: this.state.selectedAccount
2121
- })
2122
- }
2123
- >
2124
- {hoverButtonText}
2125
- </CapButton>
2128
+ isDltLegacyTemplate ? (
2129
+ <CapTooltip
2130
+ title={this.props.intl.formatMessage(messages.smsLegacyBlockedTooltip)}
2131
+ >
2132
+ <CapLabel.CapLabelInline>
2133
+ <CapButton
2134
+ className={
2135
+ this.props.isFullMode
2136
+ ? `edit-${channelLowerCase}`
2137
+ : `select-${channelLowerCase}`
2138
+ }
2139
+ disabled
2140
+ >
2141
+ {hoverButtonText}
2142
+ </CapButton>
2143
+ </CapLabel.CapLabelInline>
2144
+ </CapTooltip>
2145
+ ) : (
2146
+ <CapButton
2147
+ className={
2148
+ this.props.isFullMode
2149
+ ? `edit-${channelLowerCase}`
2150
+ : `select-${channelLowerCase}`
2151
+ }
2152
+ onClick={e =>
2153
+ handlers.handleEditClick(e, template, undefined, undefined, {
2154
+ account: this.state.selectedAccount
2155
+ })
2156
+ }
2157
+ >
2158
+ {hoverButtonText}
2159
+ </CapButton>
2160
+ )
2126
2161
  )
2127
2162
  };
2128
2163
  const {
@@ -2222,22 +2257,49 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2222
2257
  );
2223
2258
  }
2224
2259
  switch (currentChannel) {
2225
- case SMS:
2260
+ case SMS: {
2261
+ const smsBaseForCard = template.versions.base || {};
2262
+ const updatedSmsEditor = smsBaseForCard['updated-sms-editor'];
2263
+ const normalizedUpdatedSmsEditor = Array.isArray(updatedSmsEditor)
2264
+ ? updatedSmsEditor.join('')
2265
+ : updatedSmsEditor;
2266
+ const smsBodyForCard = normalizedUpdatedSmsEditor || smsBaseForCard['sms-editor'] || '';
2267
+ const hasLegacyVarToken = DLT_LEGACY_VAR_REGEX.test(smsBodyForCard);
2268
+ const rawCategoryLabel = smsBaseForCard.type || '';
2269
+ const categoryLabel = rawCategoryLabel.toLowerCase() === SMS_CATEGORY_LABELS.SERVICE_EXPLICIT
2270
+ ? this.props.intl.formatMessage(messages.promotional)
2271
+ : rawCategoryLabel;
2272
+ const categoryTagStyle = SMS_CATEGORY_TAG_STYLES[categoryLabel.toLowerCase()] || {};
2226
2273
  templateData.content = isTraiDltFeature ? (
2227
2274
  <>
2228
- <CapLabel type="label3" style={{ marginBottom: CAP_SPACE_16 }}>
2229
- {template.versions.base.type}
2230
- </CapLabel>
2231
- <CapLabel type="label1">
2232
- {template.versions.base['updated-sms-editor'] === ''
2233
- ? template.versions.base['sms-editor']
2234
- : template.versions.base['updated-sms-editor']?.join('')}
2235
- </CapLabel>
2275
+ {hasLegacyVarToken ? (
2276
+ <CapColoredTag
2277
+ tagColor="rgba(254, 197, 46, 0.15)"
2278
+ tagTextColor={CAP_YELLOW01}
2279
+ tagHeight="1.25rem"
2280
+ tagFontSize="0.75rem"
2281
+ className="sms-template-category-tag"
2282
+ >
2283
+ {this.props.intl.formatMessage(messages.smsLegacyFormatBadge)}
2284
+ </CapColoredTag>
2285
+ ) : categoryLabel ? (
2286
+ <CapColoredTag
2287
+ tagColor={categoryTagStyle.tagColor}
2288
+ tagTextColor={categoryTagStyle.tagTextColor}
2289
+ tagHeight="1.25rem"
2290
+ tagFontSize="0.75rem"
2291
+ className="sms-template-category-tag"
2292
+ >
2293
+ {categoryLabel}
2294
+ </CapColoredTag>
2295
+ ) : null}
2296
+ <CapLabel type="label1">{smsBodyForCard}</CapLabel>
2236
2297
  </>
2237
2298
  ) : (
2238
2299
  template.versions.base['sms-editor']
2239
2300
  );
2240
2301
  break;
2302
+ }
2241
2303
  case EMAIL: {
2242
2304
  const url = template.versions.base.preview_http_url;
2243
2305
  if (url) {
@@ -2761,6 +2823,12 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2761
2823
  )}
2762
2824
  </CapRow>
2763
2825
  {[WHATSAPP, ZALO, INAPP,RCS].includes(currentChannel) && this.selectedFilters()}
2826
+ {currentChannel === SMS && this.checkDLTfeatureEnable() && (
2827
+ <CapInfoNote
2828
+ className="sms-service-explicit-retired-banner"
2829
+ message={this.props.intl.formatMessage(messages.smsServiceExplicitRetiredBanner)}
2830
+ />
2831
+ )}
2764
2832
  {<div>
2765
2833
  {!isEmpty(filteredTemplates) || !isEmpty(this.state.searchText) || !isEmpty(this.props.Templates.templateError) ? (
2766
2834
  <div className={!isEmpty(this.state.searchText) && isEmpty(cardDataList) ? '' : this.isFullMode() ? "v2-pagination-container" : "v2-pagination-container-half"}>
@@ -3535,6 +3603,22 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3535
3603
  CapNotification.error({ message: this.props.intl.formatMessage(messages.cannotEditArchivedTemplate) });
3536
3604
  return;
3537
3605
  }
3606
+ if (!this.props.isFullMode
3607
+ && this.checkDLTfeatureEnable()
3608
+ && (this.state.channel || '').toLowerCase() === SMS_LOWERCASE) {
3609
+ const smsBase = template?.versions?.base || {};
3610
+ const updatedSms = smsBase['updated-sms-editor'];
3611
+ const updatedSmsJoined = Array.isArray(updatedSms) ? updatedSms.join('') : updatedSms;
3612
+ // Check BOTH candidate fields — some legacy templates keep the body only in `sms-editor`
3613
+ // while others have it in `updated-sms-editor`. Concatenating catches either.
3614
+ const smsBody = `${updatedSmsJoined || ''}${smsBase['sms-editor'] || ''}`;
3615
+ if (DLT_LEGACY_VAR_REGEX.test(smsBody)) {
3616
+ CapNotification.error({
3617
+ message: this.props.intl.formatMessage(messages.smsLegacyBlockedTooltip),
3618
+ });
3619
+ return;
3620
+ }
3621
+ }
3538
3622
  if (modeType && modeType !== undefined) {
3539
3623
  this.setState({modeType});
3540
3624
  }
@@ -4788,7 +4872,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4788
4872
  <CapRadio.CapRadioGroup className="line-filters" defaultValue={smsFilter} onChange={this.setSMSFilter}>
4789
4873
  {
4790
4874
  (() => {
4791
- const { ALL, SERVICE_IMPLICIT, SERVICE_EXPLICIT, PROMOTIONAL } = SMS_FILTERS;
4875
+ const { ALL, SERVICE_IMPLICIT, PROMOTIONAL } = SMS_FILTERS;
4792
4876
  return (
4793
4877
  <>
4794
4878
  <CapRadio.Button value={ALL}><CapLabel type="label2">
@@ -4797,9 +4881,6 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4797
4881
  <CapRadio.Button value={PROMOTIONAL}><CapLabel type="label2">
4798
4882
  <FormattedMessage {...messages.promotional} />
4799
4883
  </CapLabel></CapRadio.Button>
4800
- <CapRadio.Button value={SERVICE_EXPLICIT}><CapLabel type="label2">
4801
- <FormattedMessage {...messages.serviceExplicit} />
4802
- </CapLabel></CapRadio.Button>
4803
4884
  <CapRadio.Button value={SERVICE_IMPLICIT}><CapLabel type="label2">
4804
4885
  <FormattedMessage {...messages.serviceImplicit} />
4805
4886
  </CapLabel></CapRadio.Button>
@@ -538,6 +538,18 @@ export default defineMessages({
538
538
  id: `${scope}.serviceImplicit`,
539
539
  defaultMessage: 'Service implicit',
540
540
  },
541
+ "smsServiceExplicitRetiredBanner": {
542
+ id: `${scope}.smsServiceExplicitRetiredBanner`,
543
+ defaultMessage: 'Service Explicit is retired. Existing Service Explicit templates have been moved under the Promotional category.',
544
+ },
545
+ "smsLegacyFormatBadge": {
546
+ id: `${scope}.smsLegacyFormatBadge`,
547
+ defaultMessage: 'Legacy format',
548
+ },
549
+ "smsLegacyBlockedTooltip": {
550
+ id: `${scope}.smsLegacyBlockedTooltip`,
551
+ defaultMessage: "This template can't be used until re-registered with typed variables on the DLT portal.",
552
+ },
541
553
  "uploadTemplate": {
542
554
  id: `${scope}.uploadTemplate`,
543
555
  defaultMessage: 'Upload template',