@capillarytech/creatives-library 8.0.286 → 8.0.287
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.
- package/package.json +1 -1
- package/utils/commonUtils.js +3 -0
- package/v2Components/CapTagList/index.js +6 -2
- package/v2Components/CapTagListWithInput/index.js +4 -0
- package/v2Components/FormBuilder/index.js +26 -3
- package/v2Components/FormBuilder/messages.js +4 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +20 -0
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +39 -3
- package/v2Containers/CreativesContainer/constants.js +6 -0
- package/v2Containers/CreativesContainer/index.js +32 -1
- package/v2Containers/CreativesContainer/messages.js +12 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +339 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +18 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +37 -0
- package/v2Containers/MobilePush/Create/index.js +45 -0
- package/v2Containers/MobilePush/Create/messages.js +4 -0
- package/v2Containers/MobilePush/Edit/index.js +45 -0
- package/v2Containers/MobilePush/Edit/messages.js +4 -0
- package/v2Containers/MobilePushNew/components/PlatformContentFields.js +36 -12
- package/v2Containers/MobilePushNew/components/tests/PlatformContentFields.test.js +68 -27
- package/v2Containers/MobilePushNew/index.js +32 -3
- package/v2Containers/MobilePushNew/messages.js +8 -0
- package/v2Containers/MobilepushWrapper/index.js +7 -1
- package/v2Containers/SmsTrai/Create/index.scss +1 -1
- package/v2Containers/TagList/index.js +17 -1
- package/v2Containers/TagList/messages.js +4 -0
- package/v2Containers/TemplatesV2/index.js +43 -23
- package/v2Containers/Viber/index.scss +1 -1
- package/v2Containers/WebPush/Create/index.js +25 -6
- package/v2Containers/WebPush/Create/messages.js +8 -1
- package/v2Containers/WebPush/Create/utils/validation.js +16 -9
- package/v2Containers/WebPush/Create/utils/validation.test.js +28 -0
|
@@ -161,6 +161,15 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
161
161
|
const newFormData = cloneDeep(formData);
|
|
162
162
|
const {templateCta} = this.state;
|
|
163
163
|
const { defaultData = {}, isFullMode, showTemplateName} = this.props;
|
|
164
|
+
|
|
165
|
+
// Check for personalization tokens if restriction is enabled and notify parent
|
|
166
|
+
if (this.props.restrictPersonalization) {
|
|
167
|
+
const hasTokens = this.checkForPersonalizationTokens(newFormData);
|
|
168
|
+
if (this.props.onPersonalizationTokensChange) {
|
|
169
|
+
this.props.onPersonalizationTokensChange(hasTokens);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
164
173
|
if (!isEmpty(templateCta)) {
|
|
165
174
|
newFormData[this.state.currentTab - 1][`secondary-cta-${this.state.currentTab - 1}-label`] = get(templateCta, 'name');
|
|
166
175
|
newFormData[this.state.currentTab - 1][`secondary-cta-${this.state.currentTab - 1}-action`] = get(templateCta, 'ctaTemplateDetails[0].buttonText');
|
|
@@ -1502,8 +1511,40 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
1502
1511
|
this.injectEvents(schema);
|
|
1503
1512
|
};
|
|
1504
1513
|
|
|
1514
|
+
checkForPersonalizationTokens = (formData) => {
|
|
1515
|
+
// Check for {{ }} (liquid tags) and [ ] (event context tags) in mobile push content
|
|
1516
|
+
const tokenRegex = /\{\{[\s\S]*?\}\}|\[[\s\S]*?\]/;
|
|
1517
|
+
|
|
1518
|
+
// Check all tabs/versions for personalization tokens
|
|
1519
|
+
if (formData && typeof formData === 'object') {
|
|
1520
|
+
for (const key in formData) {
|
|
1521
|
+
const tabData = formData[key];
|
|
1522
|
+
if (tabData && typeof tabData === 'object') {
|
|
1523
|
+
for (const fieldKey in tabData) {
|
|
1524
|
+
const fieldValue = tabData[fieldKey];
|
|
1525
|
+
if (typeof fieldValue === 'string' && tokenRegex.test(fieldValue)) {
|
|
1526
|
+
return true;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
return false;
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1505
1535
|
saveFormData = (formData) => {
|
|
1506
1536
|
//this function gets called from form bulder only when the form data is valid
|
|
1537
|
+
|
|
1538
|
+
// Check for personalization tokens if restriction is enabled
|
|
1539
|
+
if (this.props.restrictPersonalization) {
|
|
1540
|
+
const hasTokens = this.checkForPersonalizationTokens(formData);
|
|
1541
|
+
if (hasTokens) {
|
|
1542
|
+
const message = this.props.intl.formatMessage(messages.personalizationTokensErrorMessage);
|
|
1543
|
+
CapNotification.error({message, key: 'personalizationTokensError'});
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1507
1548
|
const obj = this.getTransformedData(formData);
|
|
1508
1549
|
const content = getContent(obj);
|
|
1509
1550
|
const {
|
|
@@ -1907,6 +1948,7 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
1907
1948
|
isFullMode={this.props.isFullMode}
|
|
1908
1949
|
eventContextTags={this.props?.eventContextTags}
|
|
1909
1950
|
messageDetails={this.props?.messageDetails}
|
|
1951
|
+
restrictPersonalization={this.props.restrictPersonalization}
|
|
1910
1952
|
/>
|
|
1911
1953
|
</CapColumn>
|
|
1912
1954
|
{this.props.iosCtasData && this.state.showIosCtaTable &&
|
|
@@ -2011,6 +2053,9 @@ Create.propTypes = {
|
|
|
2011
2053
|
showTestAndPreviewSlidebox: PropTypes.bool,
|
|
2012
2054
|
handleTestAndPreview: PropTypes.func,
|
|
2013
2055
|
handleCloseTestAndPreview: PropTypes.func,
|
|
2056
|
+
restrictPersonalization: PropTypes.bool,
|
|
2057
|
+
isAnonymousType: PropTypes.bool,
|
|
2058
|
+
onPersonalizationTokensChange: PropTypes.func,
|
|
2014
2059
|
};
|
|
2015
2060
|
|
|
2016
2061
|
const mapStateToProps = createStructuredSelector({
|
|
@@ -342,4 +342,8 @@ export default defineMessages({
|
|
|
342
342
|
id: 'creatives.containersV2.MobilePush.Create.thisSectionDisabledHoverText',
|
|
343
343
|
defaultMessage: 'This section is being revamped. Till then it will remain disabled.',
|
|
344
344
|
},
|
|
345
|
+
"personalizationTokensErrorMessage": {
|
|
346
|
+
id: 'creatives.containersV2.MobilePush.Create.personalizationTokensErrorMessage',
|
|
347
|
+
defaultMessage: 'Personalization tags are not supported for anonymous customers. Please remove the tags.',
|
|
348
|
+
},
|
|
345
349
|
});
|
|
@@ -246,6 +246,15 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
246
246
|
onFormDataChange = (formData, tabCount, currentTab, inputField) => {
|
|
247
247
|
const newFormData = _.cloneDeep(formData);
|
|
248
248
|
const {templateCta} = this.state;
|
|
249
|
+
|
|
250
|
+
// Check for personalization tokens if restriction is enabled and notify parent
|
|
251
|
+
if (this.props.restrictPersonalization) {
|
|
252
|
+
const hasTokens = this.checkForPersonalizationTokens(newFormData);
|
|
253
|
+
if (this.props.onPersonalizationTokensChange) {
|
|
254
|
+
this.props.onPersonalizationTokensChange(hasTokens);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
249
258
|
if (!_.isEmpty(templateCta) && this.state.currentTab === 2) {
|
|
250
259
|
newFormData[this.state.currentTab - 1][`secondary-cta-${this.state.currentTab - 1}-label`] = get(templateCta, 'name');
|
|
251
260
|
newFormData[this.state.currentTab - 1][`secondary-cta-${this.state.currentTab - 1}-action`] = get(templateCta, 'ctaTemplateDetails[0].buttonText');
|
|
@@ -1687,10 +1696,42 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1687
1696
|
};
|
|
1688
1697
|
|
|
1689
1698
|
saveFormData = (formData) => {
|
|
1699
|
+
// Check for personalization tokens if restriction is enabled
|
|
1700
|
+
if (this.props.restrictPersonalization) {
|
|
1701
|
+
const hasTokens = this.checkForPersonalizationTokens(formData);
|
|
1702
|
+
if (hasTokens) {
|
|
1703
|
+
const message = this.props.intl.formatMessage(messages.personalizationTokensErrorMessage);
|
|
1704
|
+
CapNotification.error({message, key: 'personalizationTokensError'});
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1690
1709
|
const obj = this.getTransformedData(formData);
|
|
1691
1710
|
|
|
1692
1711
|
this.props.actions.editTemplate(obj, this.onUpdateTemplateComplete);
|
|
1693
1712
|
};
|
|
1713
|
+
|
|
1714
|
+
checkForPersonalizationTokens = (formData) => {
|
|
1715
|
+
// Check for {{ }} (liquid tags) and [ ] (event context tags) in mobile push content
|
|
1716
|
+
const tokenRegex = /\{\{[\s\S]*?\}\}|\[[\s\S]*?\]/;
|
|
1717
|
+
|
|
1718
|
+
// Check all tabs/versions for personalization tokens
|
|
1719
|
+
if (formData && typeof formData === 'object') {
|
|
1720
|
+
for (const key in formData) {
|
|
1721
|
+
const tabData = formData[key];
|
|
1722
|
+
if (tabData && typeof tabData === 'object') {
|
|
1723
|
+
for (const fieldKey in tabData) {
|
|
1724
|
+
const fieldValue = tabData[fieldKey];
|
|
1725
|
+
if (typeof fieldValue === 'string' && tokenRegex.test(fieldValue)) {
|
|
1726
|
+
return true;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
return false;
|
|
1733
|
+
};
|
|
1734
|
+
|
|
1694
1735
|
handleFrameTasks = (e) => {
|
|
1695
1736
|
//
|
|
1696
1737
|
const type = e.data;
|
|
@@ -2186,6 +2227,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2186
2227
|
hideTestAndPreviewBtn={this.props.hideTestAndPreviewBtn}
|
|
2187
2228
|
isFullMode={this.props.isFullMode}
|
|
2188
2229
|
eventContextTags={this.props?.eventContextTags}
|
|
2230
|
+
restrictPersonalization={this.props.restrictPersonalization}
|
|
2189
2231
|
/>;
|
|
2190
2232
|
})()}
|
|
2191
2233
|
</CapColumn>
|
|
@@ -2295,6 +2337,9 @@ Edit.propTypes = {
|
|
|
2295
2337
|
showTestAndPreviewSlidebox: PropTypes.bool,
|
|
2296
2338
|
handleTestAndPreview: PropTypes.func,
|
|
2297
2339
|
handleCloseTestAndPreview: PropTypes.func,
|
|
2340
|
+
restrictPersonalization: PropTypes.bool,
|
|
2341
|
+
isAnonymousType: PropTypes.bool,
|
|
2342
|
+
onPersonalizationTokensChange: PropTypes.func,
|
|
2298
2343
|
};
|
|
2299
2344
|
|
|
2300
2345
|
const mapStateToProps = createStructuredSelector({
|
|
@@ -334,4 +334,8 @@ export default defineMessages({
|
|
|
334
334
|
id: 'creatives.containersV2.MobilePush.Edit.thisSectionDisabledHoverText',
|
|
335
335
|
defaultMessage: 'This section is being revamped. Till then it will remain disabled.',
|
|
336
336
|
},
|
|
337
|
+
"personalizationTokensErrorMessage": {
|
|
338
|
+
id: 'creatives.containersV2.MobilePush.Edit.personalizationTokensErrorMessage',
|
|
339
|
+
defaultMessage: 'Personalization tags are not supported for anonymous customers. Please remove the tags.',
|
|
340
|
+
},
|
|
337
341
|
});
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import messages from "../messages";
|
|
25
25
|
import MediaUploaders from "./MediaUploaders";
|
|
26
26
|
import CtaButtons from "./CtaButtons";
|
|
27
|
+
import { hasPersonalizationTags } from "../../../utils/commonUtils";
|
|
27
28
|
|
|
28
29
|
const PlatformContentFields = ({
|
|
29
30
|
deviceType,
|
|
@@ -40,8 +41,17 @@ const PlatformContentFields = ({
|
|
|
40
41
|
tags,
|
|
41
42
|
injectedTags,
|
|
42
43
|
selectedOfferDetails,
|
|
44
|
+
// new prop to disable personalization features for anonymous users
|
|
45
|
+
restrictPersonalization = false,
|
|
43
46
|
}) => {
|
|
44
47
|
const { title: titleError, message: messageError } = errors;
|
|
48
|
+
|
|
49
|
+
const titleErrorToShow = titleError || (restrictPersonalization && hasPersonalizationTags(content.title)
|
|
50
|
+
? formatMessage(messages.personalizationTagsErrorMessage)
|
|
51
|
+
: "");
|
|
52
|
+
const messageErrorToShow = messageError || (restrictPersonalization && hasPersonalizationTags(content.message)
|
|
53
|
+
? formatMessage(messages.personalizationTagsErrorMessage)
|
|
54
|
+
: "");
|
|
45
55
|
const {
|
|
46
56
|
handleTitleChange,
|
|
47
57
|
handleMessageChange,
|
|
@@ -139,14 +149,19 @@ const PlatformContentFields = ({
|
|
|
139
149
|
);
|
|
140
150
|
|
|
141
151
|
const getTagList = useCallback(
|
|
142
|
-
(index) =>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
152
|
+
(index) => {
|
|
153
|
+
const disableMsg = restrictPersonalization ? formatMessage(messages.personalizationNotSupportedAnonymous) : undefined;
|
|
154
|
+
return (
|
|
155
|
+
<TagList
|
|
156
|
+
{...tagListProps}
|
|
157
|
+
disabled={restrictPersonalization}
|
|
158
|
+
disableTooltipMsg={disableMsg}
|
|
159
|
+
onTagSelect={(value) => onTagSelect(value, index)}
|
|
160
|
+
onContextChange={handleOnTagsContextChange}
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
[tagListProps, onTagSelect, handleOnTagsContextChange, restrictPersonalization, formatMessage]
|
|
150
165
|
);
|
|
151
166
|
|
|
152
167
|
const onButtonTagSelect = useCallback(
|
|
@@ -178,9 +193,9 @@ const PlatformContentFields = ({
|
|
|
178
193
|
size="default"
|
|
179
194
|
isRequired
|
|
180
195
|
errorMessage={
|
|
181
|
-
|
|
196
|
+
titleErrorToShow && (
|
|
182
197
|
<CapError className="mobile-push-template-title-error">
|
|
183
|
-
{
|
|
198
|
+
{titleErrorToShow}
|
|
184
199
|
</CapError>
|
|
185
200
|
)
|
|
186
201
|
}
|
|
@@ -202,9 +217,9 @@ const PlatformContentFields = ({
|
|
|
202
217
|
size="default"
|
|
203
218
|
isRequired
|
|
204
219
|
errorMessage={
|
|
205
|
-
|
|
220
|
+
messageErrorToShow && (
|
|
206
221
|
<CapError className="mobile-push-template-message-error">
|
|
207
|
-
{
|
|
222
|
+
{messageErrorToShow}
|
|
208
223
|
</CapError>
|
|
209
224
|
)
|
|
210
225
|
}
|
|
@@ -314,6 +329,10 @@ const PlatformContentFields = ({
|
|
|
314
329
|
tags={tags}
|
|
315
330
|
injectedTags={injectedTags}
|
|
316
331
|
selectedOfferDetails={selectedOfferDetails}
|
|
332
|
+
disabled={restrictPersonalization}
|
|
333
|
+
disableTooltipMsg={
|
|
334
|
+
restrictPersonalization ? formatMessage(messages.personalizationNotSupportedAnonymous) : undefined
|
|
335
|
+
}
|
|
317
336
|
/>
|
|
318
337
|
</CapRow>
|
|
319
338
|
<CapInput
|
|
@@ -389,6 +408,11 @@ PlatformContentFields.propTypes = {
|
|
|
389
408
|
linkProps: PropTypes.object.isRequired,
|
|
390
409
|
sameContent: PropTypes.bool.isRequired,
|
|
391
410
|
formatMessage: PropTypes.func.isRequired,
|
|
411
|
+
restrictPersonalization: PropTypes.bool,
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
PlatformContentFields.defaultProps = {
|
|
415
|
+
restrictPersonalization: false,
|
|
392
416
|
};
|
|
393
417
|
|
|
394
418
|
export default PlatformContentFields;
|
|
@@ -11,25 +11,29 @@ import {
|
|
|
11
11
|
jest.mock("@capillarytech/cap-ui-library/CapRow", () => ({ children }) => <div>{children}</div>);
|
|
12
12
|
jest.mock("@capillarytech/cap-ui-library/CapColumn", () => ({ children }) => <div>{children}</div>);
|
|
13
13
|
jest.mock("@capillarytech/cap-ui-library/CapInput", () => {
|
|
14
|
-
const MockCapInput = ({
|
|
14
|
+
const MockCapInput = ({
|
|
15
|
+
value, onChange, errorMessage, error, ...props
|
|
16
|
+
}) => (
|
|
15
17
|
<div>
|
|
16
18
|
<input value={value || ""} onChange={onChange} error={error} {...props} />
|
|
17
19
|
{(errorMessage || error) && <div data-testid="error-message">{errorMessage || error}</div>}
|
|
18
20
|
</div>
|
|
19
21
|
);
|
|
20
|
-
|
|
21
|
-
MockCapInput.TextArea = ({
|
|
22
|
+
|
|
23
|
+
MockCapInput.TextArea = ({
|
|
24
|
+
value, onChange, errorMessage, error, ...props
|
|
25
|
+
}) => (
|
|
22
26
|
<div>
|
|
23
|
-
<textarea
|
|
24
|
-
value={value || ""}
|
|
25
|
-
onChange={onChange}
|
|
27
|
+
<textarea
|
|
28
|
+
value={value || ""}
|
|
29
|
+
onChange={onChange}
|
|
26
30
|
data-testid="message-textarea"
|
|
27
31
|
{...props}
|
|
28
32
|
/>
|
|
29
33
|
{(errorMessage || error) && <div data-testid="error-message">{errorMessage || error}</div>}
|
|
30
34
|
</div>
|
|
31
35
|
);
|
|
32
|
-
|
|
36
|
+
|
|
33
37
|
return MockCapInput;
|
|
34
38
|
});
|
|
35
39
|
jest.mock("@capillarytech/cap-ui-library/CapHeading", () => ({ children }) => <div>{children}</div>);
|
|
@@ -56,17 +60,19 @@ jest.mock("@capillarytech/cap-ui-library/CapLabel", () => ({ children }) => <lab
|
|
|
56
60
|
jest.mock("@capillarytech/cap-ui-library/CapInfoNote", () => ({ message }) => <div data-testid="info-note">{message}</div>);
|
|
57
61
|
|
|
58
62
|
// Mock child components
|
|
59
|
-
jest.mock("../../../TagList", () => ({
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
jest.mock("../../../TagList", () => ({
|
|
64
|
+
onTagSelect, onContextChange, label, disabled, disableTooltipMsg, ...props
|
|
65
|
+
}) => (
|
|
66
|
+
<div data-testid="tag-list" data-disabled={disabled || false} data-tooltip={disableTooltipMsg || ''}>
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
63
69
|
onClick={() => onTagSelect && onTagSelect('test_tag')}
|
|
64
70
|
data-testid="tag-select-button"
|
|
65
71
|
>
|
|
66
72
|
Select Tag
|
|
67
73
|
</button>
|
|
68
|
-
<button
|
|
69
|
-
type="button"
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
70
76
|
onClick={() => onContextChange && onContextChange('test_context')}
|
|
71
77
|
data-testid="tag-context-button"
|
|
72
78
|
>
|
|
@@ -99,6 +105,8 @@ jest.mock("../../messages", () => ({
|
|
|
99
105
|
deepLinkKeysPlaceholder: { id: "deepLinkKeysPlaceholder", defaultMessage: "Enter {key}" },
|
|
100
106
|
deepLinkKeysRequired: { id: "deepLinkKeysRequired", defaultMessage: "Deep link keys are required" },
|
|
101
107
|
addLabels: { id: "addLabels", defaultMessage: "Add labels" },
|
|
108
|
+
personalizationTagsErrorMessage: { id: "personalizationTagsErrorMessage", defaultMessage: "Personalization tags are not supported for anonymous customers, please remove the tags." },
|
|
109
|
+
personalizationNotSupportedAnonymous: { id: "personalizationNotSupportedAnonymous", defaultMessage: "Personalization tags are not supported for anonymous customers" },
|
|
102
110
|
}));
|
|
103
111
|
|
|
104
112
|
// Mock constants
|
|
@@ -171,6 +179,7 @@ describe("PlatformContentFields", () => {
|
|
|
171
179
|
tags: ["tag1", "tag2"],
|
|
172
180
|
injectedTags: [],
|
|
173
181
|
selectedOfferDetails: null,
|
|
182
|
+
restrictPersonalization: false,
|
|
174
183
|
};
|
|
175
184
|
|
|
176
185
|
const renderComponent = (props = {}) => render(
|
|
@@ -255,6 +264,38 @@ describe("PlatformContentFields", () => {
|
|
|
255
264
|
|
|
256
265
|
expect(getByTestId("cap-error")).toHaveTextContent("Message is required");
|
|
257
266
|
});
|
|
267
|
+
|
|
268
|
+
it("should show inline personalization error when restricted and tokens present", () => {
|
|
269
|
+
const personalizationErrorMsg = "Personalization tags are not supported for anonymous customers";
|
|
270
|
+
const formatMessageStub = jest.fn((msg) => msg?.defaultMessage ?? "");
|
|
271
|
+
const { container } = renderComponent({
|
|
272
|
+
restrictPersonalization: true,
|
|
273
|
+
content: { ...defaultProps.content, message: "Hello {{user}}" },
|
|
274
|
+
formatMessage: formatMessageStub,
|
|
275
|
+
});
|
|
276
|
+
expect(container.textContent).toContain(personalizationErrorMsg);
|
|
277
|
+
expect(formatMessageStub).toHaveBeenCalledWith(
|
|
278
|
+
expect.objectContaining({ defaultMessage: expect.stringContaining("Personalization tags") })
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("Personalization restriction", () => {
|
|
284
|
+
it("disables tag list and shows tooltip when restrictPersonalization true", () => {
|
|
285
|
+
const formatMessageForTooltip = jest.fn((msg) => msg?.defaultMessage ?? "");
|
|
286
|
+
const { getAllByTestId } = renderComponent({
|
|
287
|
+
restrictPersonalization: true,
|
|
288
|
+
formatMessage: formatMessageForTooltip,
|
|
289
|
+
});
|
|
290
|
+
const tagLists = getAllByTestId("tag-list");
|
|
291
|
+
expect(tagLists.length).toBeGreaterThan(0);
|
|
292
|
+
tagLists.forEach((tag) => {
|
|
293
|
+
expect(tag).toHaveAttribute('data-disabled', 'true');
|
|
294
|
+
});
|
|
295
|
+
expect(formatMessageForTooltip).toHaveBeenCalledWith(
|
|
296
|
+
expect.objectContaining({ defaultMessage: "Personalization tags are not supported for anonymous customers" })
|
|
297
|
+
);
|
|
298
|
+
});
|
|
258
299
|
});
|
|
259
300
|
|
|
260
301
|
describe("Media Type Selection", () => {
|
|
@@ -390,7 +431,7 @@ describe("PlatformContentFields", () => {
|
|
|
390
431
|
// Deep link keys handling - covering lines 109-120, 134, 297-303
|
|
391
432
|
it('should handle deep link keys with array from selection', () => {
|
|
392
433
|
const mockDeepLink = [
|
|
393
|
-
{ value: 'test-deep-link', keys: ['key1', 'key2'] }
|
|
434
|
+
{ value: 'test-deep-link', keys: ['key1', 'key2'] },
|
|
394
435
|
];
|
|
395
436
|
const mockLinkProps = {
|
|
396
437
|
deepLink: mockDeepLink,
|
|
@@ -428,7 +469,7 @@ describe("PlatformContentFields", () => {
|
|
|
428
469
|
|
|
429
470
|
it('should handle deep link keys with single key from selection', () => {
|
|
430
471
|
const mockDeepLink = [
|
|
431
|
-
{ value: 'test-deep-link', keys: 'single-key' }
|
|
472
|
+
{ value: 'test-deep-link', keys: 'single-key' },
|
|
432
473
|
];
|
|
433
474
|
const mockLinkProps = {
|
|
434
475
|
deepLink: mockDeepLink,
|
|
@@ -466,7 +507,7 @@ describe("PlatformContentFields", () => {
|
|
|
466
507
|
|
|
467
508
|
it('should handle deep link keys with no keys from selection but existing keys', () => {
|
|
468
509
|
const mockDeepLink = [
|
|
469
|
-
{ value: 'test-deep-link', keys: [] } // No keys from selection
|
|
510
|
+
{ value: 'test-deep-link', keys: [] }, // No keys from selection
|
|
470
511
|
];
|
|
471
512
|
const mockLinkProps = {
|
|
472
513
|
deepLink: mockDeepLink,
|
|
@@ -503,7 +544,7 @@ describe("PlatformContentFields", () => {
|
|
|
503
544
|
|
|
504
545
|
it('should handle deep link keys with no keys at all', () => {
|
|
505
546
|
const mockDeepLink = [
|
|
506
|
-
{ value: 'test-deep-link', keys: [] } // No keys from selection
|
|
547
|
+
{ value: 'test-deep-link', keys: [] }, // No keys from selection
|
|
507
548
|
];
|
|
508
549
|
const mockLinkProps = {
|
|
509
550
|
deepLink: mockDeepLink,
|
|
@@ -540,7 +581,7 @@ describe("PlatformContentFields", () => {
|
|
|
540
581
|
|
|
541
582
|
it('should handle deep link keys with string value instead of array', () => {
|
|
542
583
|
const mockDeepLink = [
|
|
543
|
-
{ value: 'test-deep-link', keys: 'single-key' }
|
|
584
|
+
{ value: 'test-deep-link', keys: 'single-key' },
|
|
544
585
|
];
|
|
545
586
|
const mockLinkProps = {
|
|
546
587
|
deepLink: mockDeepLink,
|
|
@@ -577,7 +618,7 @@ describe("PlatformContentFields", () => {
|
|
|
577
618
|
|
|
578
619
|
it('should handle deep link keys with undefined deepLinkKeysValue', () => {
|
|
579
620
|
const mockDeepLink = [
|
|
580
|
-
{ value: 'test-deep-link', keys: ['key1', 'key2'] }
|
|
621
|
+
{ value: 'test-deep-link', keys: ['key1', 'key2'] },
|
|
581
622
|
];
|
|
582
623
|
const mockLinkProps = {
|
|
583
624
|
deepLink: mockDeepLink,
|
|
@@ -615,7 +656,7 @@ describe("PlatformContentFields", () => {
|
|
|
615
656
|
|
|
616
657
|
it('should handle deep link keys placeholder with fallback', () => {
|
|
617
658
|
const mockDeepLink = [
|
|
618
|
-
{ value: 'test-deep-link', keys: ['key1', 'key2'] } // Need keys to trigger the section
|
|
659
|
+
{ value: 'test-deep-link', keys: ['key1', 'key2'] }, // Need keys to trigger the section
|
|
619
660
|
];
|
|
620
661
|
const mockLinkProps = {
|
|
621
662
|
deepLink: mockDeepLink,
|
|
@@ -655,7 +696,7 @@ describe("PlatformContentFields", () => {
|
|
|
655
696
|
|
|
656
697
|
it('should handle deep link keys error display', () => {
|
|
657
698
|
const mockDeepLink = [
|
|
658
|
-
{ value: 'test-deep-link', keys: ['key1', 'key2'] }
|
|
699
|
+
{ value: 'test-deep-link', keys: ['key1', 'key2'] },
|
|
659
700
|
];
|
|
660
701
|
const mockLinkProps = {
|
|
661
702
|
deepLink: mockDeepLink,
|
|
@@ -727,7 +768,7 @@ describe("PlatformContentFields", () => {
|
|
|
727
768
|
describe('Deep link query parameter handling', () => {
|
|
728
769
|
it('should match deep link with query parameters', () => {
|
|
729
770
|
const mockDeepLink = [
|
|
730
|
-
{ value: 'myapp://profile', keys: ['userId'] }
|
|
771
|
+
{ value: 'myapp://profile', keys: ['userId'] },
|
|
731
772
|
];
|
|
732
773
|
const mockLinkProps = {
|
|
733
774
|
deepLink: mockDeepLink,
|
|
@@ -766,7 +807,7 @@ describe("PlatformContentFields", () => {
|
|
|
766
807
|
|
|
767
808
|
it('should not match deep link when no base match exists with query parameters', () => {
|
|
768
809
|
const mockDeepLink = [
|
|
769
|
-
{ value: 'myapp://settings', keys: ['theme'] }
|
|
810
|
+
{ value: 'myapp://settings', keys: ['theme'] },
|
|
770
811
|
];
|
|
771
812
|
const mockLinkProps = {
|
|
772
813
|
deepLink: mockDeepLink,
|
|
@@ -805,7 +846,7 @@ describe("PlatformContentFields", () => {
|
|
|
805
846
|
|
|
806
847
|
it('should handle multiple query parameters in deep link value', () => {
|
|
807
848
|
const mockDeepLink = [
|
|
808
|
-
{ value: 'testapp://dashboard', keys: ['category', 'filter'] }
|
|
849
|
+
{ value: 'testapp://dashboard', keys: ['category', 'filter'] },
|
|
809
850
|
];
|
|
810
851
|
const mockLinkProps = {
|
|
811
852
|
deepLink: mockDeepLink,
|
|
@@ -844,7 +885,7 @@ describe("PlatformContentFields", () => {
|
|
|
844
885
|
|
|
845
886
|
it('should handle empty query parameters in deep link value', () => {
|
|
846
887
|
const mockDeepLink = [
|
|
847
|
-
{ value: 'myapp://search', keys: ['query'] }
|
|
888
|
+
{ value: 'myapp://search', keys: ['query'] },
|
|
848
889
|
];
|
|
849
890
|
const mockLinkProps = {
|
|
850
891
|
deepLink: mockDeepLink,
|
|
@@ -860,7 +901,7 @@ describe("PlatformContentFields", () => {
|
|
|
860
901
|
linkType: 'DEEP_LINK',
|
|
861
902
|
};
|
|
862
903
|
|
|
863
|
-
const { getByText } = render(
|
|
904
|
+
const { getByText } = render(
|
|
864
905
|
<IntlProvider locale="en">
|
|
865
906
|
<PlatformContentFields
|
|
866
907
|
deviceType="ANDROID"
|
|
@@ -884,7 +925,7 @@ describe("PlatformContentFields", () => {
|
|
|
884
925
|
|
|
885
926
|
it('should handle tag selection for external link in buttons', () => {
|
|
886
927
|
const mockHandleExternalLinkChange = jest.fn();
|
|
887
|
-
|
|
928
|
+
|
|
888
929
|
// Create a test that actually renders the component and triggers the useCallback
|
|
889
930
|
const { container } = renderComponent({
|
|
890
931
|
content: {
|
|
@@ -89,13 +89,14 @@ import { PlatformContentFields } from "./components";
|
|
|
89
89
|
import { CREATE, EDIT, TRACK_CREATE_MPUSH } from "../App/constants";
|
|
90
90
|
import { validateExternalLink, validateDeepLink } from "./utils";
|
|
91
91
|
import messages from "./messages";
|
|
92
|
-
import { EXTERNAL_URL } from "../CreativesContainer/constants";
|
|
92
|
+
import { EXTERNAL_URL, MOBILE_PUSH } from "../CreativesContainer/constants";
|
|
93
93
|
import createMobilePushPayloadWithIntl from "../../utils/createMobilePushPayload";
|
|
94
94
|
import { MOBILEPUSH } from "../../v2Components/CapVideoUpload/constants";
|
|
95
95
|
import { StyledCapTab } from "./style";
|
|
96
96
|
import TestAndPreviewSlidebox from "../../v2Components/TestAndPreviewSlidebox";
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
import creativesMessages from "../CreativesContainer/messages";
|
|
99
|
+
import { error } from "jquery";
|
|
99
100
|
|
|
100
101
|
// Helper function to extract deep link keys from URL where value equals key
|
|
101
102
|
const extractDeepLinkKeys = (deepLinkValue) => {
|
|
@@ -506,6 +507,8 @@ const MobilePushNew = ({
|
|
|
506
507
|
isGetFormData,
|
|
507
508
|
getTemplateDetailsInProgress,
|
|
508
509
|
onCreateComplete,
|
|
510
|
+
// new flag from parent - when true personalization via tags should be disabled
|
|
511
|
+
restrictPersonalization = false,
|
|
509
512
|
}) => {
|
|
510
513
|
const { formatMessage } = intl;
|
|
511
514
|
|
|
@@ -836,6 +839,14 @@ const MobilePushNew = ({
|
|
|
836
839
|
if (!value || value.trim() === "") {
|
|
837
840
|
error = formatMessage(messages.emptyTemplateDescErrorMessage);
|
|
838
841
|
}
|
|
842
|
+
// personalization restriction check
|
|
843
|
+
if (
|
|
844
|
+
restrictPersonalization
|
|
845
|
+
&& value
|
|
846
|
+
&& ((value.includes("{{") && value.includes("}}")) || (value.includes("[") && value.includes("]")))
|
|
847
|
+
) {
|
|
848
|
+
error = formatMessage(creativesMessages.personalizationTokensErrorMessage);
|
|
849
|
+
}
|
|
839
850
|
return error || "";
|
|
840
851
|
},
|
|
841
852
|
[templateDescErrorHandler, formatMessage]
|
|
@@ -847,9 +858,17 @@ const MobilePushNew = ({
|
|
|
847
858
|
if (!value || value.trim() === "") {
|
|
848
859
|
error = formatMessage(messages.emptyTemplateDescErrorMessage);
|
|
849
860
|
}
|
|
861
|
+
// personalization restriction check
|
|
862
|
+
if (
|
|
863
|
+
restrictPersonalization
|
|
864
|
+
&& value
|
|
865
|
+
&& ((value.includes("{{") && value.includes("}}")) || (value.includes("[") && value.includes("]")))
|
|
866
|
+
) {
|
|
867
|
+
error = formatMessage(creativesMessages.personalizationTokensErrorMessage);
|
|
868
|
+
}
|
|
850
869
|
return error || "";
|
|
851
870
|
},
|
|
852
|
-
[templateDescErrorHandler, formatMessage]
|
|
871
|
+
[templateDescErrorHandler, formatMessage, restrictPersonalization]
|
|
853
872
|
);
|
|
854
873
|
|
|
855
874
|
const handleOnTagsContextChange = useCallback(
|
|
@@ -2001,6 +2020,11 @@ const MobilePushNew = ({
|
|
|
2001
2020
|
tags: tags || [],
|
|
2002
2021
|
injectedTags: injectedTags || {},
|
|
2003
2022
|
selectedOfferDetails,
|
|
2023
|
+
// disable tag button when personalization is restricted
|
|
2024
|
+
disabled: restrictPersonalization,
|
|
2025
|
+
disableTooltipMsg: restrictPersonalization
|
|
2026
|
+
? formatMessage(creativesMessages.personalizationNotSupportedAnonymous)
|
|
2027
|
+
: undefined,
|
|
2004
2028
|
};
|
|
2005
2029
|
|
|
2006
2030
|
// Fix nested ternary for videoAssetList and gifAssetList
|
|
@@ -2101,6 +2125,7 @@ const MobilePushNew = ({
|
|
|
2101
2125
|
tags={tags}
|
|
2102
2126
|
injectedTags={injectedTags}
|
|
2103
2127
|
selectedOfferDetails={selectedOfferDetails}
|
|
2128
|
+
restrictPersonalization={restrictPersonalization}
|
|
2104
2129
|
/>
|
|
2105
2130
|
);
|
|
2106
2131
|
}, [androidContent, iosContent, androidTitleError, iosTitleError, androidMessageError, iosMessageError, androidExternalLinkError, iosExternalLinkError, androidDeepLinkError, iosDeepLinkError, androidDeepLinkKeysError, iosDeepLinkKeysError, formatMessage, activeTab, imageSrc, isFullMode, imageData, androidAssetList, iosAssetList, videoState, videoData, location, tags, injectedTags, selectedOfferDetails, primaryButtonAndroid, secondaryButtonAndroid, primaryButtonIos, secondaryButtonIos, ctaData, deepLink, mobilePushActions, carouselActiveTabIndex, carouselLinkErrors, handleTitleChange, handleMessageChange, handleMediaTypeChange, handleActionOnClickChange, handleLinkTypeChange, handleDeepLinkChange, handleDeepLinkKeysChange, handleExternalLinkChange, onTagSelect, handleOnTagsContextChange, setUpdateMpushImageSrc, updateOnMpushImageReUpload, setUpdateMpushVideoSrc, updateOnMpushVideoReUpload, clearImageDataByMediaType, handleCarouselDataChange, updateCarouselLinkError, sameContent, updateHandler, deleteHandler]
|
|
@@ -2136,6 +2161,9 @@ const MobilePushNew = ({
|
|
|
2136
2161
|
return panes;
|
|
2137
2162
|
}, [isAndroidSupported, isIosSupported, renderContentFields, formatMessage]);
|
|
2138
2163
|
|
|
2164
|
+
const errorInTitle = activeTab === ANDROID ? androidTitleError : iosTitleError;
|
|
2165
|
+
const errorInMessage = activeTab === ANDROID ? androidMessageError : iosMessageError;
|
|
2166
|
+
|
|
2139
2167
|
// Save button disabled logic: only check enabled platforms
|
|
2140
2168
|
const isSaveDisabled = (
|
|
2141
2169
|
(isAndroidSupported && (!androidContent?.title?.trim() || !androidContent?.message?.trim()))
|
|
@@ -2143,6 +2171,7 @@ const MobilePushNew = ({
|
|
|
2143
2171
|
|| templateNameError
|
|
2144
2172
|
|| Object.values(carouselLinkErrors).some((error) => error !== null && error !== "")
|
|
2145
2173
|
|| !isCarouselDataValid()
|
|
2174
|
+
|| errorInTitle || errorInMessage
|
|
2146
2175
|
);
|
|
2147
2176
|
|
|
2148
2177
|
// Validation in handleSave: only show errors for enabled platforms
|
|
@@ -269,4 +269,12 @@ export default defineMessages({
|
|
|
269
269
|
id: `${scope}.gifFileTypeError`,
|
|
270
270
|
defaultMessage: 'Only GIF files are allowed',
|
|
271
271
|
},
|
|
272
|
+
personalizationTagsErrorMessage: {
|
|
273
|
+
id: `${scope}.personalizationTagsErrorMessage`,
|
|
274
|
+
defaultMessage: 'Personalization tags are not supported for anonymous customers, please remove the tags.',
|
|
275
|
+
},
|
|
276
|
+
personalizationNotSupportedAnonymous: {
|
|
277
|
+
id: `${scope}.personalizationNotSupportedAnonymous`,
|
|
278
|
+
defaultMessage: `Personalization tags are not supported for anonymous customers`,
|
|
279
|
+
},
|
|
272
280
|
});
|
|
@@ -72,7 +72,7 @@ export class MobilepushWrapper extends React.Component { // eslint-disable-line
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
render() {
|
|
75
|
-
const {mobilePushCreateMode, step, getFormData, setIsLoadingContent, isGetFormData, query, isFullMode, showTemplateName, type, onValidationFail, onPreviewContentClicked, onTestContentClicked, templateData, eventContextTags = [], showTestAndPreviewSlidebox, handleTestAndPreview, handleCloseTestAndPreview} = this.props;
|
|
75
|
+
const {mobilePushCreateMode, step, getFormData, setIsLoadingContent, isGetFormData, query, isFullMode, showTemplateName, type, onValidationFail, onPreviewContentClicked, onTestContentClicked, templateData, eventContextTags = [], showTestAndPreviewSlidebox, handleTestAndPreview, handleCloseTestAndPreview, restrictPersonalization, isAnonymousType, onPersonalizationTokensChange} = this.props;
|
|
76
76
|
const {templateName} = this.state;
|
|
77
77
|
const isShowMobilepushCreate = !isEmpty(mobilePushCreateMode);
|
|
78
78
|
return (
|
|
@@ -124,6 +124,9 @@ export class MobilepushWrapper extends React.Component { // eslint-disable-line
|
|
|
124
124
|
showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
|
|
125
125
|
handleTestAndPreview={handleTestAndPreview}
|
|
126
126
|
handleCloseTestAndPreview={handleCloseTestAndPreview}
|
|
127
|
+
restrictPersonalization={restrictPersonalization}
|
|
128
|
+
isAnonymousType={isAnonymousType}
|
|
129
|
+
onPersonalizationTokensChange={onPersonalizationTokensChange}
|
|
127
130
|
/>
|
|
128
131
|
|
|
129
132
|
|
|
@@ -154,6 +157,9 @@ MobilepushWrapper.propTypes = {
|
|
|
154
157
|
showTestAndPreviewSlidebox: PropTypes.bool,
|
|
155
158
|
handleTestAndPreview: PropTypes.func,
|
|
156
159
|
handleCloseTestAndPreview: PropTypes.func,
|
|
160
|
+
restrictPersonalization: PropTypes.bool,
|
|
161
|
+
isAnonymousType: PropTypes.bool,
|
|
162
|
+
onPersonalizationTokensChange: PropTypes.func,
|
|
157
163
|
};
|
|
158
164
|
|
|
159
165
|
|