@capillarytech/creatives-library 9.0.45 → 9.0.46

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
+ });
@@ -1658,6 +1658,29 @@
1658
1658
  cursor: not-allowed;
1659
1659
  }
1660
1660
 
1661
+ .sms-service-explicit-retired-banner.ant-alert {
1662
+ margin: 0 $CAP_SPACE_16 $CAP_SPACE_16 $CAP_SPACE_16;
1663
+ padding: $CAP_SPACE_08 $CAP_SPACE_12;
1664
+ align-items: center;
1665
+ }
1666
+
1667
+ .sms-template-category-tag {
1668
+ margin-bottom: $CAP_SPACE_12;
1669
+ padding: 0 $CAP_SPACE_12;
1670
+ border-radius: 62.5rem;
1671
+ }
1672
+
1673
+
1674
+ .sms-template-legacy-warning.ant-alert {
1675
+ margin-top: $CAP_SPACE_12;
1676
+
1677
+ .ant-alert-title,
1678
+ .ant-alert-title span {
1679
+ font-weight: 400;
1680
+ white-space: normal;
1681
+ }
1682
+ }
1683
+
1661
1684
  .template-action-dropdown {
1662
1685
  min-width: 14.286rem !important;
1663
1686
  .template-action-menu {
@@ -143,7 +143,10 @@ import {CREATIVE} from '../Facebook/constants';
143
143
  import videoPlay from '../../assets/videoPlay.svg';
144
144
  import whatsappImageEmptyPreview from '../../v2Components/TemplatePreview/assets/images/empty_image_preview.svg';
145
145
  import whatsappVideoEmptyPreview from '../../v2Components/TemplatePreview/assets/images/empty_video_preview.svg';
146
- import { CAP_SPACE_16, CAP_G08, CAP_G05, CAP_SPACE_08, CAP_SPACE_12 } from '@capillarytech/cap-ui-library/styled/variables';
146
+ import {
147
+ CAP_SPACE_16, CAP_G08, CAP_G05, CAP_SPACE_08, CAP_SPACE_12, CAP_YELLOW01,
148
+ } from '@capillarytech/cap-ui-library/styled/variables';
149
+ import { SMS_CATEGORY_TAG_STYLES, SMS_CATEGORY_LABELS } from '../../v2Components/SmsFallback/constants';
147
150
  import { GA } from '@capillarytech/cap-ui-utils';
148
151
  import { MAPP_SDK } from '../InApp/constants';
149
152
  import injectReducer from '../../utils/injectReducer';
@@ -159,6 +162,7 @@ import { v2MobilePushSagas } from '../MobilePushNew/sagas';
159
162
  import { AUTO_CAROUSEL, BIG_PICTURE, FILMSTRIP_CAROUSEL, MANUAL_CAROUSEL } from '../MobilePushNew/constants';
160
163
  import CapPageSpinner from '../../v2Components/CapPageSpinner';
161
164
  import webPushSagas from '../WebPush/sagas';
165
+ import { DLT_LEGACY_VAR_REGEX } from '../SmsTrai/Edit/dltVarTypes';
162
166
  const withMobilePushNewSaga = injectSaga({ key: 'mobilePushNew', saga: v2MobilePushSagas, mode: DAEMON });
163
167
  const withWebPushSaga = injectSaga({ key: 'webPush', saga: webPushSagas, mode: DAEMON });
164
168
 
@@ -455,9 +459,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
455
459
  }
456
460
 
457
461
  checkDLTfeatureEnable() {
458
- const {smsRegister, isFullMode} = this.props;
459
- const isTraiDlt = commonUtil.isTraiDLTEnable(isFullMode, smsRegister);
460
- return isTraiDlt;
462
+ const { smsRegister, isFullMode, localTemplatesConfig } = this.props;
463
+ if (commonUtil.isTraiDLTEnable(isFullMode, smsRegister)) return true;
464
+ // Local-templates picker (RCS SMS fallback, campaigns): apply DLT UI whenever the org has
465
+ // the feature, even if the parent context wasn't marked `smsRegister === 'DLT'`.
466
+ return !!localTemplatesConfig?.useLocalTemplates && commonUtil.hasTraiDltFeature();
461
467
  }
462
468
 
463
469
  componentDidMount() {
@@ -2076,11 +2082,20 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2076
2082
  const isCardArchiveEligible = isArchivalEnabled && this.isChannelArchiveEligible(currentChannel, cardWhatsappStatus, cardRcsStatus);
2077
2083
  const isArchivedMode = isArchivalEnabled && get(this.props, 'Templates.isArchivedMode', false);
2078
2084
  const isAnyArchiveInProgress = isArchivalEnabled && !!(get(this.props, 'Templates.archiveInProgress') || get(this.props, 'Templates.unarchiveInProgress') || get(this.props, 'Templates.bulkArchiveInProgress') || get(this.props, 'Templates.bulkUnarchiveInProgress'));
2085
+ const smsBaseForLegacyCheck = template?.versions?.base || {};
2086
+ const updatedSmsEditorForLegacy = smsBaseForLegacyCheck['updated-sms-editor'];
2087
+ const smsBodyForLegacyCheck = Array.isArray(updatedSmsEditorForLegacy)
2088
+ ? updatedSmsEditorForLegacy.join('')
2089
+ : updatedSmsEditorForLegacy ?? smsBaseForLegacyCheck['sms-editor'] ?? '';
2090
+ const isDltLegacyTemplate = currentChannel === SMS
2091
+ && isTraiDltFeature
2092
+ && !this.props.isFullMode
2093
+ && DLT_LEGACY_VAR_REGEX.test(smsBodyForLegacyCheck);
2079
2094
  const templateData = {
2080
2095
  key: `${currentChannel}-card-${template?.name}`,
2081
2096
  title: (
2082
2097
  <span className="template-card-title" title={template?.name}>
2083
- {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress })}
2098
+ {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress || isDltLegacyTemplate })}
2084
2099
  <CapLabel.CapLabelInline type="label1" title={template?.name} className="template-card-name">
