@capillarytech/creatives-library 9.0.56-betaa.0 → 9.0.56

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 (116) hide show
  1. package/constants/unified.js +9 -4
  2. package/package.json +1 -1
  3. package/services/api.js +25 -4
  4. package/utils/common.js +0 -12
  5. package/utils/templateVarUtils.js +16 -0
  6. package/utils/tests/templateVarUtils.test.js +44 -0
  7. package/v2Components/CapActionButton/index.js +73 -60
  8. package/v2Components/CapActionButton/index.scss +44 -28
  9. package/v2Components/CapActionButton/messages.js +7 -3
  10. package/v2Components/CapActionButton/tests/index.test.js +17 -1
  11. package/v2Components/CapWhatsappCTA/messages.js +4 -0
  12. package/v2Components/CapWhatsappCarouselButton/index.js +42 -33
  13. package/v2Components/CapWhatsappCarouselButton/index.scss +44 -2
  14. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +13 -0
  15. package/v2Components/CommonTestAndPreview/actions.js +13 -0
  16. package/v2Components/CommonTestAndPreview/constants.js +8 -0
  17. package/v2Components/CommonTestAndPreview/index.js +55 -3
  18. package/v2Components/CommonTestAndPreview/reducer.js +20 -0
  19. package/v2Components/CommonTestAndPreview/sagas.js +28 -0
  20. package/v2Components/CommonTestAndPreview/selectors.js +6 -0
  21. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +106 -0
  22. package/v2Components/CommonTestAndPreview/tests/actions.test.js +15 -0
  23. package/v2Components/CommonTestAndPreview/tests/constants.test.js +9 -1
  24. package/v2Components/CommonTestAndPreview/tests/index.test.js +101 -0
  25. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +41 -0
  26. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +84 -0
  27. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +12 -0
  28. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +46 -320
  29. package/v2Components/FormBuilder/Functional/channels/registry.js +0 -2
  30. package/v2Components/FormBuilder/Functional/constants.js +8 -43
  31. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +28 -175
  32. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +6 -75
  33. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +20 -10
  34. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +1 -9
  35. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +3 -20
  36. package/v2Components/FormBuilder/Functional/layout/Section.js +1 -6
  37. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +2 -9
  38. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -17
  39. package/v2Components/FormBuilder/index.js +12 -34
  40. package/v2Components/FormBuilder/tests/entryGate.test.js +7 -94
  41. package/v2Components/TestAndPreviewSlidebox/index.js +9 -0
  42. package/v2Containers/CommunicationFlow/CommunicationFlow.js +140 -75
  43. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +18 -2
  44. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +67 -28
  45. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +503 -51
  46. package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +98 -4
  47. package/v2Containers/CommunicationFlow/Tests/constants.test.js +85 -0
  48. package/v2Containers/CommunicationFlow/constants.js +65 -4
  49. package/v2Containers/CommunicationFlow/index.js +16 -23
  50. package/v2Containers/CommunicationFlow/messages.js +11 -3
  51. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +27 -11
  52. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +4 -0
  53. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +176 -4
  54. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +22 -4
  55. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +14 -4
  56. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +11 -0
  57. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +51 -0
  58. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +11 -4
  59. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +19 -5
  60. package/v2Containers/CreativesContainer/SlideBoxContent.js +15 -0
  61. package/v2Containers/CreativesContainer/SlideBoxFooter.js +6 -2
  62. package/v2Containers/CreativesContainer/index.js +29 -3
  63. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +29 -0
  64. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +18 -5
  65. package/v2Containers/CreativesContainer/tests/index.test.js +12 -0
  66. package/v2Containers/MobilePush/Create/index.js +18 -1
  67. package/v2Containers/MobilePush/Create/test/contentValidity.test.js +96 -0
  68. package/v2Containers/MobilePush/Edit/index.js +18 -1
  69. package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +116 -0
  70. package/v2Containers/MobilePush/commonMethods.js +49 -1
  71. package/v2Containers/MobilePushNew/index.js +29 -4
  72. package/v2Containers/MobilePushNew/tests/index.test.js +119 -0
  73. package/v2Containers/MobilePushNew/tests/utils.test.js +82 -0
  74. package/v2Containers/MobilePushNew/utils.js +34 -1
  75. package/v2Containers/MobilepushWrapper/index.js +3 -1
  76. package/v2Containers/Rcs/index.js +34 -0
  77. package/v2Containers/Rcs/index.scss +15 -0
  78. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +115 -19
  79. package/v2Containers/Rcs/tests/index.test.js +67 -0
  80. package/v2Containers/Sms/Create/index.js +3 -1
  81. package/v2Containers/Sms/Edit/index.js +25 -0
  82. package/v2Containers/Sms/Edit/tests/index.test.js +85 -0
  83. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +27 -3
  84. package/v2Containers/Viber/index.js +24 -1
  85. package/v2Containers/Viber/tests/index.test.js +103 -0
  86. package/v2Containers/WebPush/Create/index.js +19 -0
  87. package/v2Containers/WebPush/Create/preview/WebPushPreview.js +17 -12
  88. package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +5 -0
  89. package/v2Containers/WebPush/Create/tests/contentValidity.test.js +294 -0
  90. package/v2Containers/Whatsapp/index.js +25 -4
  91. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +306 -34
  92. package/v2Containers/Whatsapp/tests/index.test.js +115 -0
  93. package/v2Containers/Zalo/index.js +28 -1
  94. package/v2Containers/Zalo/tests/index.test.js +99 -0
  95. package/v2Containers/mockdata.js +25 -0
  96. package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +0 -10
  97. package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +0 -82
  98. package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +0 -74
  99. package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +0 -28
  100. package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +0 -19
  101. package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +0 -45
  102. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +0 -162
  103. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +0 -37
  104. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +0 -70
  105. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +0 -274
  106. package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +0 -251
  107. package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +0 -89
  108. package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +0 -440
  109. package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +0 -430
  110. package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +0 -282
  111. package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +0 -141
  112. package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +0 -306
  113. package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +0 -151
  114. package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +0 -342
  115. package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +0 -110
  116. package/v2Components/FormBuilder/tests/mpush.characterization.test.js +0 -459
