@capillarytech/creatives-library 8.0.359-alpha.0 → 8.0.360-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/index.html +1 -0
  2. package/package.json +1 -1
  3. package/utils/cdnTransformation.js +75 -3
  4. package/utils/tests/cdnTransformation.test.js +127 -0
  5. package/v2Components/CommonTestAndPreview/UnifiedPreview/PreviewHeader.js +16 -0
  6. package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberCarouselPreviewCards.js +132 -0
  7. package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberPreviewContent.js +2 -37
  8. package/v2Components/CommonTestAndPreview/UnifiedPreview/WebPushPreviewContent.js +169 -0
  9. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +55 -85
  10. package/v2Components/CommonTestAndPreview/UnifiedPreview/_viberCarouselPreviewCards.scss +127 -0
  11. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +52 -6
  12. package/v2Components/CommonTestAndPreview/constants.js +2 -0
  13. package/v2Components/CommonTestAndPreview/index.js +67 -3
  14. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/PreviewHeader.test.js +163 -0
  15. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WebPushPreviewContent.test.js +522 -0
  16. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +255 -0
  17. package/v2Components/CommonTestAndPreview/tests/constants.test.js +2 -1
  18. package/v2Components/CommonTestAndPreview/tests/index.test.js +194 -0
  19. package/v2Components/FormBuilder/index.js +162 -52
  20. package/v2Components/TestAndPreviewSlidebox/index.js +2 -2
  21. package/v2Containers/App/constants.js +3 -0
  22. package/v2Containers/App/tests/constants.test.js +61 -0
  23. package/v2Containers/CreativesContainer/index.js +60 -24
  24. package/v2Containers/Templates/index.js +72 -2
  25. package/v2Containers/Templates/sagas.js +6 -1
  26. package/v2Containers/Templates/tests/sagas.test.js +23 -6
  27. package/v2Containers/Templates/tests/webpush.test.js +375 -0
  28. package/v2Containers/Viber/index.js +24 -18
  29. package/v2Containers/Viber/index.scss +27 -0
  30. package/v2Containers/Viber/messages.js +4 -4
  31. package/v2Containers/WebPush/Create/index.js +91 -8
  32. package/v2Containers/WebPush/Create/index.scss +7 -0
  33. package/v2Containers/WebPush/Create/tests/getTemplateContent.test.js +348 -0
  34. package/v2Containers/WebPush/Create/tests/testAndPreviewIntegration.test.js +325 -0
