@capillarytech/creatives-library 8.0.298 → 8.0.299-alpha.4

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 (28) hide show
  1. package/package.json +1 -1
  2. package/services/api.js +17 -0
  3. package/services/tests/api.test.js +85 -0
  4. package/utils/commonUtils.js +10 -0
  5. package/utils/tests/commonUtil.test.js +169 -0
  6. package/v2Components/CommonTestAndPreview/AddTestCustomer.js +42 -0
  7. package/v2Components/CommonTestAndPreview/CustomerCreationModal.js +284 -0
  8. package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +72 -0
  9. package/v2Components/CommonTestAndPreview/SendTestMessage.js +78 -49
  10. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +200 -4
  11. package/v2Components/CommonTestAndPreview/actions.js +10 -0
  12. package/v2Components/CommonTestAndPreview/constants.js +18 -1
  13. package/v2Components/CommonTestAndPreview/index.js +274 -14
  14. package/v2Components/CommonTestAndPreview/messages.js +94 -0
  15. package/v2Components/CommonTestAndPreview/reducer.js +10 -0
  16. package/v2Components/CommonTestAndPreview/tests/AddTestCustomer.test.js +66 -0
  17. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +653 -0
  18. package/v2Components/CommonTestAndPreview/tests/CustomerCreationModal.test.js +316 -0
  19. package/v2Components/CommonTestAndPreview/tests/ExistingCustomerModal.test.js +114 -0
  20. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +53 -0
  21. package/v2Components/CommonTestAndPreview/tests/constants.test.js +25 -2
  22. package/v2Components/CommonTestAndPreview/tests/index.test.js +7 -0
  23. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +71 -0
  24. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +17 -0
  25. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +1588 -1336
  26. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +369 -306
  27. package/v2Containers/TemplatesV2/TemplatesV2.style.js +9 -3
  28. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +5794 -5080
@@ -17,6 +17,9 @@ import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
17
17
  import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
18
18
  import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
19
19
  import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
20
+ import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
21
+ import CustomerCreationModal from './CustomerCreationModal';
22
+ import { createTestCustomer } from '../../services/api';
20
23
 
21
24
  // Import messages and styles
22
25
  import messages from './messages';
@@ -27,10 +30,16 @@ import LeftPanelContent from './LeftPanelContent';
27
30
  import CustomValuesEditor from './CustomValuesEditor';
28
31
  import SendTestMessage from './SendTestMessage';
29
32
  import PreviewSection from './PreviewSection';
30
-
33
+ import AddTestCustomerButton from './AddTestCustomer';
34
+ import ExistingCustomerModal from './ExistingCustomerModal';
31
35
  // Import constants
