@capillarytech/creatives-library 8.0.297 → 8.0.299-alpha.3
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/services/api.js +17 -0
- package/services/tests/api.test.js +85 -0
- package/utils/commonUtils.js +10 -0
- package/utils/tests/commonUtil.test.js +169 -0
- package/v2Components/CommonTestAndPreview/AddTestCustomer.js +42 -0
- package/v2Components/CommonTestAndPreview/CustomerCreationModal.js +284 -0
- package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +72 -0
- package/v2Components/CommonTestAndPreview/SendTestMessage.js +78 -49
- package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +189 -4
- package/v2Components/CommonTestAndPreview/actions.js +10 -0
- package/v2Components/CommonTestAndPreview/constants.js +18 -1
- package/v2Components/CommonTestAndPreview/index.js +259 -14
- package/v2Components/CommonTestAndPreview/messages.js +94 -0
- package/v2Components/CommonTestAndPreview/reducer.js +10 -0
- package/v2Components/CommonTestAndPreview/tests/AddTestCustomer.test.js +66 -0
- package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +653 -0
- package/v2Components/CommonTestAndPreview/tests/CustomerCreationModal.test.js +316 -0
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +0 -1
- package/v2Components/CommonTestAndPreview/tests/ExistingCustomerModal.test.js +114 -0
- package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +53 -0
- package/v2Components/CommonTestAndPreview/tests/constants.test.js +25 -2
- package/v2Components/CommonTestAndPreview/tests/index.test.js +7 -0
- package/v2Components/CommonTestAndPreview/tests/reducer.test.js +71 -0
- package/v2Components/CommonTestAndPreview/tests/selectors.test.js +17 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +1408 -1276
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +321 -288
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +5246 -4872
package/package.json
CHANGED
package/services/api.js
CHANGED
|
@@ -735,4 +735,21 @@ export const getBeePopupBuilderToken = () => {
|
|
|
735
735
|
return request(url, getAPICallObject('GET'));
|
|
736
736
|
};
|
|
737
737
|
|
|
738
|
+
/**
|
|
739
|
+
* Look up member by identifier (email or mobile) for add test customer flow.
|
|
740
|
+
* Uses standard API auth headers (no x-cap-ct) to avoid CORS preflight issues with node layer.
|
|
741
|
+
* @param {string} identifierType - 'email' or 'mobile'
|
|
742
|
+
* @param {string} identifierValue - email address or mobile number
|
|
743
|
+
* @returns {Promise} Promise resolving to { success, response: { exists, customerDetails } }
|
|
744
|
+
*/
|
|
745
|
+
export const getMembersLookup = (identifierType, identifierValue) => {
|
|
746
|
+
const url = `${API_ENDPOINT}/members?identifierType=${encodeURIComponent(identifierType)}&identifierValue=${encodeURIComponent(identifierValue)}`;
|
|
747
|
+
return request(url, getAPICallObject('GET'));
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
export const createTestCustomer = (payload) => {
|
|
751
|
+
const url = `${API_ENDPOINT}/testCustomers`;
|
|
752
|
+
return request(url, getAPICallObject('POST', payload));
|
|
753
|
+
};
|
|
754
|
+
|
|
738
755
|
export {request, getAPICallObject};
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
getAssetStatus,
|
|
29
29
|
getBeePopupBuilderToken,
|
|
30
30
|
getCmsAccounts,
|
|
31
|
+
getMembersLookup,
|
|
32
|
+
createTestCustomer,
|
|
31
33
|
} from '../api';
|
|
32
34
|
import { mockData } from './mockData';
|
|
33
35
|
import getSchema from '../getSchema';
|
|
@@ -1007,3 +1009,86 @@ describe('getCmsAccounts', () => {
|
|
|
1007
1009
|
expect(result).toBeInstanceOf(Promise);
|
|
1008
1010
|
});
|
|
1009
1011
|
});
|
|
1012
|
+
|
|
1013
|
+
describe('getMembersLookup', () => {
|
|
1014
|
+
beforeEach(() => {
|
|
1015
|
+
global.fetch = jest.fn();
|
|
1016
|
+
global.fetch.mockReturnValue(Promise.resolve({
|
|
1017
|
+
status: 200,
|
|
1018
|
+
json: () => Promise.resolve({ success: true, response: { exists: false, customerDetails: [] } }),
|
|
1019
|
+
}));
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
it('should return a Promise', () => {
|
|
1023
|
+
const result = getMembersLookup('email', 'user@example.com');
|
|
1024
|
+
expect(result).toBeInstanceOf(Promise);
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
it('should be callable with identifierType and identifierValue', () => {
|
|
1028
|
+
expect(typeof getMembersLookup).toBe('function');
|
|
1029
|
+
const result = getMembersLookup('mobile', '9123456789');
|
|
1030
|
+
expect(result).toBeInstanceOf(Promise);
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
it('should call fetch with correct URL encoding and GET method', () => {
|
|
1034
|
+
global.fetch.mockClear();
|
|
1035
|
+
getMembersLookup('email', 'user+test@example.com');
|
|
1036
|
+
expect(global.fetch).toHaveBeenCalled();
|
|
1037
|
+
const calls = global.fetch.mock.calls;
|
|
1038
|
+
const withEncoding = calls.find(
|
|
1039
|
+
(c) =>
|
|
1040
|
+
c[0] &&
|
|
1041
|
+
String(c[0]).includes('members') &&
|
|
1042
|
+
String(c[0]).includes('identifierType=email') &&
|
|
1043
|
+
String(c[0]).includes('user%2Btest%40example.com')
|
|
1044
|
+
);
|
|
1045
|
+
if (withEncoding) {
|
|
1046
|
+
const [url, options] = withEncoding;
|
|
1047
|
+
expect(url).toContain('identifierType=email');
|
|
1048
|
+
expect(url).toContain('identifierValue=user%2Btest%40example.com');
|
|
1049
|
+
expect(options?.method || 'GET').toBe('GET');
|
|
1050
|
+
}
|
|
1051
|
+
const anyMembersCall = calls.find((c) => c[0] && String(c[0]).includes('members'));
|
|
1052
|
+
expect(anyMembersCall).toBeDefined();
|
|
1053
|
+
expect(anyMembersCall[0]).toContain('identifierType=');
|
|
1054
|
+
expect(anyMembersCall[0]).toContain('identifierValue=');
|
|
1055
|
+
expect(anyMembersCall[1]?.method || 'GET').toBe('GET');
|
|
1056
|
+
});
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
describe('createTestCustomer', () => {
|
|
1060
|
+
beforeEach(() => {
|
|
1061
|
+
global.fetch = jest.fn();
|
|
1062
|
+
global.fetch.mockReturnValue(Promise.resolve({
|
|
1063
|
+
status: 200,
|
|
1064
|
+
json: () => Promise.resolve({ success: true }),
|
|
1065
|
+
}));
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
it('should return a Promise', () => {
|
|
1069
|
+
const payload = { customer: { email: 'test@example.com', firstName: 'Test' } };
|
|
1070
|
+
const result = createTestCustomer(payload);
|
|
1071
|
+
expect(result).toBeInstanceOf(Promise);
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
it('should accept customerId payload for existing customer', () => {
|
|
1075
|
+
const payload = { customerId: 'cust-123' };
|
|
1076
|
+
const result = createTestCustomer(payload);
|
|
1077
|
+
expect(result).toBeInstanceOf(Promise);
|
|
1078
|
+
expect(global.fetch).toHaveBeenCalled();
|
|
1079
|
+
const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
|
|
1080
|
+
expect(lastCall[0]).toContain('testCustomers');
|
|
1081
|
+
expect(lastCall[1].method).toBe('POST');
|
|
1082
|
+
expect(lastCall[1].body).toBe(JSON.stringify(payload));
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
it('should call fetch with /testCustomers URL and POST method', () => {
|
|
1086
|
+
const payload = { customer: { email: 'n@b.co', firstName: 'N' } };
|
|
1087
|
+
createTestCustomer(payload);
|
|
1088
|
+
expect(global.fetch).toHaveBeenCalled();
|
|
1089
|
+
const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
|
|
1090
|
+
expect(lastCall[0]).toContain('testCustomers');
|
|
1091
|
+
expect(lastCall[1].method).toBe('POST');
|
|
1092
|
+
expect(lastCall[1].body).toBe(JSON.stringify(payload));
|
|
1093
|
+
});
|
|
1094
|
+
});
|
package/utils/commonUtils.js
CHANGED
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
import { GLOBAL_CONVERT_OPTIONS } from "../v2Components/FormBuilder/constants";
|
|
19
19
|
import { checkSupport, extractNames, skipTags as defaultSkipTags, isInsideLiquidBlock } from "./tagValidations";
|
|
20
20
|
import { SMS_TRAI_VAR } from '../v2Containers/SmsTrai/Edit/constants';
|
|
21
|
+
import { EMAIL_REGEX, PHONE_REGEX } from '../v2Components/CommonTestAndPreview/constants';
|
|
22
|
+
|
|
21
23
|
export const apiMessageFormatHandler = (id, fallback) => (
|
|
22
24
|
<FormattedMessage id={id} defaultMessage={fallback} />
|
|
23
25
|
);
|
|
@@ -587,3 +589,11 @@ export const checkForPersonalizationTokens = (formData) => {
|
|
|
587
589
|
}
|
|
588
590
|
return false;
|
|
589
591
|
};
|
|
592
|
+
|
|
593
|
+
export const isValidEmail = (email) => EMAIL_REGEX.test(email);
|
|
594
|
+
export const isValidMobile = (mobile) => PHONE_REGEX.test(mobile);
|
|
595
|
+
|
|
596
|
+
export const formatPhoneNumber = (phone) => {
|
|
597
|
+
if (!phone) return '';
|
|
598
|
+
return String(phone).replace(/[^\d]/g, '');
|
|
599
|
+
};
|
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
validateCarouselCards,
|
|
9
9
|
hasPersonalizationTags,
|
|
10
10
|
checkForPersonalizationTokens,
|
|
11
|
+
isValidEmail,
|
|
12
|
+
isValidMobile,
|
|
13
|
+
formatPhoneNumber,
|
|
14
|
+
getMessageForDevice,
|
|
15
|
+
getTitleForDevice,
|
|
11
16
|
} from "../commonUtils";
|
|
12
17
|
import { skipTags } from "../tagValidations";
|
|
13
18
|
import { SMS_TRAI_VAR } from '../../v2Containers/SmsTrai/Edit/constants';
|
|
@@ -586,6 +591,170 @@ describe("validateLiquidTemplateContent", () => {
|
|
|
586
591
|
});
|
|
587
592
|
});
|
|
588
593
|
|
|
594
|
+
describe("isValidEmail", () => {
|
|
595
|
+
it("returns true for valid email addresses", () => {
|
|
596
|
+
expect(isValidEmail("user@example.com")).toBe(true);
|
|
597
|
+
expect(isValidEmail("test.user@domain.co")).toBe(true);
|
|
598
|
+
expect(isValidEmail("a@b.co")).toBe(true);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("returns false for invalid email addresses", () => {
|
|
602
|
+
expect(isValidEmail("")).toBe(false);
|
|
603
|
+
expect(isValidEmail("invalid")).toBe(false);
|
|
604
|
+
expect(isValidEmail("@nodomain.com")).toBe(false);
|
|
605
|
+
expect(isValidEmail("noatsign.com")).toBe(false);
|
|
606
|
+
expect(isValidEmail("missingtld@domain")).toBe(false);
|
|
607
|
+
expect(isValidEmail(" user@example.com ")).toBe(false);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it("returns true for edge case single-char local part", () => {
|
|
611
|
+
expect(isValidEmail("x@y.co")).toBe(true);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
describe("isValidMobile", () => {
|
|
616
|
+
it("returns true for valid mobile numbers (8-15 digits, no leading zero)", () => {
|
|
617
|
+
expect(isValidMobile("12345678")).toBe(true);
|
|
618
|
+
expect(isValidMobile("9123456789")).toBe(true);
|
|
619
|
+
expect(isValidMobile("123456789012345")).toBe(true);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("returns false for invalid mobile numbers", () => {
|
|
623
|
+
expect(isValidMobile("")).toBe(false);
|
|
624
|
+
expect(isValidMobile("01234567")).toBe(false);
|
|
625
|
+
expect(isValidMobile("1234567")).toBe(false);
|
|
626
|
+
expect(isValidMobile("1234567890123456")).toBe(false);
|
|
627
|
+
expect(isValidMobile("abc12345678")).toBe(false);
|
|
628
|
+
expect(isValidMobile("12345 67890")).toBe(false);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("returns true for exactly 8 and exactly 15 digits", () => {
|
|
632
|
+
expect(isValidMobile("12345678")).toBe(true);
|
|
633
|
+
expect(isValidMobile("123456789012345")).toBe(true);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
describe("formatPhoneNumber", () => {
|
|
638
|
+
it("returns empty string for falsy input", () => {
|
|
639
|
+
expect(formatPhoneNumber("")).toBe("");
|
|
640
|
+
expect(formatPhoneNumber(null)).toBe("");
|
|
641
|
+
expect(formatPhoneNumber(undefined)).toBe("");
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it("strips non-digit characters", () => {
|
|
645
|
+
expect(formatPhoneNumber("91 234 567 890")).toBe("91234567890");
|
|
646
|
+
expect(formatPhoneNumber("+91-234567890")).toBe("91234567890");
|
|
647
|
+
expect(formatPhoneNumber("(123) 456-7890")).toBe("1234567890");
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it("returns digits-only string unchanged", () => {
|
|
651
|
+
expect(formatPhoneNumber("9123456789")).toBe("9123456789");
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it("returns empty string for whitespace-only input", () => {
|
|
655
|
+
expect(formatPhoneNumber(" ")).toBe("");
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
describe("hasPersonalizationTags", () => {
|
|
660
|
+
it("returns true when text has liquid tags {{ }}", () => {
|
|
661
|
+
expect(hasPersonalizationTags("Hello {{name}}")).toBe(true);
|
|
662
|
+
expect(hasPersonalizationTags("{{foo}}")).toBe(true);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it("returns true when text has bracket tags [ ]", () => {
|
|
666
|
+
expect(hasPersonalizationTags("Hello [event.name]")).toBe(true);
|
|
667
|
+
expect(hasPersonalizationTags("[tag]")).toBe(true);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it("returns false for empty or no tags", () => {
|
|
671
|
+
expect(hasPersonalizationTags("")).toBeFalsy();
|
|
672
|
+
expect(hasPersonalizationTags()).toBeFalsy();
|
|
673
|
+
expect(hasPersonalizationTags("plain text")).toBe(false);
|
|
674
|
+
expect(hasPersonalizationTags("only {{ open")).toBe(false);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it("returns false when only [ or only ] present without matching pair", () => {
|
|
678
|
+
expect(hasPersonalizationTags("only [ open")).toBe(false);
|
|
679
|
+
expect(hasPersonalizationTags("only ] close")).toBe(false);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it("returns true when liquid tags have content with spaces", () => {
|
|
683
|
+
expect(hasPersonalizationTags("Hello {{ customer.name }}")).toBe(true);
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
describe("getMessageForDevice", () => {
|
|
688
|
+
it("returns message for device from templateData", () => {
|
|
689
|
+
const templateData = {
|
|
690
|
+
versions: {
|
|
691
|
+
android: { base: { expandableDetails: { message: "Android msg" } } },
|
|
692
|
+
ios: { base: { expandableDetails: { message: "iOS msg" } } },
|
|
693
|
+
},
|
|
694
|
+
};
|
|
695
|
+
expect(getMessageForDevice(templateData, "android")).toBe("Android msg");
|
|
696
|
+
expect(getMessageForDevice(templateData, "ios")).toBe("iOS msg");
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it("returns undefined for missing path", () => {
|
|
700
|
+
expect(getMessageForDevice(null, "android")).toBeUndefined();
|
|
701
|
+
expect(getMessageForDevice({}, "android")).toBeUndefined();
|
|
702
|
+
expect(getMessageForDevice({ versions: {} }, "android")).toBeUndefined();
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it("returns undefined when base exists but expandableDetails is missing", () => {
|
|
706
|
+
expect(getMessageForDevice({ versions: { android: { base: {} } } }, "android")).toBeUndefined();
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
describe("getTitleForDevice", () => {
|
|
711
|
+
it("returns title for device from templateData", () => {
|
|
712
|
+
const templateData = {
|
|
713
|
+
versions: {
|
|
714
|
+
android: { base: { title: "Android title" } },
|
|
715
|
+
ios: { base: { title: "iOS title" } },
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
expect(getTitleForDevice(templateData, "android")).toBe("Android title");
|
|
719
|
+
expect(getTitleForDevice(templateData, "ios")).toBe("iOS title");
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it("returns empty string for missing title", () => {
|
|
723
|
+
expect(getTitleForDevice(null, "android")).toBe("");
|
|
724
|
+
expect(getTitleForDevice({}, "android")).toBe("");
|
|
725
|
+
expect(getTitleForDevice({ versions: { android: { base: {} } } }, "android")).toBe("");
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it("returns empty string when base.title is undefined", () => {
|
|
729
|
+
expect(getTitleForDevice({ versions: { android: { base: { title: undefined } } } }, "android")).toBe("");
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
describe("checkForPersonalizationTokens", () => {
|
|
734
|
+
it("returns true when formData contains liquid or bracket tokens", () => {
|
|
735
|
+
expect(checkForPersonalizationTokens({ 0: { content: "Hi {{name}}" } })).toBe(true);
|
|
736
|
+
expect(checkForPersonalizationTokens({ tab1: { message: "Hello [event.id]" } })).toBe(true);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it("returns false for empty or no tokens", () => {
|
|
740
|
+
expect(checkForPersonalizationTokens(null)).toBe(false);
|
|
741
|
+
expect(checkForPersonalizationTokens({})).toBe(false);
|
|
742
|
+
expect(checkForPersonalizationTokens({ 0: { content: "plain" } })).toBe(false);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it("returns false for non-object formData", () => {
|
|
746
|
+
expect(checkForPersonalizationTokens("string")).toBe(false);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it("returns true for two-level nested object containing token", () => {
|
|
750
|
+
expect(checkForPersonalizationTokens({ tab: { body: "Hi {{name}}" } })).toBe(true);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it("returns false for formData with array value (no string tokens)", () => {
|
|
754
|
+
expect(checkForPersonalizationTokens({ 0: { items: ["a", "b"] } })).toBe(false);
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
|
|
589
758
|
describe("validateMobilePushContent", () => {
|
|
590
759
|
const formatMessage = jest.fn(msg => msg.id);
|
|
591
760
|
const messages = {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import PropTypes from "prop-types";
|
|
2
|
+
import { FormattedMessage } from "react-intl";
|
|
3
|
+
import CapButton from "@capillarytech/cap-ui-library/CapButton";
|
|
4
|
+
import CapIcon from "@capillarytech/cap-ui-library/CapIcon";
|
|
5
|
+
import messages from "./messages";
|
|
6
|
+
import React from "react";
|
|
7
|
+
|
|
8
|
+
const AddTestCustomerButton = ({
|
|
9
|
+
searchValue,
|
|
10
|
+
handleAddTestCustomer
|
|
11
|
+
}) => (
|
|
12
|
+
<CapButton
|
|
13
|
+
onClick={handleAddTestCustomer}
|
|
14
|
+
type="flat"
|
|
15
|
+
size="small"
|
|
16
|
+
className="test-customer-add-btn"
|
|
17
|
+
prefix={
|
|
18
|
+
<CapIcon
|
|
19
|
+
type="add-profile"
|
|
20
|
+
className="add-test-customer-icon"
|
|
21
|
+
/>
|
|
22
|
+
}
|
|
23
|
+
>
|
|
24
|
+
<FormattedMessage
|
|
25
|
+
{...messages.addTestCustomerWithValue}
|
|
26
|
+
values={{
|
|
27
|
+
searchValue: (
|
|
28
|
+
<span className="test-customer-add-btn-value" title={searchValue || ""}>
|
|
29
|
+
"{searchValue}"
|
|
30
|
+
</span>
|
|
31
|
+
),
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
</CapButton>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
AddTestCustomerButton.propTypes = {
|
|
38
|
+
searchValue: PropTypes.string.isRequired,
|
|
39
|
+
handleAddTestCustomer: PropTypes.func.isRequired
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default AddTestCustomerButton;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import CapModal from "@capillarytech/cap-ui-library/CapModal";
|
|
2
|
+
import CapButton from "@capillarytech/cap-ui-library/CapButton";
|
|
3
|
+
import CapInput from "@capillarytech/cap-ui-library/CapInput";
|
|
4
|
+
import CapLabel from "@capillarytech/cap-ui-library/CapLabel";
|
|
5
|
+
import CapRow from "@capillarytech/cap-ui-library/CapRow";
|
|
6
|
+
import CapColumn from "@capillarytech/cap-ui-library/CapColumn";
|
|
7
|
+
import CapSpin from "@capillarytech/cap-ui-library/CapSpin";
|
|
8
|
+
import { FormattedMessage, injectIntl } from "react-intl";
|
|
9
|
+
import messages from "./messages";
|
|
10
|
+
import React, { useState, useCallback, useEffect, useRef } from "react";
|
|
11
|
+
import { CHANNELS, IDENTIFIER_TYPE_EMAIL, IDENTIFIER_TYPE_MOBILE, INPUT_HAS_ERROR_CLASS } from "./constants";
|
|
12
|
+
import { getMembersLookup } from '../../services/api';
|
|
13
|
+
import { isValidEmail, isValidMobile, formatPhoneNumber } from "../../utils/commonUtils";
|
|
14
|
+
|
|
15
|
+
const CustomerCreationModal = ({
|
|
16
|
+
customerModal,
|
|
17
|
+
setCustomerModal,
|
|
18
|
+
channel,
|
|
19
|
+
customerData,
|
|
20
|
+
setCustomerData,
|
|
21
|
+
onSave,
|
|
22
|
+
intl,
|
|
23
|
+
}) => {
|
|
24
|
+
const [validationErrors, setValidationErrors] = useState({
|
|
25
|
+
email: "",
|
|
26
|
+
mobile: ""
|
|
27
|
+
});
|
|
28
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
29
|
+
const [lookupLoading, setLookupLoading] = useState(null); // null | IDENTIFIER_TYPE_EMAIL | IDENTIFIER_TYPE_MOBILE
|
|
30
|
+
|
|
31
|
+
// Refs to track the latest values for async validation
|
|
32
|
+
const latestEmailRef = useRef(customerData.email || "");
|
|
33
|
+
const latestMobileRef = useRef(customerData.mobile || "");
|
|
34
|
+
|
|
35
|
+
// Helper function to render validation errors
|
|
36
|
+
const renderValidationError = (error) => {
|
|
37
|
+
if (!error) return null;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="customer-creation-modal-validation-error">
|
|
41
|
+
{typeof error === 'string' ? error : <FormattedMessage {...error} />}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Check if required fields are filled based on channel
|
|
47
|
+
const isRequiredFieldMissing = () => {
|
|
48
|
+
if (channel === CHANNELS.EMAIL && !customerData.email) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (channel === CHANNELS.SMS && !customerData.mobile) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Check if save button should be disabled
|
|
58
|
+
const isSaveDisabled = () => {
|
|
59
|
+
return (
|
|
60
|
+
validationErrors.email ||
|
|
61
|
+
validationErrors.mobile ||
|
|
62
|
+
isRequiredFieldMissing() ||
|
|
63
|
+
!!lookupLoading
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Perform lookup (same logic as in index.js)
|
|
68
|
+
const performLookup = useCallback(async (identifierType, value) => {
|
|
69
|
+
try {
|
|
70
|
+
const response = await getMembersLookup(identifierType, value);
|
|
71
|
+
const success = response?.success && !response?.status?.isError;
|
|
72
|
+
const res = response?.response || {};
|
|
73
|
+
const exists = res.exists || false;
|
|
74
|
+
const details = res.customerDetails || [];
|
|
75
|
+
if (!success) {
|
|
76
|
+
const errorMessage = response?.message || response?.status?.message
|
|
77
|
+
if (errorMessage.toLowerCase().includes("merged customer found")) {
|
|
78
|
+
return { success: true, exists: true, details: [] };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { success, exists, details };
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return { success: false, exists: false, details: [] };
|
|
85
|
+
}
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
// Handle validation and lookup for email or mobile
|
|
89
|
+
const handleLookupValidation = useCallback(async (type, value) => {
|
|
90
|
+
const isEmail = type === IDENTIFIER_TYPE_EMAIL;
|
|
91
|
+
const ref = isEmail ? latestEmailRef : latestMobileRef;
|
|
92
|
+
const field = isEmail ? IDENTIFIER_TYPE_EMAIL : IDENTIFIER_TYPE_MOBILE;
|
|
93
|
+
|
|
94
|
+
if (!value) {
|
|
95
|
+
if (ref.current === value) {
|
|
96
|
+
setValidationErrors(prev => ({ ...prev, [field]: "" }));
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (isEmail && !isValidEmail(value)) {
|
|
102
|
+
if (ref.current === value) {
|
|
103
|
+
setValidationErrors(prev => ({ ...prev, [IDENTIFIER_TYPE_EMAIL]: "Please enter a valid email address" }));
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!isEmail && !isValidMobile(value)) {
|
|
108
|
+
if (ref.current === value) {
|
|
109
|
+
setValidationErrors(prev => ({ ...prev, [IDENTIFIER_TYPE_MOBILE]: "Please enter a valid mobile number" }));
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setLookupLoading(field);
|
|
115
|
+
try {
|
|
116
|
+
const { success, exists } = await performLookup(type, value);
|
|
117
|
+
|
|
118
|
+
if (ref.current === value) {
|
|
119
|
+
if (success && exists) {
|
|
120
|
+
setValidationErrors(prev => ({
|
|
121
|
+
...prev,
|
|
122
|
+
[field]: isEmail ? messages.emailAlreadyExists : messages.mobileAlreadyExists
|
|
123
|
+
}));
|
|
124
|
+
} else {
|
|
125
|
+
setValidationErrors(prev => ({ ...prev, [field]: "" }));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
setLookupLoading(null);
|
|
130
|
+
}
|
|
131
|
+
}, [performLookup]);
|
|
132
|
+
|
|
133
|
+
// Debounce timer refs
|
|
134
|
+
const emailTimeoutRef = React.useRef(null);
|
|
135
|
+
const mobileTimeoutRef = React.useRef(null);
|
|
136
|
+
|
|
137
|
+
// Handle email change with debounced validation
|
|
138
|
+
const handleEmailChange = (e) => {
|
|
139
|
+
const email = e.target.value;
|
|
140
|
+
latestEmailRef.current = email; // Update the latest email ref
|
|
141
|
+
setCustomerData({ ...customerData, email });
|
|
142
|
+
|
|
143
|
+
// Clear previous timeout
|
|
144
|
+
if (emailTimeoutRef.current) {
|
|
145
|
+
clearTimeout(emailTimeoutRef.current);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Set new timeout for validation
|
|
149
|
+
emailTimeoutRef.current = setTimeout(() => {
|
|
150
|
+
handleLookupValidation(IDENTIFIER_TYPE_EMAIL, email);
|
|
151
|
+
}, 500);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Handle mobile change with debounced validation
|
|
155
|
+
const handleMobileChange = (e) => {
|
|
156
|
+
const rawValue = e.target.value;
|
|
157
|
+
const formattedMobile = formatPhoneNumber(rawValue);
|
|
158
|
+
latestMobileRef.current = formattedMobile; // Update the latest mobile ref
|
|
159
|
+
setCustomerData({ ...customerData, mobile: formattedMobile });
|
|
160
|
+
|
|
161
|
+
// Clear previous timeout
|
|
162
|
+
if (mobileTimeoutRef.current) {
|
|
163
|
+
clearTimeout(mobileTimeoutRef.current);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Set new timeout for validation
|
|
167
|
+
mobileTimeoutRef.current = setTimeout(() => {
|
|
168
|
+
handleLookupValidation(IDENTIFIER_TYPE_MOBILE, formattedMobile);
|
|
169
|
+
}, 500);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Update refs when customerData changes externally
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
latestEmailRef.current = customerData.email || "";
|
|
175
|
+
latestMobileRef.current = customerData.mobile || "";
|
|
176
|
+
}, [customerData.email, customerData.mobile]);
|
|
177
|
+
|
|
178
|
+
// Cleanup timeouts on unmount
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
return () => {
|
|
181
|
+
if (emailTimeoutRef.current) {
|
|
182
|
+
clearTimeout(emailTimeoutRef.current);
|
|
183
|
+
}
|
|
184
|
+
if (mobileTimeoutRef.current) {
|
|
185
|
+
clearTimeout(mobileTimeoutRef.current);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}, []);
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<CapModal
|
|
192
|
+
visible={customerModal[0]}
|
|
193
|
+
onCancel={() => setCustomerModal([false, ""])}
|
|
194
|
+
centered={true}
|
|
195
|
+
width={500}
|
|
196
|
+
maskStyle={{ backgroundColor: 'rgba(244, 245, 247, 0.9)' }}
|
|
197
|
+
footer={
|
|
198
|
+
<CapRow justify="start" text-align="left" gutter={8}>
|
|
199
|
+
<CapButton
|
|
200
|
+
type="primary"
|
|
201
|
+
onClick={() => onSave(validationErrors, setIsLoading)}
|
|
202
|
+
disabled={isSaveDisabled() || lookupLoading}
|
|
203
|
+
loading={isLoading}
|
|
204
|
+
>
|
|
205
|
+
<FormattedMessage {...messages.saveButton} />
|
|
206
|
+
</CapButton>
|
|
207
|
+
<CapButton
|
|
208
|
+
type="secondary"
|
|
209
|
+
onClick={() => setCustomerModal([false, ""])}
|
|
210
|
+
disabled={isLoading || lookupLoading}
|
|
211
|
+
>
|
|
212
|
+
<FormattedMessage {...messages.cancelButton} />
|
|
213
|
+
</CapButton>
|
|
214
|
+
</CapRow>
|
|
215
|
+
}
|
|
216
|
+
title={<FormattedMessage {...messages.customerCreationModalTitle} />}
|
|
217
|
+
wrapClassName="common-test-preview-modal-wrap"
|
|
218
|
+
className="common-test-preview-modal"
|
|
219
|
+
>
|
|
220
|
+
<CapRow className="customer-creation-modal-row">
|
|
221
|
+
<CapColumn span={24}>
|
|
222
|
+
<span className="customer-creation-modal-description">
|
|
223
|
+
<FormattedMessage {...messages.customerCreationModalDescription} />
|
|
224
|
+
</span>
|
|
225
|
+
</CapColumn>
|
|
226
|
+
</CapRow>
|
|
227
|
+
|
|
228
|
+
<CapRow className="customer-creation-modal-row">
|
|
229
|
+
<CapColumn span={24}>
|
|
230
|
+
<CapLabel type="label1" className="customer-creation-modal-label">
|
|
231
|
+
<FormattedMessage {...messages.customerName} /> <span className="customer-creation-modal-optional">(Optional)</span>
|
|
232
|
+
</CapLabel>
|
|
233
|
+
<CapInput
|
|
234
|
+
value={customerData.name || ""}
|
|
235
|
+
onChange={e =>
|
|
236
|
+
setCustomerData({ ...customerData, name: e.target.value })
|
|
237
|
+
}
|
|
238
|
+
placeholder={intl.formatMessage(messages.customerNamePlaceholder)}
|
|
239
|
+
size="default"
|
|
240
|
+
className="customer-creation-modal-input"
|
|
241
|
+
/>
|
|
242
|
+
</CapColumn>
|
|
243
|
+
</CapRow>
|
|
244
|
+
|
|
245
|
+
<CapRow className="customer-creation-modal-row">
|
|
246
|
+
<CapColumn span={24}>
|
|
247
|
+
<CapLabel type="label1" className="customer-creation-modal-label">
|
|
248
|
+
<FormattedMessage {...messages.customerEmail} /> {channel !== CHANNELS.EMAIL && <span className="customer-creation-modal-optional">(Optional)</span>}
|
|
249
|
+
</CapLabel>
|
|
250
|
+
<CapInput
|
|
251
|
+
value={customerData.email || ""}
|
|
252
|
+
onChange={handleEmailChange}
|
|
253
|
+
placeholder={intl.formatMessage(messages.customerEmailPlaceholder)}
|
|
254
|
+
size="default"
|
|
255
|
+
className={`customer-creation-modal-input${validationErrors.email ? INPUT_HAS_ERROR_CLASS : ''}`}
|
|
256
|
+
disabled={!!lookupLoading}
|
|
257
|
+
suffix={lookupLoading === IDENTIFIER_TYPE_EMAIL ? <CapSpin size="small" /> : null}
|
|
258
|
+
/>
|
|
259
|
+
{renderValidationError(validationErrors.email)}
|
|
260
|
+
</CapColumn>
|
|
261
|
+
</CapRow>
|
|
262
|
+
|
|
263
|
+
<CapRow className="customer-creation-modal-row customer-creation-modal-row--last">
|
|
264
|
+
<CapColumn span={24}>
|
|
265
|
+
<CapLabel type="label1" className="customer-creation-modal-label">
|
|
266
|
+
<FormattedMessage {...messages.customerMobile} /> {channel !== CHANNELS.SMS && <span className="customer-creation-modal-optional">(Optional)</span>}
|
|
267
|
+
</CapLabel>
|
|
268
|
+
<CapInput
|
|
269
|
+
value={customerData.mobile || ""}
|
|
270
|
+
onChange={handleMobileChange}
|
|
271
|
+
placeholder={intl.formatMessage(messages.customerMobilePlaceholder)}
|
|
272
|
+
size="default"
|
|
273
|
+
className={`customer-creation-modal-input${validationErrors.mobile ? INPUT_HAS_ERROR_CLASS : ''}`}
|
|
274
|
+
disabled={!!lookupLoading}
|
|
275
|
+
suffix={lookupLoading === IDENTIFIER_TYPE_MOBILE ? <CapSpin size="small" /> : null}
|
|
276
|
+
/>
|
|
277
|
+
{renderValidationError(validationErrors.mobile)}
|
|
278
|
+
</CapColumn>
|
|
279
|
+
</CapRow>
|
|
280
|
+
</CapModal>
|
|
281
|
+
);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
export default injectIntl(CustomerCreationModal);
|