@@ -0,0 +1,375 @@
1
+ /**
2
+ * Tests for Templates container - WEBPUSH channel additions
3
+ *
4
+ * Covers:
5
+ * - extractTemplateContentForPreview for WEBPUSH channel
6
+ * - isTestAndPreviewSupported with WEBPUSH channel
7
+ */
8
+
9
+ import React from 'react';
10
+ import { shallowWithIntl } from '../../../helpers/intl-enzym-test-helpers';
11
+ import { Templates } from '../index';
12
+
13
+ jest.mock('../../CreativesContainer', () => ({
14
+ __esModule: true,
15
+ default: (props) => (
16
+ <div className="creatives-container-mock" {...props}>
17
+ CreativesContainer
18
+ </div>
19
+ ),
20
+ }));
21
+
22
+ // Helper: build a valid template for WEBPUSH
23
+ // extractTemplateContentForPreview reads:
24
+ // - template.versions.base.content.webpush → webpushContent
25
+ // - template.definition.accountId → accountId
26
+ // - template.name → templateName
27
+ const makeTemplate = (webpush = {}, extra = {}) => ({
28
+ name: 'My Template',
29
+ definition: { accountId: 'acc-123' },
30
+ versions: {
31
+ base: {
32
+ content: { webpush },
33
+ },
34
+ },
35
+ ...extra,
36
+ });
37
+
38
+ describe('Templates - WEBPUSH channel', () => {
39
+ const mockActions = {
40
+ getWeCrmAccounts: jest.fn(),
41
+ setChannelAccount: jest.fn(),
42
+ getAllTemplates: jest.fn(),
43
+ getUserList: jest.fn(),
44
+ getSenderDetails: jest.fn(),
45
+ resetTemplate: jest.fn(),
46
+ setArchivedMode: jest.fn(),
47
+ clearTemplateSelection: jest.fn(),
48
+ toggleTemplateSelection: jest.fn(),
49
+ };
50
+
51
+ const baseProps = {
52
+ route: { name: 'webpush' },
53
+ Templates: { templates: [] },
54
+ actions: mockActions,
55
+ location: { pathname: '/webpush', query: {}, search: '' },
56
+ EmailCreate: { duplicateTemplateInProgress: false },
57
+ isFullMode: false,
58
+ intl: { formatMessage: jest.fn((msg) => msg.defaultMessage || '') },
59
+ };
60
+
61
+ beforeEach(() => {
62
+ jest.clearAllMocks();
63
+ });
64
+
65
+ const renderComponent = (channel = 'webpush') => {
66
+ const props = { ...baseProps, route: { name: channel } };
67
+ return shallowWithIntl(<Templates {...props} />);
68
+ };
69
+
70
+ // ─────────────────────────────────────────────────────────────────────────
71
+ describe('isTestAndPreviewSupported', () => {
72
+ it('should return true for WEBPUSH channel', () => {
73
+ const component = renderComponent('webpush');
74
+ component.setState({ channel: 'webpush' });
75
+ expect(component.instance().isTestAndPreviewSupported()).toBe(true);
76
+ });
77
+
78
+ it('should return true for WEBPUSH in uppercase state', () => {
79
+ const component = renderComponent('webpush');
80
+ component.setState({ channel: 'WEBPUSH' });
81
+ expect(component.instance().isTestAndPreviewSupported()).toBe(true);
82
+ });
83
+
84
+ it('should return true for EMAIL channel', () => {
85
+ const component = renderComponent('webpush');
86
+ component.setState({ channel: 'email' });
87
+ expect(component.instance().isTestAndPreviewSupported()).toBe(true);
88
+ });
89
+
90
+ it('should return true for SMS channel', () => {
91
+ const component = renderComponent('webpush');
92
+ component.setState({ channel: 'sms' });
93
+ expect(component.instance().isTestAndPreviewSupported()).toBe(true);
94
+ });
95
+
96
+ it('should return true for INAPP channel', () => {
97
+ const component = renderComponent('webpush');
98
+ component.setState({ channel: 'inapp' });
99
+ expect(component.instance().isTestAndPreviewSupported()).toBe(true);
100
+ });
101
+
102
+ it('should return true for VIBER channel', () => {
103
+ const component = renderComponent('webpush');
104
+ component.setState({ channel: 'viber' });
105
+ expect(component.instance().isTestAndPreviewSupported()).toBe(true);
106
+ });
107
+
108
+ it('should return true for ZALO channel', () => {
109
+ const component = renderComponent('webpush');
110
+ component.setState({ channel: 'zalo' });
111
+ expect(component.instance().isTestAndPreviewSupported()).toBe(true);
112
+ });
113
+
114
+ it('should return false for WHATSAPP channel', () => {
115
+ const component = renderComponent('webpush');
116
+ component.setState({ channel: 'whatsapp' });
117
+ expect(component.instance().isTestAndPreviewSupported()).toBe(false);
118
+ });
119
+
120
+ it('should return false for RCS channel', () => {
121
+ const component = renderComponent('webpush');
122
+ component.setState({ channel: 'rcs' });
123
+ expect(component.instance().isTestAndPreviewSupported()).toBe(false);
124
+ });
125
+ });
126
+
127
+ // ─────────────────────────────────────────────────────────────────────────
128
+ describe('extractTemplateContentForPreview - WEBPUSH channel', () => {
129
+ it('should return null when template has no versions.base', () => {
130
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
131
+ const component = renderComponent('webpush');
132
+ const result = component.instance().extractTemplateContentForPreview(
133
+ { name: 'T', definition: {} },
134
+ 'WEBPUSH'
135
+ );
136
+ expect(result).toBeNull();
137
+ consoleSpy.mockRestore();
138
+ });
139
+
140
+ it('should extract title and message from versions.base.content.webpush', () => {
141
+ const component = renderComponent('webpush');
142
+ const template = makeTemplate({ title: 'Push Title', message: 'Push body' });
143
+
144
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
145
+ expect(result).toBeDefined();
146
+ expect(result.channel).toBe('WEBPUSH');
147
+ expect(result.accountId).toBe('acc-123');
148
+ expect(result.content.title).toBe('Push Title');
149
+ expect(result.content.message).toBe('Push body');
150
+ expect(result.messageSubject).toBe('My Template');
151
+ expect(result.offers).toEqual([]);
152
+ });
153
+
154
+ it('should fall back to title for messageSubject when template name is empty', () => {
155
+ const component = renderComponent('webpush');
156
+ const template = makeTemplate({ title: 'Title Only', message: 'M' }, { name: '' });
157
+
158
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
159
+ expect(result.messageSubject).toBe('Title Only');
160
+ });
161
+
162
+ it('should extract brandIcon as iconImageUrl', () => {
163
+ const component = renderComponent('webpush');
164
+ const template = makeTemplate({
165
+ title: 'T',
166
+ message: 'M',
167
+ brandIcon: 'https://example.com/brand.png',
168
+ });
169
+
170
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
171
+ expect(result.content.iconImageUrl).toBe('https://example.com/brand.png');
172
+ });
173
+
174
+ it('should use iconImageUrl field when brandIcon is absent', () => {
175
+ const component = renderComponent('webpush');
176
+ const template = makeTemplate({
177
+ title: 'T',
178
+ message: 'M',
179
+ iconImageUrl: 'https://example.com/icon.png',
180
+ });
181
+
182
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
183
+ expect(result.content.iconImageUrl).toBe('https://example.com/icon.png');
184
+ });
185
+
186
+ it('should NOT include iconImageUrl when neither brandIcon nor iconImageUrl present', () => {
187
+ const component = renderComponent('webpush');
188
+ const template = makeTemplate({ title: 'T', message: 'M' });
189
+
190
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
191
+ expect(result.content.iconImageUrl).toBeUndefined();
192
+ });
193
+
194
+ it('should convert onClickAction type URL to EXTERNAL_URL cta', () => {
195
+ const component = renderComponent('webpush');
196
+ const template = makeTemplate({
197
+ title: 'T',
198
+ message: 'M',
199
+ onClickAction: { type: 'URL', url: 'https://example.com' },
200
+ });
201
+
202
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
203
+ expect(result.content.cta).toEqual({ type: 'EXTERNAL_URL', actionLink: 'https://example.com' });
204
+ });
205
+
206
+ it('should preserve onClickAction type SITE_URL as-is', () => {
207
+ const component = renderComponent('webpush');
208
+ const template = makeTemplate({
209
+ title: 'T',
210
+ message: 'M',
211
+ onClickAction: { type: 'SITE_URL', url: 'https://site.com' },
212
+ });
213
+
214
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
215
+ expect(result.content.cta).toEqual({ type: 'SITE_URL', actionLink: 'https://site.com' });
216
+ });
217
+
218
+ it('should use existingCta when onClickAction is absent', () => {
219
+ const component = renderComponent('webpush');
220
+ const template = makeTemplate({
221
+ title: 'T',
222
+ message: 'M',
223
+ cta: { type: 'EXTERNAL_URL', actionLink: 'https://existing.com' },
224
+ });
225
+
226
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
227
+ expect(result.content.cta).toEqual({ type: 'EXTERNAL_URL', actionLink: 'https://existing.com' });
228
+ });
229
+
230
+ it('should default existingCta type to EXTERNAL_URL when type is missing', () => {
231
+ const component = renderComponent('webpush');
232
+ const template = makeTemplate({
233
+ title: 'T',
234
+ message: 'M',
235
+ cta: { actionLink: 'https://example.com' },
236
+ });
237
+
238
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
239
+ expect(result.content.cta.type).toBe('EXTERNAL_URL');
240
+ });
241
+
242
+ it('should NOT include cta when neither onClickAction nor cta present', () => {
243
+ const component = renderComponent('webpush');
244
+ const template = makeTemplate({ title: 'T', message: 'M' });
245
+
246
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
247
+ expect(result.content.cta).toBeUndefined();
248
+ });
249
+
250
+ it('should build expandableDetails from image and ctas', () => {
251
+ const component = renderComponent('webpush');
252
+ const template = makeTemplate({
253
+ title: 'T',
254
+ message: 'M',
255
+ image: 'https://example.com/image.jpg',
256
+ ctas: [{ type: 'URL', actionText: 'Click', actionLink: 'https://a.com', action: '' }],
257
+ });
258
+
259
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
260
+ expect(result.content.expandableDetails).toBeDefined();
261
+ expect(result.content.expandableDetails.media).toEqual([
262
+ { url: 'https://example.com/image.jpg', type: 'IMAGE' },
263
+ ]);
264
+ expect(result.content.expandableDetails.ctas[0].title).toBe('Click');
265
+ expect(result.content.expandableDetails.ctas[0].type).toBe('EXTERNAL_URL');
266
+ });
267
+
268
+ it('should convert CTA type URL → EXTERNAL_URL in expandableDetails.ctas', () => {
269
+ const component = renderComponent('webpush');
270
+ const template = makeTemplate({
271
+ title: 'T',
272
+ message: 'M',
273
+ ctas: [{ type: 'URL', actionText: 'Go', actionLink: 'https://x.com' }],
274
+ });
275
+
276
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
277
+ expect(result.content.expandableDetails.ctas[0].type).toBe('EXTERNAL_URL');
278
+ });
279
+
280
+ it('should keep EXTERNAL_URL type unchanged in expandableDetails.ctas', () => {
281
+ const component = renderComponent('webpush');
282
+ const template = makeTemplate({
283
+ title: 'T',
284
+ message: 'M',
285
+ ctas: [{ type: 'EXTERNAL_URL', title: 'Go', actionLink: 'https://x.com' }],
286
+ });
287
+
288
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
289
+ expect(result.content.expandableDetails.ctas[0].type).toBe('EXTERNAL_URL');
290
+ });
291
+
292
+ it('should build image-only expandableDetails when no ctas', () => {
293
+ const component = renderComponent('webpush');
294
+ const template = makeTemplate({
295
+ title: 'T',
296
+ message: 'M',
297
+ image: 'https://example.com/img.jpg',
298
+ });
299
+
300
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
301
+ expect(result.content.expandableDetails.media).toHaveLength(1);
302
+ expect(result.content.expandableDetails.ctas).toEqual([]);
303
+ });
304
+
305
+ it('should build ctas-only expandableDetails when no image', () => {
306
+ const component = renderComponent('webpush');
307
+ const template = makeTemplate({
308
+ title: 'T',
309
+ message: 'M',
310
+ ctas: [{ type: 'EXTERNAL_URL', actionText: 'Btn', actionLink: 'https://b.com' }],
311
+ });
312
+
313
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
314
+ expect(result.content.expandableDetails.media).toEqual([]);
315
+ expect(result.content.expandableDetails.ctas).toHaveLength(1);
316
+ });
317
+
318
+ it('should use existingExpandable when no image and no ctas', () => {
319
+ const component = renderComponent('webpush');
320
+ const existingExpandable = {
321
+ media: [{ url: 'https://example.com/existing.jpg', type: 'IMAGE' }],
322
+ ctas: [{ title: 'Existing', actionLink: 'https://existing.com', type: 'EXTERNAL_URL' }],
323
+ };
324
+ const template = makeTemplate({
325
+ title: 'T',
326
+ message: 'M',
327
+ expandableDetails: existingExpandable,
328
+ });
329
+
330
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
331
+ expect(result.content.expandableDetails).toEqual(existingExpandable);
332
+ });
333
+
334
+ it('should NOT include expandableDetails when none present', () => {
335
+ const component = renderComponent('webpush');
336
+ const template = makeTemplate({ title: 'T', message: 'M' });
337
+
338
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
339
+ expect(result.content.expandableDetails).toBeUndefined();
340
+ });
341
+
342
+ it('should handle empty webpush content gracefully', () => {
343
+ const component = renderComponent('webpush');
344
+ const template = makeTemplate({}, { name: '' });
345
+
346
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
347
+ expect(result).toBeDefined();
348
+ expect(result.content.title).toBe('');
349
+ expect(result.content.message).toBe('');
350
+ });
351
+
352
+ it('should handle missing definition.accountId gracefully (returns null)', () => {
353
+ const component = renderComponent('webpush');
354
+ const template = {
355
+ name: 'T',
356
+ versions: { base: { content: { webpush: { title: 'T', message: 'M' } } } },
357
+ // no definition
358
+ };
359
+
360
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
361
+ expect(result).toBeDefined();
362
+ expect(result.accountId).toBeNull();
363
+ });
364
+
365
+ it('should return null for unsupported channel', () => {
366
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
367
+ const component = renderComponent('webpush');
368
+ const template = makeTemplate({ title: 'T', message: 'M' });
369
+
370
+ const result = component.instance().extractTemplateContentForPreview(template, 'UNSUPPORTED_CHANNEL');
371
+ expect(result).toBeNull();
372
+ consoleSpy.mockRestore();
373
+ });
374
+ });
375
+ });
@@ -47,7 +47,6 @@ import {
47
47
  NONE,
48
48
  mediaRadioOptions,
49
49
  buttonRadioOptions,
50
- AI_CONTENT_BOT_DISABLED,
51
50
  VIBER_CAROUSEL_MAX_BUTTONS,
52
51
  VIBER_CAROUSEL_MAX_CARDS,
53
52
  VIBER_CAROUSEL_MIN_CARDS,
@@ -719,20 +718,23 @@ export const Viber = (props) => {
719
718
  };
720
719
 
721
720
  const removeCarouselButton = (cardIndex, buttonIndex) => {
722
- if (buttonIndex === 0) {
723
- return;
724
- }
725
721
  setCarouselCards((prevCards) => prevCards.map((card, index) => {
726
722
  if (index !== cardIndex) {
727
723
  return card;
728
724
  }
725
+ const buttons = card?.buttons || [];
726
+ if (buttons.length <= 1) {
727
+ return card;
728
+ }
729
729
  return {
730
730
  ...card,
731
- buttons: (card?.buttons || []).filter((_, idx) => idx !== buttonIndex),
731
+ buttons: buttons.filter((_, idx) => idx !== buttonIndex),
732
732
  };
733
733
  }));
734
734
  };
735
735
 
736
+ const canRemoveCarouselButton = (card) => (card?.buttons || []).length > 1;
737
+
736
738
  const saveCarouselButton = (cardIndex, buttonIndex) => {
737
739
  setCarouselCards((prevCards) => prevCards.map((card, index) => {
738
740
  if (index !== cardIndex) {
@@ -876,18 +878,19 @@ export const Viber = (props) => {
876
878
  <CapLabel type="label4" className="viber-carousel-saved-button-text">
877
879
  {button?.title}
878
880
  </CapLabel>
879
- <CapColumn className="button-edit-icon" onClick={() => editCarouselButton(cardIndex, buttonIndex)}>
880
- <CapIcon type="edit" size="s" />
881
- </CapColumn>
882
- {buttonIndex > 0 && (
883
- <CapButton
884
- type="flat"
885
- className="viber-carousel-delete-icon-btn"
886
- onClick={() => removeCarouselButton(cardIndex, buttonIndex)}
887
- >
888
- <CapIcon type="delete" size="s" />
889
- </CapButton>
890
- )}
881
+ <CapRow className="viber-carousel-saved-button-actions" align="middle" type="flex">
882
+ <CapColumn className="button-edit-icon" onClick={() => editCarouselButton(cardIndex, buttonIndex)}>
883
+ <CapIcon type="edit" size="s" />
884
+ </CapColumn>
885
+ {canRemoveCarouselButton(card) && (
886
+ <CapColumn
887
+ className="button-edit-icon viber-carousel-delete-icon"
888
+ onClick={() => removeCarouselButton(cardIndex, buttonIndex)}
889
+ >
890
+ <CapIcon type="delete" size="s" />
891
+ </CapColumn>
892
+ )}
893
+ </CapRow>
891
894
  </CapRow>
892
895
  ) : (
893
896
  <div className="cta-section">
@@ -949,7 +952,7 @@ export const Viber = (props) => {
949
952
  >
950
953
  {formatMessage(messages.save)}
951
954
  </CapButton>
952
- {buttonIndex > 0 && (
955
+ {canRemoveCarouselButton(card) && (
953
956
  <CapButton
954
957
  type="secondary"
955
958
  onClick={() => removeCarouselButton(cardIndex, buttonIndex)}
@@ -1466,6 +1469,9 @@ export const Viber = (props) => {
1466
1469
  if ((isMediaTypeImage || isMediaTypeVideo) && viber?.assetUploading) {
1467
1470
  return true;
1468
1471
  }
1472
+ if (isMediaTypeCarousel && (isCarouselCardCountInvalid || hasInvalidCarouselCard || hasInvalidCarouselButton)) {
1473
+ return true;
1474
+ }
1469
1475
  if (isBtnTypeCta && !isCtaSaved) {
1470
1476
  return true;
1471
1477
  }
@@ -239,16 +239,43 @@
239
239
  border-radius: $CAP_SPACE_04;
240
240
  padding: $CAP_SPACE_12 $CAP_SPACE_16;
241
241
  margin-top: $CAP_SPACE_12;
242
+ min-height: 2.75rem;
243
+ box-sizing: border-box;
242
244
  }
243
245
 
244
246
  .viber-carousel-saved-button-icon {
245
247
  margin-right: $CAP_SPACE_12;
246
248
  color: $FONT_COLOR_01;
249
+ flex-shrink: 0;
250
+ line-height: 1;
247
251
  }
248
252
 
249
253
  .viber-carousel-saved-button-text {
250
254
  flex: 1;
251
255
  margin-right: $CAP_SPACE_12;
256
+ min-width: 0;
257
+ line-height: 1.25rem;
258
+ }
259
+
260
+ .viber-carousel-saved-button-actions {
261
+ display: flex;
262
+ align-items: center;
263
+ gap: $CAP_SPACE_24;
264
+ margin-left: auto;
265
+ flex-shrink: 0;
266
+ min-height: 1.5rem;
267
+
268
+ .button-edit-icon {
269
+ margin-left: 0;
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: center;
273
+ line-height: 1;
274
+ }
275
+ }
276
+
277
+ .viber-carousel-delete-icon {
278
+ margin-left: 0;
252
279
  }
253
280
 
254
281
  .viber-carousel-delete-icon-btn {
@@ -214,6 +214,10 @@ export default defineMessages({
214
214
  id: `${scope}.cancel`,
215
215
  defaultMessage: 'Cancel',
216
216
  },
217
+ assetIdMissingError: {
218
+ id: `${scope}.assetIdMissingError`,
219
+ defaultMessage: 'Asset upload initiated but no asset ID was returned from the server. Unable to track processing status.',
220
+ },
217
221
  carouselCardsLabel: {
218
222
  id: `${scope}.carouselCardsLabel`,
219
223
  defaultMessage: 'Carousel cards',
@@ -326,8 +330,4 @@ export default defineMessages({
326
330
  id: `${scope}.carouselCardTitleMaxLengthError`,
327
331
  defaultMessage: 'Title can not be more than 38 characters',
328
332
  },
329
- assetIdMissingError: {
330
- id: `${scope}.assetIdMissingError`,
331
- defaultMessage: 'Asset upload initiated but no asset ID was returned from the server. Unable to track processing status.',
332
- },
333
333
  });
@@ -45,6 +45,8 @@ import {
45
45
  ACTION_TYPES,
46
46
  NOTIFICATION_TITLE_MAX_LENGTH,
47
47
  MESSAGE_MAX_LENGTH,
48
+ EXTERNAL_URL,
49
+ SITE_URL,
48
50
  } from '../constants';
49
51
  import * as actions from '../actions';
50
52
  import {
@@ -73,6 +75,9 @@ import {
73
75
  import './index.scss';
74
76
  import { WEBPUSH } from '../../CreativesContainer/constants';
75
77
  import { isAiContentBotDisabled } from '../../../utils/common';
78
+ import TestAndPreviewSlidebox from '../../../v2Components/TestAndPreviewSlidebox';
79
+ import creativesMessages from '../../CreativesContainer/messages';
80
+ import CapButton from '@capillarytech/cap-ui-library/CapButton';
76
81
 
77
82
  // Memoized TagList wrapper components for better performance
78
83
  const MemoizedTagList = memo(({
@@ -182,6 +187,7 @@ const WebPushCreate = ({
182
187
  const [redirectUrlError, setRedirectUrlError] = useState('');
183
188
  const [activeUploadField, setActiveUploadField] = useState(null);
184
189
  const [templateIdError, setTemplateIdError] = useState('');
190
+ const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
185
191
 
186
192
  // Refs
187
193
  const titleCountRef = useRef(null);
@@ -569,6 +575,67 @@ const WebPushCreate = ({
569
575
  return !(templateNameInvalid || titleValidation || messageValidation);
570
576
  };
571
577
 
578
+ const getTemplateContent = useCallback(() => {
579
+ let cta = null;
580
+ if (onClickBehaviour === ON_CLICK_BEHAVIOUR_OPTIONS.REDIRECT_TO_URL && redirectUrl) {
581
+ cta = { type: EXTERNAL_URL, actionLink: redirectUrl?.trim() };
582
+ } else if (onClickBehaviour === ON_CLICK_BEHAVIOUR_OPTIONS.SITE_URL && websiteLink) {
583
+ cta = { type: SITE_URL, actionLink: websiteLink };
584
+ }
585
+
586
+ const hasImage = mediaType === WEBPUSH_MEDIA_TYPES.IMAGE && imageSrc;
587
+ const hasCtas = buttons?.length > 0;
588
+ let expandableDetails = null;
589
+ if (hasImage || hasCtas) {
590
+ expandableDetails = {
591
+ media: hasImage ? [{ url: imageSrc, type: WEBPUSH_MEDIA_TYPES.IMAGE }] : [],
592
+ ctas: hasCtas ? buttons.map((btn) => ({
593
+ type: EXTERNAL_URL,
594
+ action: '',
595
+ title: btn.text || '',
596
+ actionLink: btn.url || '',
597
+ })) : [],
598
+ };
599
+ }
600
+
601
+ const iconImageUrl = brandIconOption !== BRAND_ICON_OPTIONS.DONT_SHOW && brandIconSrc ? brandIconSrc : undefined;
602
+
603
+ return {
604
+ channel: WEBPUSH,
605
+ accountId,
606
+ content: {
607
+ title: notificationTitle || '',
608
+ message: message || '',
609
+ ...(iconImageUrl ? { iconImageUrl } : {}),
610
+ ...(cta ? { cta } : {}),
611
+ ...(expandableDetails ? { expandableDetails } : {}),
612
+ },
613
+ messageSubject: templateName || notificationTitle || '',
614
+ offers: [],
615
+ };
616
+ }, [
617
+ notificationTitle,
618
+ message,
619
+ accountId,
620
+ mediaType,
621
+ imageSrc,
622
+ brandIconOption,
623
+ brandIconSrc,
624
+ buttons,
625
+ onClickBehaviour,
626
+ redirectUrl,
627
+ websiteLink,
628
+ templateName,
629
+ ]);
630
+
631
+ const handleTestAndPreview = useCallback(() => {
632
+ setShowTestAndPreviewSlidebox(true);
633
+ }, []);
634
+
635
+ const handleCloseTestAndPreview = useCallback(() => {
636
+ setShowTestAndPreviewSlidebox(false);
637
+ }, []);
638
+
572
639
  const isFormValid = () => {
573
640
  const templateNameInvalid = isFullMode && validateTemplateName(templateName);
574
641
  const titleValidation = validateTitle(notificationTitle);
@@ -1040,14 +1107,23 @@ const WebPushCreate = ({
1040
1107
  formatMessage={formatMessage}
1041
1108
  messages={messages}
1042
1109
  />
1043
- <FormActions
1044
- onSave={handleSave}
1045
- isSaveDisabled={isSaveDisabled}
1046
- errorText={errorText}
1047
- accountErrorText={accountErrorText}
1048
- formatMessage={formatMessage}
1049
- messages={messages}
1050
- />
1110
+ <CapRow className="webpush-test-preview-action">
1111
+ <FormActions
1112
+ onSave={handleSave}
1113
+ isSaveDisabled={isSaveDisabled}
1114
+ errorText={errorText}
1115
+ accountErrorText={accountErrorText}
1116
+ formatMessage={formatMessage}
1117
+ messages={messages}
1118
+ />
1119
+ <CapButton
1120
+ type="secondary"
1121
+ onClick={handleTestAndPreview}
1122
+ className="webpush-test-preview-btn"
1123
+ >
1124
+ {formatMessage(creativesMessages.testAndPreview)}
1125
+ </CapButton>
1126
+ </CapRow>
1051
1127
  </CapColumn>
1052
1128
  <CapColumn className="preview-section" span={10}>
1053
1129
  <WebPushPreview
@@ -1059,6 +1135,13 @@ const WebPushCreate = ({
1059
1135
  buttons={buttons}
1060
1136
  />
1061
1137
  </CapColumn>
1138
+ <TestAndPreviewSlidebox
1139
+ show={showTestAndPreviewSlidebox}
1140
+ onClose={handleCloseTestAndPreview}
1141
+ content={getTemplateContent()}
1142
+ currentChannel={WEBPUSH}
1143
+ accountId={accountId || null}
1144
+ />
1062
1145
  </CapRow>
1063
1146
  );
1064
1147
  };
@@ -135,5 +135,12 @@
135
135
  font-family: 'Roboto', open-sans, sans-serif;
136
136
  }
137
137
  }
138
+
139
+ .webpush-test-preview-action {
140
+ display: flex;
141
+ .webpush-test-preview-btn {
142
+ margin-left: $CAP_SPACE_12;
143
+ }
144
+ }
138
145
  }
139
146