@@ -158,6 +158,7 @@ const renderHelper = (args) => {
158
158
  {...(args.omitGetDefaultTags ? {} : { getDefaultTags: true })}
159
159
  isDltEnabled={args.isDltEnabled || false}
160
160
  smsRegister={'DLT'}
161
+ onContentValidityChange={args.onContentValidityChange}
161
162
  />
162
163
  </Provider>,
163
164
  );
@@ -1923,6 +1924,72 @@ describe('Character Counting Functions', () => {
1923
1924
  });
1924
1925
  });
1925
1926
 
1927
+ describe('Rcs — onContentValidityChange reporting', () => {
1928
+ // Full-mode create with a fresh (non-hydrating) rcsData: templateDesc starts out empty,
1929
+ // same as the pre-existing "template title change" test's initial-state assumption.
1930
+ it('reports isContentEmpty: true on mount when the (default text_message) description is empty', () => {
1931
+ const onContentValidityChange = jest.fn();
1932
+ renderHelper({ onContentValidityChange });
1933
+
1934
+ expect(onContentValidityChange).toHaveBeenCalledWith(
1935
+ expect.objectContaining({ isContentEmpty: true }),
1936
+ );
1937
+ });
1938
+
1939
+ it('reports isContentEmpty: false once the description has content, then true again once cleared', () => {
1940
+ const onContentValidityChange = jest.fn();
1941
+ renderHelper({ onContentValidityChange });
1942
+ onContentValidityChange.mockClear();
1943
+
1944
+ const descTextArea = renderedComponent.find('CapInputTextArea#rcs_template_message_textarea').at(0);
1945
+ act(() => {
1946
+ descTextArea.props().onChange({ target: { value: 'Hello there' } });
1947
+ });
1948
+ renderedComponent.update();
1949
+
1950
+ expect(onContentValidityChange).toHaveBeenCalledWith(
1951
+ expect.objectContaining({ isContentEmpty: false }),
1952
+ );
1953
+ onContentValidityChange.mockClear();
1954
+
1955
+ const descTextAreaAfter = renderedComponent.find('CapInputTextArea#rcs_template_message_textarea').at(0);
1956
+ act(() => {
1957
+ descTextAreaAfter.props().onChange({ target: { value: ' ' } });
1958
+ });
1959
+ renderedComponent.update();
1960
+
1961
+ expect(onContentValidityChange).toHaveBeenCalledWith(
1962
+ expect.objectContaining({ isContentEmpty: true }),
1963
+ );
1964
+ });
1965
+
1966
+ it('does not throw and does not report when onContentValidityChange is not provided', () => {
1967
+ expect(() => {
1968
+ renderHelper({});
1969
+ const descTextArea = renderedComponent.find('CapInputTextArea#rcs_template_message_textarea').at(0);
1970
+ act(() => {
1971
+ descTextArea.props().onChange({ target: { value: 'abc' } });
1972
+ });
1973
+ renderedComponent.update();
1974
+ }).not.toThrow();
1975
+ });
1976
+
1977
+ it('does not dispatch again on a re-render that does not change validity (dedup guard)', () => {
1978
+ const onContentValidityChange = jest.fn();
1979
+ renderHelper({ onContentValidityChange });
1980
+ onContentValidityChange.mockClear();
1981
+
1982
+ const descTextArea = renderedComponent.find('CapInputTextArea#rcs_template_message_textarea').at(0);
1983
+ act(() => {
1984
+ // Still empty (still whitespace-only) — validity does not actually change.
1985
+ descTextArea.props().onChange({ target: { value: ' ' } });
1986
+ });
1987
+ renderedComponent.update();
1988
+
1989
+ expect(onContentValidityChange).not.toHaveBeenCalled();
1990
+ });
1991
+ });
1992
+
1926
1993
  describe('CapActionButton (mocked within RCS tests)', () => {
1927
1994
  it('should render the mocked CapActionButton placeholder', () => {
1928
1995
  const wrapper = mountWithIntl(
@@ -185,7 +185,9 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
185
185
  }
186
186
 
187
187
  componentDidUpdate() {
188
- if (!this.props.embeddedSmsFallback || typeof this.props.onEmbeddedSmsFooterValidity !== 'function') {
188
+ // Reporting itself is opt-in via the callback prop — not tied to embeddedSmsFallback,
189
+ // which is a separate, unrelated flag (tag-popover styling for the SMS-fallback surface).
190
+ if (typeof this.props.onEmbeddedSmsFooterValidity !== 'function') {
189
191
  return;
190
192
  }
191
193
  const validity = getSmsEmbeddedFooterValidity(this.state.formData, this.state.tabCount);
@@ -34,6 +34,7 @@ import v2EditSmsReducer from './reducer';
34
34
  import { v2SmsEditSagas } from './sagas';
35
35
  import * as globalActions from '../../Cap/actions';
36
36
  import TestAndPreviewSlidebox from '../../../v2Components/TestAndPreviewSlidebox';
37
+ import { getSmsEmbeddedFooterValidity } from '../smsFormDataHelpers';
37
38
 
38
39
  export class Edit extends React.Component { // eslint-disable-line react/prefer-stateless-function
39
40
  constructor(props) {
@@ -54,6 +55,11 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
54
55
  isTestAndPreviewMode: false,
55
56
  pendingGetFormData: false,
56
57
  };
58
+ // Tracks the last validity value reported to the parent so componentDidUpdate does not
59
+ // dispatch on every render. Intentionally undefined (not true) so the first render
60
+ // always reports the real validity rather than assuming the form starts invalid.
61
+ this._lastReportedSmsFooterTemplateNameEmpty = undefined;
62
+ this._lastReportedSmsFooterMessageEmpty = undefined;
57
63
  this.saveFormData = this.saveFormData.bind(this);
58
64
  this.onFormDataChange = this.onFormDataChange.bind(this);
59
65
  this.onTemplateNameChange = this.onTemplateNameChange.bind(this);
@@ -189,6 +195,24 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
189
195
  }
190
196
  }
191
197
 
198
+ componentDidUpdate() {
199
+ // Reports live form validity so the host (CreativesContainer) can disable Done/Preview
200
+ // and Test as soon as the message body is cleared — mirrors Sms/Create's own reporting.
201
+ if (typeof this.props.onEmbeddedSmsFooterValidity !== 'function') {
202
+ return;
203
+ }
204
+ const validity = getSmsEmbeddedFooterValidity(this.state.formData, this.state.tabCount);
205
+ const isTemplateNameEmpty = !!validity.isTemplateNameEmpty;
206
+ const isMessageEmpty = !!validity.isMessageEmpty;
207
+ if (
208
+ this._lastReportedSmsFooterTemplateNameEmpty === isTemplateNameEmpty &&
209
+ this._lastReportedSmsFooterMessageEmpty === isMessageEmpty
210
+ ) return;
211
+ this._lastReportedSmsFooterTemplateNameEmpty = isTemplateNameEmpty;
212
+ this._lastReportedSmsFooterMessageEmpty = isMessageEmpty;
213
+ this.props.onEmbeddedSmsFooterValidity(validity);
214
+ }
215
+
192
216
  componentWillUnmount() {
193
217
  if (this.pendingGetFormDataTimeout) {
194
218
  clearTimeout(this.pendingGetFormDataTimeout);
@@ -1138,6 +1162,7 @@ Edit.propTypes = {
1138
1162
  handleTestAndPreview: PropTypes.func,
1139
1163
  handleCloseTestAndPreview: PropTypes.func,
1140
1164
  isTestAndPreviewMode: PropTypes.bool,
1165
+ onEmbeddedSmsFooterValidity: PropTypes.func,
1141
1166
  };
1142
1167
 
1143
1168
  const mapStateToProps = createStructuredSelector({
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import { shallow } from 'enzyme';
3
+ import { Edit } from '../index';
4
+
5
+ // Shallow-render the plain class (bypassing the withCreatives/Redux/saga HOC) so this test
6
+ // only exercises the componentDidUpdate → onEmbeddedSmsFooterValidity wiring in isolation.
7
+
8
+ const baseProps = () => ({
9
+ actions: {
10
+ getTemplateDetails: jest.fn(),
11
+ setTemplateDetails: jest.fn(),
12
+ clearEditResponse: jest.fn(),
13
+ resetEditTemplate: jest.fn(),
14
+ editTemplate: jest.fn(),
15
+ },
16
+ globalActions: {
17
+ fetchSchemaForEntity: jest.fn(),
18
+ setInjectedTags: jest.fn(),
19
+ addMessageToQueue: jest.fn(),
20
+ },
21
+ Edit: {},
22
+ params: { id: 'tpl-1' },
23
+ location: { query: { type: 'embedded', module: 'library' } },
24
+ metaEntities: {},
25
+ intl: { formatMessage: (m) => m.defaultMessage || m.id },
26
+ isFullMode: false,
27
+ getFormSubscriptionData: jest.fn(),
28
+ });
29
+
30
+ describe('Sms/Edit — onEmbeddedSmsFooterValidity reporting', () => {
31
+ it('reports isMessageEmpty: true on mount when the message body is empty', () => {
32
+ const onEmbeddedSmsFooterValidity = jest.fn();
33
+ const wrapper = shallow(<Edit {...baseProps()} onEmbeddedSmsFooterValidity={onEmbeddedSmsFooterValidity} />);
34
+ wrapper.setState({ formData: { 0: { 'sms-editor': '' }, base: { 'sms-editor': '' } }, tabCount: 1 });
35
+
36
+ expect(onEmbeddedSmsFooterValidity).toHaveBeenCalledWith(
37
+ expect.objectContaining({ isMessageEmpty: true }),
38
+ );
39
+ });
40
+
41
+ it('reports isMessageEmpty: false once the message body has content', () => {
42
+ const onEmbeddedSmsFooterValidity = jest.fn();
43
+ const wrapper = shallow(<Edit {...baseProps()} onEmbeddedSmsFooterValidity={onEmbeddedSmsFooterValidity} />);
44
+ wrapper.setState({ formData: { 0: { 'sms-editor': '' }, base: { 'sms-editor': '' } }, tabCount: 1 });
45
+ onEmbeddedSmsFooterValidity.mockClear();
46
+
47
+ wrapper.setState({ formData: { 0: { 'sms-editor': 'Hello there' }, base: { 'sms-editor': 'Hello there' } }, tabCount: 1 });
48
+
49
+ expect(onEmbeddedSmsFooterValidity).toHaveBeenCalledWith(
50
+ expect.objectContaining({ isMessageEmpty: false }),
51
+ );
52
+ });
53
+
54
+ it('re-reports empty when an existing (edit-loaded) message is cleared out', () => {
55
+ const onEmbeddedSmsFooterValidity = jest.fn();
56
+ const wrapper = shallow(<Edit {...baseProps()} onEmbeddedSmsFooterValidity={onEmbeddedSmsFooterValidity} />);
57
+ wrapper.setState({ formData: { 0: { 'sms-editor': 'Existing content' }, base: { 'sms-editor': 'Existing content' } }, tabCount: 1 });
58
+ onEmbeddedSmsFooterValidity.mockClear();
59
+
60
+ wrapper.setState({ formData: { 0: { 'sms-editor': '' }, base: { 'sms-editor': '' } }, tabCount: 1 });
61
+
62
+ expect(onEmbeddedSmsFooterValidity).toHaveBeenCalledWith(
63
+ expect.objectContaining({ isMessageEmpty: true }),
64
+ );
65
+ });
66
+
67
+ it('does not throw and does not report when onEmbeddedSmsFooterValidity is not provided', () => {
68
+ const wrapper = shallow(<Edit {...baseProps()} />);
69
+ expect(() => {
70
+ wrapper.setState({ formData: { 0: { 'sms-editor': '' } }, tabCount: 1 });
71
+ }).not.toThrow();
72
+ });
73
+
74
+ it('does not dispatch again on a re-render that does not change validity (dedup guard)', () => {
75
+ const onEmbeddedSmsFooterValidity = jest.fn();
76
+ const wrapper = shallow(<Edit {...baseProps()} onEmbeddedSmsFooterValidity={onEmbeddedSmsFooterValidity} />);
77
+ wrapper.setState({ formData: { 0: { 'sms-editor': '' } }, tabCount: 1 });
78
+ onEmbeddedSmsFooterValidity.mockClear();
79
+
80
+ // Unrelated state change — validity tuple (isTemplateNameEmpty/isMessageEmpty) is unchanged.
81
+ wrapper.setState({ currentTab: 1 });
82
+
83
+ expect(onEmbeddedSmsFooterValidity).not.toHaveBeenCalled();
84
+ });
85
+ });
@@ -12707,6 +12707,7 @@ FREE GIFTS-
12707
12707
  "getTestGroupsRequested": [Function],
12708
12708
  "getWeCrmAccountsRequested": [Function],
12709
12709
  "searchCustomersRequested": [Function],
12710
+ "sendCcsTestMessageRequested": [Function],
12710
12711
  "sendTestMessageRequested": [Function],
12711
12712
  "updatePreviewRequested": [Function],
12712
12713
  }
@@ -12752,6 +12753,7 @@ FREE GIFTS-
12752
12753
  isFetchingTestGroups={false}
12753
12754
  isLoadingSenderDetails={false}
12754
12755
  isSearchingCustomer={false}
12756
+ isSendingCcsTestMessage={false}
12755
12757
  isSendingTestMessage={false}
12756
12758
  isUpdatingPreview={false}
12757
12759
  messageMetaConfigId={null}
@@ -12788,11 +12790,13 @@ FREE GIFTS-
12788
12790
  "getTestGroupsRequested": [Function],
12789
12791
  "getWeCrmAccountsRequested": [Function],
12790
12792
  "searchCustomersRequested": [Function],
12793
+ "sendCcsTestMessageRequested": [Function],
12791
12794
  "sendTestMessageRequested": [Function],
12792
12795
  "updatePreviewRequested": [Function],
12793
12796
  }
12794
12797
  }
12795
12798
  beeInstance={null}
12799
+ ccsSendTransaction={null}
12796
12800
  channel={null}
12797
12801
  content="LATEST FASHION@VISHAL
12798
12802
  {#var#}
@@ -15081,6 +15085,7 @@ new message content.",
15081
15085
  isFetchingTestGroups={false}
15082
15086
  isLoadingSenderDetails={false}
15083
15087
  isSearchingCustomer={false}
15088
+ isSendingCcsTestMessage={false}
15084
15089
  isSendingTestMessage={false}
15085
15090
  isUpdatingPreview={false}
15086
15091
  messageMetaConfigId={null}
@@ -15119,11 +15124,13 @@ new message content.",
15119
15124
  "getTestGroupsRequested": [Function],
15120
15125
  "getWeCrmAccountsRequested": [Function],
15121
15126
  "searchCustomersRequested": [Function],
15127
+ "sendCcsTestMessageRequested": [Function],
15122
15128
  "sendTestMessageRequested": [Function],
15123
15129
  "updatePreviewRequested": [Function],
15124
15130
  }
15125
15131
  }
15126
15132
  beeInstance={null}
15133
+ ccsSendTransaction={null}
15127
15134
  channel="SMS"
15128
15135
  config={
15129
15136
  Object {
@@ -17419,6 +17426,7 @@ new message content.",
17419
17426
  isFetchingTestGroups={false}
17420
17427
  isLoadingSenderDetails={false}
17421
17428
  isSearchingCustomer={false}
17429
+ isSendingCcsTestMessage={false}
17422
17430
  isSendingTestMessage={false}
17423
17431
  isUpdatingPreview={false}
17424
17432
  messageMetaConfigId={null}
@@ -17638,7 +17646,7 @@ FREE GIFTS-
17638
17646
  </CapRow>
17639
17647
  }
17640
17648
  show={false}
17641
- size="size-xl"
17649
+ size="size-l"
17642
17650
  />
17643
17651
  </CommonTestAndPreview>
17644
17652
  </TestAndPreviewSlidebox>
@@ -31827,6 +31835,7 @@ FREE GIFTS-
31827
31835
  "getTestGroupsRequested": [Function],
31828
31836
  "getWeCrmAccountsRequested": [Function],
31829
31837
  "searchCustomersRequested": [Function],
31838
+ "sendCcsTestMessageRequested": [Function],
31830
31839
  "sendTestMessageRequested": [Function],
31831
31840
  "updatePreviewRequested": [Function],
31832
31841
  }
@@ -31872,6 +31881,7 @@ FREE GIFTS-
31872
31881
  isFetchingTestGroups={false}
31873
31882
  isLoadingSenderDetails={false}
31874
31883
  isSearchingCustomer={false}
31884
+ isSendingCcsTestMessage={false}
31875
31885
  isSendingTestMessage={false}
31876
31886
  isUpdatingPreview={false}
31877
31887
  messageMetaConfigId={null}
@@ -31908,11 +31918,13 @@ FREE GIFTS-
31908
31918
  "getTestGroupsRequested": [Function],
31909
31919
  "getWeCrmAccountsRequested": [Function],
31910
31920
  "searchCustomersRequested": [Function],
31921
+ "sendCcsTestMessageRequested": [Function],
31911
31922
  "sendTestMessageRequested": [Function],
31912
31923
  "updatePreviewRequested": [Function],
31913
31924
  }
31914
31925
  }
31915
31926
  beeInstance={null}
31927
+ ccsSendTransaction={null}
31916
31928
  channel={null}
31917
31929
  content="LATEST FASHION@VISHAL
31918
31930
  test1
@@ -34201,6 +34213,7 @@ new message content.",
34201
34213
  isFetchingTestGroups={false}
34202
34214
  isLoadingSenderDetails={false}
34203
34215
  isSearchingCustomer={false}
34216
+ isSendingCcsTestMessage={false}
34204
34217
  isSendingTestMessage={false}
34205
34218
  isUpdatingPreview={false}
34206
34219
  messageMetaConfigId={null}
@@ -34239,11 +34252,13 @@ new message content.",
34239
34252
  "getTestGroupsRequested": [Function],
34240
34253
  "getWeCrmAccountsRequested": [Function],
34241
34254
  "searchCustomersRequested": [Function],
34255
+ "sendCcsTestMessageRequested": [Function],
34242
34256
  "sendTestMessageRequested": [Function],
34243
34257
  "updatePreviewRequested": [Function],
34244
34258
  }
34245
34259
  }
34246
34260
  beeInstance={null}
34261
+ ccsSendTransaction={null}
34247
34262
  channel="SMS"
34248
34263
  config={
34249
34264
  Object {
@@ -36539,6 +36554,7 @@ new message content.",
36539
36554
  isFetchingTestGroups={false}
36540
36555
  isLoadingSenderDetails={false}
36541
36556
  isSearchingCustomer={false}
36557
+ isSendingCcsTestMessage={false}
36542
36558
  isSendingTestMessage={false}
36543
36559
  isUpdatingPreview={false}
36544
36560
  messageMetaConfigId={null}
@@ -36758,7 +36774,7 @@ FREE GIFTS-
36758
36774
  </CapRow>
36759
36775
  }
36760
36776
  show={false}
36761
- size="size-xl"
36777
+ size="size-l"
36762
36778
  />
36763
36779
  </CommonTestAndPreview>
36764
36780
  </TestAndPreviewSlidebox>
@@ -49483,6 +49499,7 @@ FREE GIFTS-
49483
49499
  "getTestGroupsRequested": [Function],
49484
49500
  "getWeCrmAccountsRequested": [Function],
49485
49501
  "searchCustomersRequested": [Function],
49502
+ "sendCcsTestMessageRequested": [Function],
49486
49503
  "sendTestMessageRequested": [Function],
49487
49504
  "updatePreviewRequested": [Function],
49488
49505
  }
@@ -49528,6 +49545,7 @@ FREE GIFTS-
49528
49545
  isFetchingTestGroups={false}
49529
49546
  isLoadingSenderDetails={false}
49530
49547
  isSearchingCustomer={false}
49548
+ isSendingCcsTestMessage={false}
49531
49549
  isSendingTestMessage={false}
49532
49550
  isUpdatingPreview={false}
49533
49551
  messageMetaConfigId={null}
@@ -49564,11 +49582,13 @@ FREE GIFTS-
49564
49582
  "getTestGroupsRequested": [Function],
49565
49583
  "getWeCrmAccountsRequested": [Function],
49566
49584
  "searchCustomersRequested": [Function],
49585
+ "sendCcsTestMessageRequested": [Function],
49567
49586
  "sendTestMessageRequested": [Function],
49568
49587
  "updatePreviewRequested": [Function],
49569
49588
  }