32
36
  import {
33
37
  CHANNELS,
38
+ CUSTOMER_MODAL_NEW,
39
+ CUSTOMER_MODAL_EXISTING,
40
+ IDENTIFIER_TYPE_EMAIL,
41
+ IDENTIFIER_TYPE_MOBILE,
42
+ IDENTIFIER_TYPE_PHONE,
34
43
  TEST,
35
44
  DESKTOP,
36
45
  ANDROID,
@@ -74,6 +83,8 @@ import {
74
83
 
75
84
  // Import utilities
76
85
  import { getCdnUrl } from '../../utils/cdnTransformation';
86
+ import { isValidEmail, isValidMobile } from '../../utils/commonUtils';
87
+ import { getMembersLookup } from '../../services/api';
77
88
 
78
89
  /**
79
90
  * Preview Component Factory - REMOVED IN PHASE 5
@@ -130,6 +141,11 @@ const CommonTestAndPreview = (props) => {
130
141
  const [customValues, setCustomValues] = useState({});
131
142
  const [showJSON, setShowJSON] = useState(false);
132
143
  const [tagsExtracted, setTagsExtracted] = useState(false);
144
+ const [searchValue, setSearchValue] = useState("");
145
+ const [customerModal, setCustomerModal] = useState([false, ""]);
146
+ const [isCustomerDataLoading, setIsCustomerDataLoading] = useState(false);
147
+ const [customerData, setCustomerData] = useState({ name: '', email: '', mobile: '', customerId: '' });
148
+
133
149
  // Initialize device based on channel: SMS uses Android/iOS, others use Desktop/Mobile
134
150
  // Initialize device based on channel: SMS, WhatsApp, RCS, InApp, MobilePush, and Viber use Android/iOS, others use Desktop/Mobile
135
151
  const initialDevice = (channel === CHANNELS.SMS || channel === CHANNELS.WHATSAPP || channel === CHANNELS.RCS || channel === CHANNELS.INAPP || channel === CHANNELS.MOBILEPUSH || channel === CHANNELS.VIBER) ? ANDROID : DESKTOP;
@@ -155,6 +171,9 @@ const CommonTestAndPreview = (props) => {
155
171
  const [selectedTestEntities, setSelectedTestEntities] = useState([]);
156
172
  const [beeContent, setBeeContent] = useState(''); // Track BEE editor content separately (EMAIL only)
157
173
  const previousBeeContentRef = useRef(''); // Track previous BEE content (EMAIL only)
174
+ // Container for notifications so they render inside the slidebox (visible in campaigns/library mode)
175
+ const notificationContainerRef = useRef(null);
176
+ const getNotificationContainer = () => notificationContainerRef.current || document.body;
158
177
  // Delivery settings for Test and Preview (SMS, Email, WhatsApp) — user selection only
159
178
  const [testPreviewDeliverySettings, setTestPreviewDeliverySettings] = useState({
160
179
  [CHANNELS.SMS]: {
@@ -348,7 +367,7 @@ const CommonTestAndPreview = (props) => {
348
367
  return content || '';
349
368
  }, [channel, formData, currentTab, beeContent, content, beeInstance]);
350
369
 
351
- // Build test entities tree data
370
+ // Build test entities tree data from testCustomers prop (includes customers added via addTestCustomer action)
352
371
  const testEntitiesTreeData = useMemo(() => {
353
372
  const groupsNode = {
354
373
  title: 'Groups',
@@ -361,7 +380,10 @@ const CommonTestAndPreview = (props) => {
361
380
  title: 'Individuals',
362
381
  value: 'customers-node',
363
382
  selectable: false,
364
- children: testCustomers?.map((customer) => ({ title: customer.name, value: customer?.userId })),
383
+ children: testCustomers?.map((customer) => ({
384
+ title: customer?.name?.trim() || customer?.email?.trim() || customer?.mobile?.trim() || customer?.userId || customer?.customerId,
385
+ value: customer?.userId ?? customer?.customerId,
386
+ })) || [],
365
387
  };
366
388
 
367
389
  return [groupsNode, customersNode];
@@ -388,6 +410,94 @@ const CommonTestAndPreview = (props) => {
388
410
  return resolvedText;
389
411
  };
390
412
 
413
+ /**
414
+ * Common handler for saving test customers (both new and existing)
415
+ */
416
+ const handleSaveTestCustomer = async (validationErrors = {},setIsLoading = false) => {
417
+ // Check for validation errors before saving (for new customers)
418
+ if (customerModal[1] === CUSTOMER_MODAL_NEW && (validationErrors.email || validationErrors.mobile)) {
419
+ return;
420
+ }
421
+
422
+ setIsLoading(true);
423
+
424
+ try {
425
+ let payload;
426
+
427
+ if (customerModal[1] === CUSTOMER_MODAL_EXISTING) {
428
+ // For existing customers, use customerId
429
+ payload = {
430
+ customerId: customerData.customerId
431
+ };
432
+ } else {
433
+ // For new customers, use customer object
434
+ payload = {
435
+ customer: {
436
+ firstName: customerData.name || "",
437
+ mobile: customerData.mobile || "",
438
+ email: customerData.email || ""
439
+ }
440
+ };
441
+ }
442
+
443
+ const response = await createTestCustomer(payload);
444
+
445
+
446
+ // Handle success: add to test customers list and selection (existing and new)
447
+ if (response && response.success) {
448
+ CapNotification.success({
449
+ message: formatMessage(messages.newTestCustomerAddedSuccess),
450
+ getContainer: getNotificationContainer,
451
+ });
452
+ // API may return customerId in response.response (e.g. { response: { customerId: 438845651 } })
453
+ const res = response?.response || response;
454
+ const addedId = customerModal[1] === CUSTOMER_MODAL_EXISTING
455
+ ? customerData?.customerId
456
+ : res?.customerId;
457
+ if (addedId) {
458
+ actions.addTestCustomer({
459
+ userId: addedId,
460
+ customerId: addedId,
461
+ name: customerData?.name?.trim() || '',
462
+ email: customerData?.email || '',
463
+ mobile: customerData?.mobile || '',
464
+ });
465
+ setSelectedTestEntities((prev) => [...prev, addedId]);
466
+ }
467
+ handleCloseCustomerModal();
468
+ } else {
469
+ // Show error notification for unsuccessful response
470
+ CapNotification.error({
471
+ message: formatMessage(messages.errorTitle),
472
+ description: response?.message || formatMessage(messages.failedToAddTestCustomer),
473
+ getContainer: getNotificationContainer,
474
+ });
475
+ }
476
+ } catch (error) {
477
+ if (customerModal[1] === CUSTOMER_MODAL_EXISTING) {
478
+ // Show error notification for caught exceptions (existing customers only)
479
+ CapNotification.error({
480
+ message: formatMessage(messages.errorTitle),
481
+ description: error?.message || formatMessage(messages.errorAddingTestCustomer),
482
+ getContainer: getNotificationContainer,
483
+ });
484
+ }
485
+ } finally {
486
+ setIsLoading(false);
487
+ }
488
+ };
489
+
490
+ const handleCloseCustomerModal = () => {
491
+ setCustomerModal([false, ""]);
492
+ setSearchValue('');
493
+ setCustomerData({
494
+ name: '',
495
+ email: '',
496
+ mobile: '',
497
+ customerId: '',
498
+ });
499
+ };
500
+
391
501
  /**
392
502
  * Prepare payload for preview API based on channel
393
503
  */
@@ -2418,6 +2528,7 @@ const CommonTestAndPreview = (props) => {
2418
2528
  } catch (error) {
2419
2529
  CapNotification.error({
2420
2530
  message: formatMessage(messages.invalidJSON),
2531
+ getContainer: getNotificationContainer,
2421
2532
  });
2422
2533
  }
2423
2534
  };
@@ -2464,6 +2575,7 @@ const CommonTestAndPreview = (props) => {
2464
2575
  } catch (error) {
2465
2576
  CapNotification.error({
2466
2577
  message: formatMessage(messages.previewUpdateError),
2578
+ getContainer: getNotificationContainer,
2467
2579
  });
2468
2580
  }
2469
2581
  };