2085
2100
  {template?.name}
2086
2101
  {currentChannel === INAPP && (
@@ -2140,20 +2155,39 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2140
2155
  })()
2141
2156
  ],
2142
2157
  hoverOption: isArchivedMode || !this.canPerform(PERMISSIONS.CREATIVE_EDIT) ? null : (
2143
- <CapButton
2144
- className={
2145
- this.props.isFullMode
2146
- ? `edit-${channelLowerCase}`
2147
- : `select-${channelLowerCase}`
2148
- }
2149
- onClick={e =>
2150
- handlers.handleEditClick(e, template, undefined, undefined, {
2151
- account: this.state.selectedAccount
2152
- })
2153
- }
2154
- >
2155
- {hoverButtonText}
2156
- </CapButton>
2158
+ isDltLegacyTemplate ? (
2159
+ <CapTooltip
2160
+ title={this.props.intl.formatMessage(messages.smsLegacyBlockedTooltip)}
2161
+ >
2162
+ <CapLabel.CapLabelInline>
2163
+ <CapButton
2164
+ className={
2165
+ this.props.isFullMode
2166
+ ? `edit-${channelLowerCase}`
2167
+ : `select-${channelLowerCase}`
2168
+ }
2169
+ disabled
2170
+ >
2171
+ {hoverButtonText}
2172
+ </CapButton>
2173
+ </CapLabel.CapLabelInline>
2174
+ </CapTooltip>
2175
+ ) : (
2176
+ <CapButton
2177
+ className={
2178
+ this.props.isFullMode
2179
+ ? `edit-${channelLowerCase}`
2180
+ : `select-${channelLowerCase}`
2181
+ }
2182
+ onClick={e =>
2183
+ handlers.handleEditClick(e, template, undefined, undefined, {
2184
+ account: this.state.selectedAccount
2185
+ })
2186
+ }
2187
+ >
2188
+ {hoverButtonText}
2189
+ </CapButton>
2190
+ )
2157
2191
  )
2158
2192
  };
2159
2193
  const {
@@ -2253,22 +2287,49 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2253
2287
  );
2254
2288
  }