49570
49589
  }
49571
49590
  beeInstance={null}
49591
+ ccsSendTransaction={null}
49572
49592
  channel={null}
49573
49593
  content="LATEST FASHION@VISHAL
49574
49594
  {#var#}
@@ -51857,6 +51877,7 @@ new message content.",
51857
51877
  isFetchingTestGroups={false}
51858
51878
  isLoadingSenderDetails={false}
51859
51879
  isSearchingCustomer={false}
51880
+ isSendingCcsTestMessage={false}
51860
51881
  isSendingTestMessage={false}
51861
51882
  isUpdatingPreview={false}
51862
51883
  messageMetaConfigId={null}
@@ -51895,11 +51916,13 @@ new message content.",
51895
51916
  "getTestGroupsRequested": [Function],
51896
51917
  "getWeCrmAccountsRequested": [Function],
51897
51918
  "searchCustomersRequested": [Function],
51919
+ "sendCcsTestMessageRequested": [Function],
51898
51920
  "sendTestMessageRequested": [Function],
51899
51921
  "updatePreviewRequested": [Function],
51900
51922
  }
51901
51923
  }
51902
51924
  beeInstance={null}
51925
+ ccsSendTransaction={null}
51903
51926
  channel="SMS"
51904
51927
  config={
51905
51928
  Object {
@@ -54195,6 +54218,7 @@ new message content.",
54195
54218
  isFetchingTestGroups={false}
54196
54219
  isLoadingSenderDetails={false}
54197
54220
  isSearchingCustomer={false}
54221
+ isSendingCcsTestMessage={false}
54198
54222
  isSendingTestMessage={false}
54199
54223
  isUpdatingPreview={false}
54200
54224
  messageMetaConfigId={null}
@@ -54414,7 +54438,7 @@ FREE GIFTS-
54414
54438
  </CapRow>
54415
54439
  }
54416
54440
  show={false}
54417
- size="size-xl"
54441
+ size="size-l"
54418
54442
  />
54419
54443
  </CommonTestAndPreview>
54420
54444
  </TestAndPreviewSlidebox>
@@ -1,4 +1,6 @@
1
- import React, { useState, useEffect, useCallback } from 'react';
1
+ import React, {
2
+ useState, useEffect, useCallback, useRef,
3
+ } from 'react';
2
4
  import { bindActionCreators } from 'redux';
3
5
  import { createStructuredSelector } from 'reselect';
4
6
  import { injectIntl, FormattedMessage } from 'react-intl';
@@ -139,6 +141,7 @@ export const Viber = (props) => {
139
141
  showTestAndPreviewSlidebox: propsShowTestAndPreviewSlidebox,
140
142
  handleTestAndPreview: propsHandleTestAndPreview,
141
143
  handleCloseTestAndPreview: propsHandleCloseTestAndPreview,
144
+ onContentValidityChange,
142
145
  } = props || {};
143
146
 
144
147
  const { formatMessage } = intl;
@@ -257,6 +260,26 @@ export const Viber = (props) => {
257
260
  }
258
261
  }, [viber.templateDetails || templateData]);