@@ -2472,7 +2584,6 @@ const CommonTestAndPreview = (props) => {
2472
2584
  * Handle extract tags
2473
2585
  */
2474
2586
  const handleExtractTags = () => {
2475
- // Get content based on channel
2476
2587
  let contentToExtract = getCurrentContent;
2477
2588
 
2478
2589
  if (channel === CHANNELS.EMAIL && formData) {
@@ -2512,6 +2623,110 @@ const CommonTestAndPreview = (props) => {
2512
2623
  setSelectedTestEntities(value);
2513
2624
  };
2514
2625
 
2626
+ /**
2627
+ * Map API customerDetails item to our customerData shape
2628
+ */
2629
+ const mapCustomerDetailsToCustomerData = (detail, identifierValue) => {
2630
+ const firstName = detail.firstName || '';
2631
+ const lastName = detail.lastName || '';
2632
+ const name = [firstName, lastName].filter(Boolean).join(' ').trim() || '';
2633
+ const getIdentifierValue = (type) => {
2634
+ const fromIdentifiers = detail.identifiers?.find((i) => i.type === type)?.value;
2635
+ if (fromIdentifiers) return fromIdentifiers;
2636
+ const fromCommChannels = detail.commChannels?.find((c) => c.type === type)?.value;
2637
+ return fromCommChannels || (channel === CHANNELS.EMAIL && type === IDENTIFIER_TYPE_EMAIL ? identifierValue : channel === CHANNELS.SMS && type === IDENTIFIER_TYPE_MOBILE ? identifierValue : '');
2638
+ };
2639
+ return {
2640
+ name,
2641
+ email: channel === CHANNELS.EMAIL ? (getIdentifierValue(IDENTIFIER_TYPE_EMAIL) || identifierValue) : (getIdentifierValue(IDENTIFIER_TYPE_EMAIL) || ''),
2642
+ mobile: channel === CHANNELS.SMS ? (getIdentifierValue(IDENTIFIER_TYPE_MOBILE) || getIdentifierValue(IDENTIFIER_TYPE_PHONE) || identifierValue) : (getIdentifierValue(IDENTIFIER_TYPE_MOBILE) || getIdentifierValue(IDENTIFIER_TYPE_PHONE) || ''),
2643
+ customerId: detail.userId != null ? String(detail.userId) : '',
2644
+ };
2645
+ };
2646
+
2647
+ const handleAddTestCustomer = async () => {
2648
+ const identifierType = channel === CHANNELS.EMAIL ? IDENTIFIER_TYPE_EMAIL : IDENTIFIER_TYPE_MOBILE;
2649
+ const searchValueToCheck = searchValue || '';
2650
+
2651
+ // Check if this customer is already in the test customers list
2652
+ const existingTestCustomer = testCustomers?.find(customer => {
2653
+ if (channel === CHANNELS.EMAIL) {
2654
+ return customer.email === searchValueToCheck;
2655
+ } else if (channel === CHANNELS.SMS) {
2656
+ return customer.mobile === searchValueToCheck;
2657
+ }
2658
+ return false;
2659
+ });
2660
+
2661
+ if (existingTestCustomer) {
2662
+ const entityId = existingTestCustomer.userId ?? existingTestCustomer.customerId;
2663
+ if (entityId != null) {
2664
+ const id = String(entityId);
2665
+ setSelectedTestEntities((prev) =>
2666
+ prev.includes(id) ? prev : [...prev, id]
2667
+ );
2668
+ }
2669
+ setSearchValue('');
2670
+ CapNotification.success({
2671
+ message: formatMessage(messages.customerAlreadyInTestList),
2672
+ getContainer: getNotificationContainer,
2673
+ });
2674
+ return;
2675
+ }
2676
+
2677
+ setIsCustomerDataLoading(true);
2678
+
2679
+ try {
2680
+ const response = await getMembersLookup(identifierType, searchValueToCheck);
2681
+ const success = response?.success && !response?.status?.isError;
2682
+ const res = response?.response || {};
2683
+ const exists = res.exists || false;
2684
+ const details = res.customerDetails || [];
2685
+
2686
+ if (!success) {
2687
+ const errorMessage = response?.message || response?.status?.message || formatMessage(messages.memberLookupError);
2688
+ CapNotification.error({ title: formatMessage(messages.errorTitle), message: errorMessage, getContainer: getNotificationContainer });
2689
+ return;
2690
+ }
2691
+
2692
+ if (exists && details.length > 0) {
2693
+ const mapped = mapCustomerDetailsToCustomerData(details[0], searchValueToCheck);
2694
+ const customerIdFromLookup = mapped.customerId;
2695
+ const alreadyInTestListByCustomerId = customerIdFromLookup && testCustomers?.some(
2696
+ (c) => String(c?.customerId) === customerIdFromLookup || String(c?.userId) === customerIdFromLookup
2697
+ );
2698
+ if (alreadyInTestListByCustomerId) {
2699
+ setSelectedTestEntities((prev) =>
2700
+ prev.includes(customerIdFromLookup) ? prev : [...prev, customerIdFromLookup]
2701
+ );
2702
+ setSearchValue('');
2703
+ CapNotification.success({
2704
+ message: formatMessage(messages.customerAlreadyInTestList),
2705
+ getContainer: getNotificationContainer,
2706
+ });
2707
+ return;
2708
+ }
2709
+ setCustomerData(mapped);
2710
+ setCustomerModal([true, CUSTOMER_MODAL_EXISTING]);
2711
+ } else {
2712
+ setCustomerData({
2713
+ name: '',
2714
+ email: channel === CHANNELS.EMAIL ? searchValueToCheck : '',
2715
+ mobile: channel === CHANNELS.SMS ? searchValueToCheck : '',
2716
+ customerId: '',
2717
+ });
2718
+ setCustomerModal([true, CUSTOMER_MODAL_NEW]);
2719
+ }
2720
+ } catch {
2721
+ CapNotification.error({
2722
+ message: formatMessage(messages.memberLookupError),
2723
+ getContainer: getNotificationContainer,
2724
+ });
2725
+ } finally {
2726
+ setIsCustomerDataLoading(false);
2727
+ }
2728
+ };
2729
+
2515
2730
  /**
2516
2731
  * Handle send test message
2517
2732
  */
@@ -2558,10 +2773,12 @@ const CommonTestAndPreview = (props) => {
2558
2773
  if (result) {
2559
2774
  CapNotification.success({
2560
2775
  message: formatMessage(messages.testMessageSent),
2776
+ getContainer: getNotificationContainer,
2561
2777
  });
2562
2778
  } else {
2563
2779
  CapNotification.error({
2564
2780
  message: formatMessage(messages.testMessageFailed),
2781
+ getContainer: getNotificationContainer,
2565
2782
  });
2566
2783
  }
2567
2784
  });
@@ -2630,6 +2847,7 @@ const CommonTestAndPreview = (props) => {
2630
2847
  content={getCurrentContent}
2631
2848
  channel={channel}
2632
2849
  isSendingTestMessage={isSendingTestMessage}
2850
+ renderAddTestCustomerButton={renderAddTestCustomerButton}
2633
2851
  formatMessage={formatMessage}
2634
2852
  deliverySettings={testPreviewDeliverySettings[channel]}
2635
2853
  senderDetailsOptions={senderDetailsByChannel[channel]}
@@ -2638,6 +2856,8 @@ const CommonTestAndPreview = (props) => {
2638
2856
  isLoadingSenderDetails={isLoadingSenderDetails}
2639
2857
  smsTraiDltEnabled={smsTraiDltEnabled}
2640
2858
  registeredSenderIds={registeredSenderIds}
2859
+ searchValue={searchValue}
2860
+ setSearchValue={setSearchValue}
2641
2861
  />
2642
2862
  );
2643
2863
 
@@ -2647,6 +2867,20 @@ const CommonTestAndPreview = (props) => {
2647
2867
  />
2648
2868
  );
2649
2869
 
2870
+ const renderAddTestCustomerButton = () => {
2871
+ const value = searchValue || "";
2872
+ const showAddButton =
2873
+ [CHANNELS.EMAIL, CHANNELS.SMS].includes(channel) &&
2874
+ (channel === CHANNELS.EMAIL ? isValidEmail(value) : isValidMobile(value));
2875
+ if (!showAddButton) return null;
2876
+ return (
2877
+ <AddTestCustomerButton
2878
+ searchValue={value}
2879
+ handleAddTestCustomer={handleAddTestCustomer}
2880
+ />
2881
+ );
2882
+ };
2883
+
2650
2884
  // Header content for the slidebox
2651
2885
  const slideboxHeader = (
2652
2886
  <CapRow className="test-preview-header">
@@ -2666,14 +2900,19 @@ const CommonTestAndPreview = (props) => {
2666
2900
  show={show}
2667
2901
  size="size-xl"
2668
2902
  content={(
2669
- <CapRow className="test-preview-container">
2670
- <CapRow className="test-and-preview-panels">
2671
- {/* Left Panel */}
2672
- <CapRow className="left-panel">
2673
- {channel === CHANNELS.ZALO ? null : renderLeftPanelContent()}
2674
- <CapDivider className="panel-divider" />
2675
-
2676
- {/* Send Test Message Section */}
2903
+ <div ref={notificationContainerRef} className="common-test-and-preview-notification-container" style={{ position: 'relative', height: '100%' }}>
2904
+ <CapSpin
2905
+ spinning={isCustomerDataLoading}
2906
+ className={`common-test-preview-lookup-spin ${isCustomerDataLoading ? 'common-test-preview-customer-loading' : ''}`}
2907
+ >
2908
+ <CapRow className="test-preview-container">
2909
+ <CapRow className="test-and-preview-panels">
2910
+ {/* Left Panel */}
2911
+ <CapRow className="left-panel">
2912
+ {channel === CHANNELS.ZALO ? null : renderLeftPanelContent()}
2913
+ <CapDivider className="panel-divider" />
2914
+
2915
+ {/* Send Test Message Section */}
2677
2916
  {config.enableTestMessage !== false && (
2678
2917
  <CapRow className="panel-section send-test-section">
2679
2918
  {renderSendTestMessage()}
@@ -2687,7 +2926,28 @@ const CommonTestAndPreview = (props) => {
2687
2926
  {renderPreview()}
2688
2927
  </CapRow>
2689
2928
  </CapRow>
2690
- </CapRow>
2929
+ {customerModal[0] && customerModal[1] === CUSTOMER_MODAL_EXISTING && (
2930
+ <ExistingCustomerModal
2931
+ customerData={customerData}
2932
+ setCustomerModal={setCustomerModal}
2933
+ customerModal={customerModal}
2934
+ channel={channel}
2935
+ onSave={handleSaveTestCustomer}
2936
+ />
2937
+ )}
2938
+ {customerModal[0] && customerModal[1] === CUSTOMER_MODAL_NEW && (
2939
+ <CustomerCreationModal
2940
+ customerData={customerData}
2941
+ setCustomerData={setCustomerData}
2942
+ setCustomerModal={setCustomerModal}
2943
+ customerModal={customerModal}
2944
+ onSave={handleSaveTestCustomer}
2945
+ channel={channel}
2946
+ />
2947
+ )}
2948
+ </CapRow>
2949
+ </CapSpin>
2950
+ </div>
2691
2951
  )}
2692
2952
  />
2693
2953
  );
@@ -2789,4 +3049,4 @@ CommonTestAndPreview.defaultProps = {
2789
3049
  // Note: Redux connection is handled by the wrapper components (e.g., TestAndPreviewSlidebox)
2790
3050
  // This component receives all Redux props (including intl) from its parent
2791
3051
 
2792
- export default CommonTestAndPreview;
3052
+ export default CommonTestAndPreview;
@@ -8,10 +8,72 @@ import { defineMessages } from 'react-intl';
8
8
  export const scope = 'app.v2Components.TestAndPreviewSlidebox';
9
9
 
10
10
  export default defineMessages({
11
+ mobileAlreadyExists: {
12
+ id: `${scope}.mobileAlreadyExists`,
13
+ defaultMessage: 'This phone number matches with already existing user profile. Please remove or use a different number.',
14
+ },
15
+ customerNamePlaceholder: {
16
+ id: `${scope}.customerNamePlaceholder`,
17
+ defaultMessage: 'Enter the name',
18
+ },
19
+ customerEmailPlaceholder: {
20
+ id: `${scope}.customerEmailPlaceholder`,
21
+ defaultMessage: 'Enter the Email',
22
+ },
23
+ customerMobilePlaceholder: {
24
+ id: `${scope}.customerMobilePlaceholder`,
25
+ defaultMessage: 'Enter the Mobile Number',
26
+ },
27
+ customerAlreadyInTestList: {
28
+ id: `${scope}.customerAlreadyInTestList`,
29
+ defaultMessage: 'This customer is already in the test customers list.',
30
+ },
31
+ emailAlreadyExists: {
32
+ id: `${scope}.emailAlreadyExists`,
33
+ defaultMessage: 'This email matches with already existing user profile. Please remove or use a different email.',
34
+ },
35
+ customerName: {
36
+ id: `${scope}.customerName`,
37
+ defaultMessage: 'Name',
38
+ },
39
+ customerEmail: {
40
+ id: `${scope}.customerEmail`,
41
+ defaultMessage: 'Email',
42
+ },
43
+ customerMobile: {
44
+ id: `${scope}.customerMobileNumber`,
45
+ defaultMessage: 'Mobile Number',
46
+ },
47
+ saveButton: {
48
+ id: `${scope}.saveButton`,
49
+ defaultMessage: 'Save',
50
+ },
51
+ cancelButton: {
52
+ id: `${scope}.cancelButton`,
53
+ defaultMessage: 'Cancel',
54
+ },
55
+ customerID: {
56
+ id: `${scope}.customerID`,
57
+ defaultMessage: 'Customer ID',
58
+ },
59
+ existingCustomerModalDescription: {
60
+ id: `${scope}.existingCustomerModalDescription`,
61
+ defaultMessage: 'This user profile already exists in the system. Would you like to add them to Test Customer list?'
62
+ },
63
+
64
+ customerCreationModalTitle: {
65
+ id: `${scope}.customerCreationModalTitle`,
66
+ defaultMessage: 'Add new test customer',
67
+ },
68
+ customerCreationModalDescription: {
69
+ id: `${scope}.customerCreationModalDescription`,
70
+ defaultMessage: 'This customer profile will be available for testing across multiple communication channels.',
71
+ },
11
72
  testAndPreviewHeader: {
12
73
  id: `${scope}.testAndPreviewHeader`,
13
74
  defaultMessage: 'Preview and Test',
14
75
  },
76
+
15
77
  customerSearchTitle: {
16
78
  id: `${scope}.customerSearchTitle`,
17
79
  defaultMessage: 'Customer',
@@ -32,6 +94,34 @@ export default defineMessages({
32
94
  id: `${scope}.showJSON`,
33
95
  defaultMessage: 'Show JSON',
34
96
  },
97
+ addTestCustomer: {
98
+ id: `${scope}.addTestCustomer`,
99
+ defaultMessage: 'Add as Test Customer',
100
+ },
101
+ addTestCustomerWithValue: {
102
+ id: `${scope}.addTestCustomerWithValue`,
103
+ defaultMessage: 'Add {searchValue} as Test Customer',
104
+ },
105
+ memberLookupError: {
106
+ id: `${scope}.memberLookupError`,
107
+ defaultMessage: 'Unable to look up customer. Please try again.',
108
+ },
109
+ newTestCustomerAddedSuccess: {
110
+ id: `${scope}.newTestCustomerAddedSuccess`,
111
+ defaultMessage: 'New test customer added successfully!',
112
+ },
113
+ errorTitle: {
114
+ id: `${scope}.errorTitle`,
115
+ defaultMessage: 'Error',
116
+ },
117
+ failedToAddTestCustomer: {
118
+ id: `${scope}.failedToAddTestCustomer`,
119
+ defaultMessage: 'Failed to add test customer',
120
+ },
121
+ errorAddingTestCustomer: {
122
+ id: `${scope}.errorAddingTestCustomer`,
123
+ defaultMessage: 'An error occurred while adding test customer',
124
+ },
35
125
  discardCustomValues: {
36
126
  id: `${scope}.discardCustomValues`,
37
127
  defaultMessage: 'Discard custom values',
@@ -120,6 +210,10 @@ export default defineMessages({
120
210
  id: `${scope}.testCustomersPlaceholder`,
121
211
  defaultMessage: 'Search and select a group or individual test customers',
122
212
  },
213
+ noMatchingOptions: {
214
+ id: `${scope}.noMatchingOptions`,
215
+ defaultMessage: 'No matching options',
216
+ },
123
217
  updatingPreview: {
124
218
  id: `${scope}.updatingPreview`,
125
219
  defaultMessage: 'Updating preview with the latest changes',
@@ -20,6 +20,7 @@ import {
20
20
  GET_TEST_CUSTOMERS_REQUESTED,
21
21
  GET_TEST_CUSTOMERS_SUCCESS,
22
22
  GET_TEST_CUSTOMERS_FAILURE,
23
+ ADD_TEST_CUSTOMER,
23
24
  GET_TEST_GROUPS_REQUESTED,
24
25
  GET_TEST_GROUPS_SUCCESS,
25
26
  GET_TEST_GROUPS_FAILURE,
@@ -216,6 +217,15 @@ const previewAndTestReducer = (state = initialState, action) => {
216
217
  return state.set('isFetchingTestCustomers', false)
217
218
  .set('fetchTestCustomersError', action.payload.error);
218
219
 
220
+ case ADD_TEST_CUSTOMER: {
221
+ const raw = state.get('testCustomers');
222
+ const list = Array.isArray(raw) ? raw : (raw && raw.toArray ? raw.toArray().map((i) => (i && i.toJS ? i.toJS() : i)) : []);
223
+ const customer = action.payload.customer;
224
+ const newId = customer.userId || customer.customerId;
225
+ if (list.some((c) => (c.userId || c.customerId) === newId)) return state;
226
+ return state.set('testCustomers', list.concat([customer]));
227
+ }
228
+
219
229
  // Test Groups
220
230
  case GET_TEST_GROUPS_REQUESTED:
221
231
  return state.set('isFetchingTestGroups', true)
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Tests for AddTestCustomerButton Component
3
+ *
4
+ * The parent (index.js) only renders this button when channel is EMAIL/SMS and value is valid.
5
+ * This component always renders the button when mounted; visibility is the parent's responsibility.
6
+ */
7
+
8
+ import React from 'react';
9
+ import { render, screen, fireEvent } from '@testing-library/react';
10
+ import { IntlProvider } from 'react-intl';
11
+ import AddTestCustomerButton from '../AddTestCustomer';
12
+
13
+ const mockMessages = {
14
+ 'app.v2Components.TestAndPreviewSlidebox.addTestCustomerWithValue': 'Add {searchValue} as Test Customer',
15
+ };
16
+
17
+ const TestWrapper = ({ children }) => (
18
+ <IntlProvider locale="en" messages={mockMessages}>
19
+ {children}
20
+ </IntlProvider>
21
+ );
22
+
23
+ describe('AddTestCustomerButton', () => {
24
+ const defaultProps = {
25
+ searchValue: 'user@example.com',
26
+ handleAddTestCustomer: jest.fn(),
27
+ };
28
+
29
+ beforeEach(() => {
30
+ jest.clearAllMocks();
31
+ });
32
+
33
+ it('should render button with searchValue in message', () => {
34
+ render(
35
+ <TestWrapper>
36
+ <AddTestCustomerButton {...defaultProps} />
37
+ </TestWrapper>
38
+ );
39
+ const button = screen.getByRole('button', { name: /add.*test customer/i });
40
+ expect(button).toBeTruthy();
41
+ });
42
+
43
+ it('should show searchValue in button text', () => {
44
+ render(
45
+ <TestWrapper>
46
+ <AddTestCustomerButton {...defaultProps} searchValue="user@example.com" />
47
+ </TestWrapper>
48
+ );
49
+ expect(screen.getByRole('button', { name: /user@example.com/i })).toBeTruthy();
50
+ });
51
+
52
+ it('should call handleAddTestCustomer when button is clicked', () => {
53
+ const handleAddTestCustomer = jest.fn();
54
+ render(
55
+ <TestWrapper>
56
+ <AddTestCustomerButton
57
+ searchValue="user@example.com"
58
+ handleAddTestCustomer={handleAddTestCustomer}
59
+ />
60
+ </TestWrapper>
61
+ );
62
+ const button = screen.getByRole('button', { name: /add.*test customer/i });
63
+ fireEvent.click(button);
64
+ expect(handleAddTestCustomer).toHaveBeenCalledTimes(1);
65
+ });
66
+ });