2255
2289
  switch (currentChannel) {
2256
- case SMS:
2290
+ case SMS: {
2291
+ const smsBaseForCard = template.versions.base || {};
2292
+ const updatedSmsEditor = smsBaseForCard['updated-sms-editor'];
2293
+ const normalizedUpdatedSmsEditor = Array.isArray(updatedSmsEditor)
2294
+ ? updatedSmsEditor.join('')
2295
+ : updatedSmsEditor;
2296
+ const smsBodyForCard = normalizedUpdatedSmsEditor || smsBaseForCard['sms-editor'] || '';
2297
+ const hasLegacyVarToken = DLT_LEGACY_VAR_REGEX.test(smsBodyForCard);
2298
+ const rawCategoryLabel = smsBaseForCard.type || '';
2299
+ const categoryLabel = rawCategoryLabel.toLowerCase() === SMS_CATEGORY_LABELS.SERVICE_EXPLICIT
2300
+ ? this.props.intl.formatMessage(messages.promotional)
2301
+ : rawCategoryLabel;
2302
+ const categoryTagStyle = SMS_CATEGORY_TAG_STYLES[categoryLabel.toLowerCase()] || {};
2257
2303
  templateData.content = isTraiDltFeature ? (
2258
2304
  <>
2259
- <CapLabel type="label3" style={{ marginBottom: CAP_SPACE_16 }}>
2260
- {template.versions.base.type}
2261
- </CapLabel>
2262
- <CapLabel type="label1">
2263
- {template.versions.base['updated-sms-editor'] === ''
2264
- ? template.versions.base['sms-editor']
2265
- : template.versions.base['updated-sms-editor']?.join('')}
2266
- </CapLabel>
2305
+ {hasLegacyVarToken ? (
2306
+ <CapColoredTag
2307
+ tagColor="rgba(254, 197, 46, 0.15)"
2308
+ tagTextColor={CAP_YELLOW01}
2309
+ tagHeight="1.25rem"
2310
+ tagFontSize="0.75rem"
2311
+ className="sms-template-category-tag"
2312
+ >
2313
+ {this.props.intl.formatMessage(messages.smsLegacyFormatBadge)}
2314
+ </CapColoredTag>
2315
+ ) : categoryLabel ? (
2316
+ <CapColoredTag
2317
+ tagColor={categoryTagStyle.tagColor}
2318
+ tagTextColor={categoryTagStyle.tagTextColor}
2319
+ tagHeight="1.25rem"
2320
+ tagFontSize="0.75rem"
2321
+ className="sms-template-category-tag"
2322
+ >
2323
+ {categoryLabel}
2324
+ </CapColoredTag>
2325
+ ) : null}
2326
+ <CapLabel type="label1">{smsBodyForCard}</CapLabel>
2267
2327
  </>
2268
2328
  ) : (
2269
2329
  template.versions.base['sms-editor']
2270
2330
  );
2271
2331
  break;
2332
+ }
2272
2333
  case EMAIL: {
2273
2334
  const url = template.versions.base.preview_http_url;
2274
2335
  if (url) {
@@ -2841,6 +2902,12 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2841
2902
  )}
2842
2903
  </CapRow>
2843
2904
  {[WHATSAPP, ZALO, INAPP,RCS].includes(currentChannel) && this.selectedFilters()}
2905
+ {currentChannel === SMS && this.checkDLTfeatureEnable() && (
2906
+ <CapInfoNote
2907
+ className="sms-service-explicit-retired-banner"
2908
+ message={this.props.intl.formatMessage(messages.smsServiceExplicitRetiredBanner)}
2909
+ />
2910
+ )}
2844
2911
  {<div>
2845
2912
  {!isEmpty(filteredTemplates) || !isEmpty(this.state.searchText) || !isEmpty(this.props.Templates.templateError) ? (
2846
2913
  <div className={!isEmpty(this.state.searchText) && isEmpty(cardDataList) ? '' : this.isFullMode() ? "v2-pagination-container" : "v2-pagination-container-half"}>
@@ -4868,7 +4935,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4868
4935
  <CapRadio.CapRadioGroup className="line-filters" defaultValue={smsFilter} onChange={this.setSMSFilter}>
4869
4936
  {
4870
4937
  (() => {
4871
- const { ALL, SERVICE_IMPLICIT, SERVICE_EXPLICIT, PROMOTIONAL } = SMS_FILTERS;
4938
+ const { ALL, SERVICE_IMPLICIT, PROMOTIONAL } = SMS_FILTERS;
4872
4939
  return (
4873
4940
  <>
4874
4941
  <CapRadio.Button value={ALL}><CapLabel type="label2">
@@ -4877,9 +4944,6 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4877
4944
  <CapRadio.Button value={PROMOTIONAL}><CapLabel type="label2">
4878
4945
  <FormattedMessage {...messages.promotional} />
4879
4946
  </CapLabel></CapRadio.Button>
4880
- <CapRadio.Button value={SERVICE_EXPLICIT}><CapLabel type="label2">
4881
- <FormattedMessage {...messages.serviceExplicit} />
4882
- </CapLabel></CapRadio.Button>
4883
4947
  <CapRadio.Button value={SERVICE_IMPLICIT}><CapLabel type="label2">
4884
4948
  <FormattedMessage {...messages.serviceImplicit} />
4885
4949
  </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',
@@ -3189,6 +3189,7 @@ const isAuthenticationTemplate = isEqual(templateCategory, WHATSAPP_CATEGORIES.a
3189
3189
  }),
3190
3190
  // Payload fields (for test message API)
3191
3191
  _id: params?.id || null,
3192
+ templateId,
3192
3193
  templateName,
3193
3194
  templateEditor: templateEditorValue,
3194
3195
  category: templateCategory === formatMessage(messages.select) ? '' : templateCategory,
@@ -3236,6 +3237,7 @@ const isAuthenticationTemplate = isEqual(templateCategory, WHATSAPP_CATEGORIES.a
3236
3237
  formatMessage,
3237
3238
  params,
3238
3239
  templateName,
3240
+ templateId,
3239
3241
  templateCategory,
3240
3242
  templateLanguage,
3241
3243
  headerVarMappedData,