259
262
 
263
+ // Reports live message-content validity to a parent (e.g. CreativesContainer's
264
+ // slidebox) so it can disable its own Done/Preview-and-test buttons whenever the
265
+ // Viber message body is empty/whitespace-only. Mirrors the Sms Create/Edit
266
+ // componentDidUpdate pattern, using a ref (instead of an instance field) as the
267
+ // dedup guard so the callback only fires when the reported value actually changes -
268
+ // calling it unconditionally on every render risks an infinite update loop when the
269
+ // parent's setState triggers a re-render of this component.
270
+ const lastReportedContentEmptyRef = useRef(undefined);
271
+ useEffect(() => {
272
+ if (typeof onContentValidityChange !== 'function') {
273
+ return;
274
+ }
275
+ const isContentEmpty = (messageContent || '').trim() === '';
276
+ if (lastReportedContentEmptyRef.current === isContentEmpty) {
277
+ return;
278
+ }
279
+ lastReportedContentEmptyRef.current = isContentEmpty;
280
+ onContentValidityChange({ isContentEmpty });
281
+ }, [messageContent, onContentValidityChange]);
282
+
260
283
  // Text area Code start here
261
284
 
262
285
  // Tags Code start from here
@@ -469,4 +469,107 @@ describe('Test Viber container', () => {
469
469
 
470
470
  expect(await screen.findByText("URL can't be empty")).toBeInTheDocument();
471
471
  });
472
+
473
+ describe('onContentValidityChange reporting (Done/Preview button gating in the slidebox)', () => {
474
+ it('reports isContentEmpty: true on mount for an empty create flow, then false once a message is typed', async () => {
475
+ const onContentValidityChange = jest.fn();
476
+ renderComponent({
477
+ actions: mockActions,
478
+ globalActions: mockGlobalActions,
479
+ templateData: { mode: 'create' },
480
+ viber: {
481
+ uploadedAssetData: {},
482
+ createTemplateInProgress: false,
483
+ },
484
+ location: {
485
+ pathname: '/sms/edit',
486
+ query: { type: false, module: 'default' },
487
+ search: '',
488
+ },
489
+ isFullMode: true,
490
+ handleClose: jest.fn(),
491
+ onContentValidityChange,
492
+ });
493
+
494
+ await waitFor(() => {
495
+ expect(onContentValidityChange).toHaveBeenCalledWith({ isContentEmpty: true });
496
+ });
497
+
498
+ const msgBox = screen.getAllByPlaceholderText(/Enter message/i)[0];
499
+ fireEvent.change(msgBox, { target: { value: 'hello there' } });
500
+
501
+ await waitFor(() => {
502
+ expect(onContentValidityChange).toHaveBeenLastCalledWith({ isContentEmpty: false });
503
+ });
504
+ });
505
+
506
+ it('does not re-report the same validity value (dedup guard)', async () => {
507
+ const onContentValidityChange = jest.fn();
508
+ renderComponent({
509
+ actions: mockActions,
510
+ globalActions: mockGlobalActions,
511
+ templateData: { mode: 'create' },
512
+ viber: {
513
+ uploadedAssetData: {},
514
+ createTemplateInProgress: false,
515
+ },
516
+ location: {
517
+ pathname: '/sms/edit',
518
+ query: { type: false, module: 'default' },
519
+ search: '',
520
+ },
521
+ isFullMode: true,
522
+ handleClose: jest.fn(),
523
+ onContentValidityChange,
524
+ });
525
+
526
+ await waitFor(() => {
527
+ expect(onContentValidityChange).toHaveBeenCalledWith({ isContentEmpty: true });
528
+ });
529
+ const callsAfterMount = onContentValidityChange.mock.calls.length;
530
+
531
+ // Whitespace-only change: still empty, should not trigger another call.
532
+ const msgBox = screen.getAllByPlaceholderText(/Enter message/i)[0];
533
+ fireEvent.change(msgBox, { target: { value: ' ' } });
534
+
535
+ expect(onContentValidityChange).toHaveBeenCalledTimes(callsAfterMount);
536
+ });
537
+
538
+ it('reports isContentEmpty: false for an edit flow pre-filled with message content, then true once cleared', async () => {
539
+ const onContentValidityChange = jest.fn();
540
+ renderComponent({
541
+ actions: mockActions,
542
+ globalActions: mockGlobalActions,
543
+ templateData: { mode: 'create' },
544
+ viber: {
545
+ uploadedAssetData: {},
546
+ createTemplateInProgress: false,
547
+ templateDetails: templateDetailsText,
548
+ },
549
+ location: {
550
+ pathname: '/sms/edit',
551
+ query: { type: false, module: 'default' },
552
+ search: '',
553
+ },
554
+ isFullMode: true,
555
+ params: { id: 'test' },
556
+ handleClose: jest.fn(),
557
+ metaEntities,
558
+ getDefaultTags,
559
+ injectedTags,
560
+ onContentValidityChange,
561
+ });
562
+
563
+ await waitFor(() => {
564
+ expect(onContentValidityChange).toHaveBeenLastCalledWith({ isContentEmpty: false });
565
+ });
566
+
567
+ const msgBox = screen.getAllByPlaceholderText(/Enter message/i)[0];
568
+ fireEvent.change(msgBox, { target: { value: '' } });
569
+
570
+ await waitFor(() => {
571
+ expect(onContentValidityChange).toHaveBeenLastCalledWith({ isContentEmpty: true });
572
+ });
573
+ });
574
+ });
472
575
  });
@@ -164,6 +164,7 @@ const WebPushCreate = ({
164
164
  templateActions: templateActionsProps,
165
165
  Templates,
166
166
  restrictPersonalization = false,
167
+ onContentValidityChange,
167
168
  }) => {
168
169
  const { formatMessage } = intl;
169
170
  const aiContentBotDisabled = isAiContentBotDisabled();
@@ -194,6 +195,10 @@ const WebPushCreate = ({
194
195
  const messageCountRef = useRef(null);
195
196
  const saveInitiatedRef = useRef(false);
196
197
  const messageTextAreaRef = useRef(null);
198
+ // Tracks the last isContentEmpty value reported to the parent (CreativesContainer) so the
199
+ // effect below does not dispatch on every render. Intentionally undefined (not a boolean)
200
+ // so the first render always reports the real validity rather than assuming a starting value.
201
+ const lastReportedContentEmptyRef = useRef(undefined);
197
202
 
198
203
  // Custom hooks
199
204
  const { updateCharacterCount } = useCharacterCount(formatMessage, messages);
@@ -566,6 +571,19 @@ const WebPushCreate = ({
566
571
  });
567
572
  }, [isFullMode, templateName]);
568
573
 
574
+ // Dedup guard (lastReportedContentEmptyRef) avoids re-triggering the parent's setState loop.
575
+ useEffect(() => {
576
+ if (typeof onContentValidityChange !== 'function') {
577
+ return;
578
+ }
579
+ const isContentEmpty = !notificationTitle.trim() || !message.trim();
580
+ if (lastReportedContentEmptyRef.current === isContentEmpty) {
581
+ return;
582
+ }
583
+ lastReportedContentEmptyRef.current = isContentEmpty;
584
+ onContentValidityChange({ isContentEmpty });
585
+ }, [notificationTitle, message, onContentValidityChange]);
586
+
569
587
  // Pure validator that returns boolean without setting error state
570
588
  const validateFormSilent = () => {
571
589
  const templateNameInvalid = isFullMode && validateTemplateName(templateName);
@@ -1175,6 +1193,7 @@ WebPushCreate.propTypes = {
1175
1193
  waitEventContextTags: PropTypes.object,
1176
1194
  templateActions: PropTypes.object,
1177
1195
  restrictPersonalization: PropTypes.bool,
1196
+ onContentValidityChange: PropTypes.func,
1178
1197
  };
1179
1198
 
1180
1199
  WebPushCreate.defaultProps = {