@capillarytech/creatives-library 8.0.235 → 8.0.236-alpha.1
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/assets/Android.png +0 -0
- package/assets/iOS.png +0 -0
- package/config/app.js +0 -1
- package/constants/unified.js +1 -1
- package/initialReducer.js +2 -0
- package/package.json +1 -1
- package/services/api.js +5 -2
- package/services/tests/api.test.js +18 -0
- package/utils/common.js +1 -2
- package/utils/commonUtils.js +14 -1
- package/utils/transformTemplateConfig.js +0 -10
- package/v2Components/CapDeviceContent/index.js +61 -56
- package/v2Components/CapTagList/index.js +4 -0
- package/v2Components/CapWhatsappCTA/tests/index.test.js +5 -0
- package/v2Components/HtmlEditor/HTMLEditor.js +165 -80
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +532 -0
- package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +17 -12
- package/v2Components/HtmlEditor/_htmlEditor.scss +0 -4
- package/v2Components/HtmlEditor/_index.lazy.scss +0 -1
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +0 -98
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +125 -148
- package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +1 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/index.js +3 -3
- package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +4 -7
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +35 -45
- package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +1 -3
- package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +33 -33
- package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +7 -6
- package/v2Components/HtmlEditor/constants.js +29 -20
- package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +158 -17
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +53 -143
- package/v2Components/HtmlEditor/index.js +1 -1
- package/v2Components/HtmlEditor/messages.js +85 -85
- package/v2Components/MobilePushPreviewV2/index.js +32 -7
- package/v2Components/TemplatePreview/_templatePreview.scss +31 -21
- package/v2Components/TemplatePreview/index.js +47 -32
- package/v2Components/TemplatePreview/messages.js +4 -0
- package/v2Containers/BeeEditor/index.js +82 -80
- package/v2Containers/BeePopupEditor/constants.js +10 -0
- package/v2Containers/BeePopupEditor/index.js +180 -0
- package/v2Containers/BeePopupEditor/tests/index.test.js +627 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +69 -34
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +2 -1
- package/v2Containers/CreativesContainer/constants.js +1 -0
- package/v2Containers/CreativesContainer/index.js +65 -13
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +4 -12
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +15 -0
- package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +376 -0
- package/v2Containers/InApp/__tests__/sagas.test.js +363 -0
- package/v2Containers/InApp/actions.js +7 -0
- package/v2Containers/InApp/constants.js +18 -4
- package/v2Containers/InApp/index.js +642 -355
- package/v2Containers/InApp/index.scss +4 -3
- package/v2Containers/InApp/messages.js +7 -3
- package/v2Containers/InApp/reducer.js +21 -3
- package/v2Containers/InApp/sagas.js +29 -9
- package/v2Containers/InApp/selectors.js +25 -5
- package/v2Containers/InApp/tests/index.test.js +154 -50
- package/v2Containers/InApp/tests/reducer.test.js +34 -0
- package/v2Containers/InApp/tests/sagas.test.js +61 -9
- package/v2Containers/InApp/tests/selectors.test.js +612 -0
- package/v2Containers/InAppWrapper/components/InAppWrapperView.js +162 -0
- package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +267 -0
- package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +9 -0
- package/v2Containers/InAppWrapper/constants.js +16 -0
- package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +473 -0
- package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +198 -0
- package/v2Containers/InAppWrapper/index.js +148 -0
- package/v2Containers/InAppWrapper/messages.js +49 -0
- package/v2Containers/InappAdvance/index.js +1006 -0
- package/v2Containers/InappAdvance/index.scss +10 -0
- package/v2Containers/InappAdvance/tests/index.test.js +448 -0
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
- package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +2 -0
- package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +9 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +12 -0
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4 -0
- package/v2Containers/TagList/index.js +65 -1
- package/v2Containers/Templates/_templates.scss +49 -1
- package/v2Containers/Templates/index.js +93 -5
- package/v2Containers/Templates/messages.js +4 -0
- package/v2Containers/Templates/reducer.js +20 -7
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +8 -88
- package/v2Containers/Templates/tests/reducer.test.js +125 -0
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +35 -0
|
@@ -76,7 +76,7 @@ describe('useInAppContent', () => {
|
|
|
76
76
|
|
|
77
77
|
it('initializes with custom content for Android', () => {
|
|
78
78
|
const customContent = {
|
|
79
|
-
[DEVICE_TYPES.ANDROID]: '<p>Custom Android</p>'
|
|
79
|
+
[DEVICE_TYPES.ANDROID]: '<p>Custom Android</p>',
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
render(<TestComponent initialContent={customContent} />);
|
|
@@ -86,7 +86,7 @@ describe('useInAppContent', () => {
|
|
|
86
86
|
|
|
87
87
|
it('initializes with custom content for iOS', () => {
|
|
88
88
|
const customContent = {
|
|
89
|
-
[DEVICE_TYPES.IOS]: '<p>Custom iOS</p>'
|
|
89
|
+
[DEVICE_TYPES.IOS]: '<p>Custom iOS</p>',
|
|
90
90
|
};
|
|
91
91
|
|
|
92
92
|
render(<TestComponent initialContent={customContent} />);
|
|
@@ -97,7 +97,7 @@ describe('useInAppContent', () => {
|
|
|
97
97
|
it('initializes with custom content for both devices', () => {
|
|
98
98
|
const customContent = {
|
|
99
99
|
[DEVICE_TYPES.ANDROID]: '<p>Android</p>',
|
|
100
|
-
[DEVICE_TYPES.IOS]: '<p>iOS</p>'
|
|
100
|
+
[DEVICE_TYPES.IOS]: '<p>iOS</p>',
|
|
101
101
|
};
|
|
102
102
|
|
|
103
103
|
render(<TestComponent initialContent={customContent} />);
|
|
@@ -112,10 +112,10 @@ describe('useInAppContent', () => {
|
|
|
112
112
|
expect(screen.getByTestId('active-device')).toHaveTextContent(DEVICE_TYPES.ANDROID);
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
it('has content by default', () => {
|
|
115
|
+
it('has no content by default (empty strings)', () => {
|
|
116
116
|
render(<TestComponent />);
|
|
117
117
|
|
|
118
|
-
expect(screen.getByTestId('has-content')).toHaveTextContent('
|
|
118
|
+
expect(screen.getByTestId('has-content')).toHaveTextContent('false');
|
|
119
119
|
});
|
|
120
120
|
});
|
|
121
121
|
|
|
@@ -152,7 +152,7 @@ describe('useInAppContent', () => {
|
|
|
152
152
|
it('updates current content when switching devices', () => {
|
|
153
153
|
const customContent = {
|
|
154
154
|
[DEVICE_TYPES.ANDROID]: '<p>Android Content</p>',
|
|
155
|
-
[DEVICE_TYPES.IOS]: '<p>iOS Content</p>'
|
|
155
|
+
[DEVICE_TYPES.IOS]: '<p>iOS Content</p>',
|
|
156
156
|
};
|
|
157
157
|
|
|
158
158
|
render(<TestComponent initialContent={customContent} />);
|
|
@@ -220,7 +220,7 @@ describe('useInAppContent', () => {
|
|
|
220
220
|
it('updates only current device when sync is disabled', () => {
|
|
221
221
|
const customContent = {
|
|
222
222
|
[DEVICE_TYPES.ANDROID]: '<p>Android</p>',
|
|
223
|
-
[DEVICE_TYPES.IOS]: '<p>iOS</p>'
|
|
223
|
+
[DEVICE_TYPES.IOS]: '<p>iOS</p>',
|
|
224
224
|
};
|
|
225
225
|
|
|
226
226
|
let inAppState;
|
|
@@ -254,7 +254,7 @@ describe('useInAppContent', () => {
|
|
|
254
254
|
expect(mockOnChange).toHaveBeenCalled();
|
|
255
255
|
expect(mockOnChange).toHaveBeenCalledWith(
|
|
256
256
|
expect.objectContaining({
|
|
257
|
-
[DEVICE_TYPES.ANDROID]: '<p>New</p>'
|
|
257
|
+
[DEVICE_TYPES.ANDROID]: '<p>New</p>',
|
|
258
258
|
}),
|
|
259
259
|
DEVICE_TYPES.ANDROID
|
|
260
260
|
);
|
|
@@ -294,7 +294,7 @@ describe('useInAppContent', () => {
|
|
|
294
294
|
it('syncs content to both devices when sync is enabled', () => {
|
|
295
295
|
const customContent = {
|
|
296
296
|
[DEVICE_TYPES.ANDROID]: '<p>Android</p>',
|
|
297
|
-
[DEVICE_TYPES.IOS]: '<p>iOS</p>'
|
|
297
|
+
[DEVICE_TYPES.IOS]: '<p>iOS</p>',
|
|
298
298
|
};
|
|
299
299
|
|
|
300
300
|
render(<TestComponent initialContent={customContent} />);
|
|
@@ -457,7 +457,7 @@ describe('useInAppContent', () => {
|
|
|
457
457
|
describe('Content Queries', () => {
|
|
458
458
|
it('reports hasContent as true for non-empty content', () => {
|
|
459
459
|
const customContent = {
|
|
460
|
-
[DEVICE_TYPES.ANDROID]: '<p>Content</p>'
|
|
460
|
+
[DEVICE_TYPES.ANDROID]: '<p>Content</p>',
|
|
461
461
|
};
|
|
462
462
|
|
|
463
463
|
render(<TestComponent initialContent={customContent} />);
|
|
@@ -468,7 +468,7 @@ describe('useInAppContent', () => {
|
|
|
468
468
|
it('reports hasContent as false for empty content', () => {
|
|
469
469
|
const customContent = {
|
|
470
470
|
[DEVICE_TYPES.ANDROID]: '',
|
|
471
|
-
[DEVICE_TYPES.IOS]: ''
|
|
471
|
+
[DEVICE_TYPES.IOS]: '',
|
|
472
472
|
};
|
|
473
473
|
|
|
474
474
|
let inAppState;
|
|
@@ -487,7 +487,7 @@ describe('useInAppContent', () => {
|
|
|
487
487
|
|
|
488
488
|
it('calculates content size correctly', () => {
|
|
489
489
|
const customContent = {
|
|
490
|
-
[DEVICE_TYPES.ANDROID]: '12345'
|
|
490
|
+
[DEVICE_TYPES.ANDROID]: '12345',
|
|
491
491
|
};
|
|
492
492
|
|
|
493
493
|
render(<TestComponent initialContent={customContent} />);
|
|
@@ -498,7 +498,7 @@ describe('useInAppContent', () => {
|
|
|
498
498
|
it('returns 0 size for empty content', () => {
|
|
499
499
|
const customContent = {
|
|
500
500
|
[DEVICE_TYPES.ANDROID]: '',
|
|
501
|
-
[DEVICE_TYPES.IOS]: ''
|
|
501
|
+
[DEVICE_TYPES.IOS]: '',
|
|
502
502
|
};
|
|
503
503
|
|
|
504
504
|
let inAppState;
|
|
@@ -518,7 +518,7 @@ describe('useInAppContent', () => {
|
|
|
518
518
|
it('getDeviceContent returns content for specific device', () => {
|
|
519
519
|
const customContent = {
|
|
520
520
|
[DEVICE_TYPES.ANDROID]: '<p>Android</p>',
|
|
521
|
-
[DEVICE_TYPES.IOS]: '<p>iOS</p>'
|
|
521
|
+
[DEVICE_TYPES.IOS]: '<p>iOS</p>',
|
|
522
522
|
};
|
|
523
523
|
|
|
524
524
|
let inAppState;
|
|
@@ -655,7 +655,7 @@ describe('useInAppContent', () => {
|
|
|
655
655
|
it('handles very large content', () => {
|
|
656
656
|
const largeContent = 'a'.repeat(100000);
|
|
657
657
|
const customContent = {
|
|
658
|
-
[DEVICE_TYPES.ANDROID]: largeContent
|
|
658
|
+
[DEVICE_TYPES.ANDROID]: largeContent,
|
|
659
659
|
};
|
|
660
660
|
|
|
661
661
|
let inAppState;
|
|
@@ -698,7 +698,7 @@ describe('useInAppContent', () => {
|
|
|
698
698
|
it('switches device and updates content', () => {
|
|
699
699
|
const customContent = {
|
|
700
700
|
[DEVICE_TYPES.ANDROID]: '<p>Android</p>',
|
|
701
|
-
[DEVICE_TYPES.IOS]: '<p>iOS</p>'
|
|
701
|
+
[DEVICE_TYPES.IOS]: '<p>iOS</p>',
|
|
702
702
|
};
|
|
703
703
|
|
|
704
704
|
let inAppState;
|
|
@@ -781,5 +781,146 @@ describe('useInAppContent', () => {
|
|
|
781
781
|
expect(mockOnSave).toHaveBeenCalledTimes(1);
|
|
782
782
|
});
|
|
783
783
|
});
|
|
784
|
-
});
|
|
785
784
|
|
|
785
|
+
describe('setDeviceContent Coverage', () => {
|
|
786
|
+
it('sets content for specific device when sync is disabled', () => {
|
|
787
|
+
let inAppState;
|
|
788
|
+
render(<TestComponent onStateChange={(state) => { inAppState = state; }} />);
|
|
789
|
+
|
|
790
|
+
act(() => {
|
|
791
|
+
inAppState.setDeviceContent(DEVICE_TYPES.ANDROID, '<p>Android Only</p>');
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
expect(screen.getByTestId('android-content')).toHaveTextContent('<p>Android Only</p>');
|
|
795
|
+
expect(screen.getByTestId('ios-content')).toHaveTextContent('');
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it('sets content for both devices when sync is enabled', () => {
|
|
799
|
+
let inAppState;
|
|
800
|
+
render(<TestComponent onStateChange={(state) => { inAppState = state; }} />);
|
|
801
|
+
|
|
802
|
+
// Enable sync first
|
|
803
|
+
act(() => {
|
|
804
|
+
inAppState.toggleContentSync(true);
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
// Then set content
|
|
808
|
+
act(() => {
|
|
809
|
+
inAppState.setDeviceContent(DEVICE_TYPES.ANDROID, '<p>Synced Content</p>');
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
expect(screen.getByTestId('android-content')).toHaveTextContent('<p>Synced Content</p>');
|
|
813
|
+
expect(screen.getByTestId('ios-content')).toHaveTextContent('<p>Synced Content</p>');
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
it('warns when setting content for invalid device', () => {
|
|
817
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
818
|
+
let inAppState;
|
|
819
|
+
render(<TestComponent onStateChange={(state) => { inAppState = state; }} />);
|
|
820
|
+
|
|
821
|
+
act(() => {
|
|
822
|
+
inAppState.setDeviceContent('InvalidDevice', '<p>Content</p>');
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
826
|
+
expect.stringContaining('Invalid device type')
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
consoleSpy.mockRestore();
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
it('warns when setting non-string content', () => {
|
|
833
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
834
|
+
let inAppState;
|
|
835
|
+
render(<TestComponent onStateChange={(state) => { inAppState = state; }} />);
|
|
836
|
+
|
|
837
|
+
act(() => {
|
|
838
|
+
inAppState.setDeviceContent(DEVICE_TYPES.ANDROID, 123);
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
842
|
+
expect.stringContaining('content must be a string')
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
consoleSpy.mockRestore();
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
describe('Edge Cases Coverage', () => {
|
|
850
|
+
it('handles updateContent with non-string input', () => {
|
|
851
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
852
|
+
let inAppState;
|
|
853
|
+
render(<TestComponent onStateChange={(state) => { inAppState = state; }} />);
|
|
854
|
+
|
|
855
|
+
act(() => {
|
|
856
|
+
inAppState.updateContent(123);
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
860
|
+
expect.stringContaining('newContent must be a string')
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
consoleSpy.mockRestore();
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
it('handles switchDevice with invalid device type', () => {
|
|
867
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
868
|
+
let inAppState;
|
|
869
|
+
render(<TestComponent onStateChange={(state) => { inAppState = state; }} />);
|
|
870
|
+
|
|
871
|
+
act(() => {
|
|
872
|
+
inAppState.switchDevice('InvalidDevice');
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
876
|
+
expect.stringContaining('Invalid device type')
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
consoleSpy.mockRestore();
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it('handles auto-save with content length below minimum', () => {
|
|
883
|
+
const onSave = jest.fn();
|
|
884
|
+
let inAppState;
|
|
885
|
+
render(
|
|
886
|
+
<TestComponent
|
|
887
|
+
options={{ autoSave: true, autoSaveInterval: 100, onSave }}
|
|
888
|
+
onStateChange={(state) => { inAppState = state; }}
|
|
889
|
+
/>
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
act(() => {
|
|
893
|
+
inAppState.updateContent(''); // Empty content
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
act(() => {
|
|
897
|
+
jest.advanceTimersByTime(200);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// onSave should not be called for empty content
|
|
901
|
+
expect(onSave).not.toHaveBeenCalled();
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it('handles auto-save interval below minimum', () => {
|
|
905
|
+
const onSave = jest.fn();
|
|
906
|
+
let inAppState;
|
|
907
|
+
render(
|
|
908
|
+
<TestComponent
|
|
909
|
+
options={{ autoSave: true, autoSaveInterval: 500, onSave }} // Below 1000ms minimum
|
|
910
|
+
onStateChange={(state) => { inAppState = state; }}
|
|
911
|
+
/>
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
act(() => {
|
|
915
|
+
inAppState.updateContent('<p>Content</p>');
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
act(() => {
|
|
919
|
+
jest.advanceTimersByTime(1000);
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// onSave should not be called when interval is below minimum
|
|
923
|
+
expect(onSave).not.toHaveBeenCalled();
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
});
|
|
@@ -4,133 +4,30 @@
|
|
|
4
4
|
* Manages separate HTML content for Android and iOS devices with sync functionality
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
useState, useCallback, useRef, useEffect, useMemo,
|
|
9
|
+
} from 'react';
|
|
8
10
|
import { DEVICE_TYPES, PERFORMANCE } from '../constants';
|
|
9
11
|
|
|
10
12
|
// Constants for better maintainability
|
|
11
13
|
const CONTENT_VALIDATION = {
|
|
12
14
|
MIN_CONTENT_LENGTH: 0,
|
|
13
|
-
DEFAULT_CONTENT_TYPE: 'string'
|
|
15
|
+
DEFAULT_CONTENT_TYPE: 'string',
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
const AUTO_SAVE_CONFIG = {
|
|
17
19
|
DEFAULT_ENABLED: true,
|
|
18
20
|
DEFAULT_INTERVAL: PERFORMANCE.AUTO_SAVE_INTERVAL,
|
|
19
|
-
MIN_AUTO_SAVE_INTERVAL_MS: 1000 // Minimum 1 second between auto-saves
|
|
21
|
+
MIN_AUTO_SAVE_INTERVAL_MS: 1000, // Minimum 1 second between auto-saves
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Default InApp content for different devices
|
|
26
|
+
* Empty strings - no default content for new templates
|
|
24
27
|
*/
|
|
25
28
|
const DEFAULT_INAPP_CONTENT = {
|
|
26
|
-
[DEVICE_TYPES.ANDROID]:
|
|
27
|
-
|
|
28
|
-
<head>
|
|
29
|
-
<meta charset="UTF-8">
|
|
30
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
31
|
-
<title>In-App Notification</title>
|
|
32
|
-
<style>
|
|
33
|
-
body {
|
|
34
|
-
margin: 0;
|
|
35
|
-
padding: 16px;
|
|
36
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif;
|
|
37
|
-
background-color: #ffffff;
|
|
38
|
-
color: #212121;
|
|
39
|
-
}
|
|
40
|
-
.notification {
|
|
41
|
-
max-width: 100%;
|
|
42
|
-
background: white;
|
|
43
|
-
border-radius: 8px;
|
|
44
|
-
padding: 16px;
|
|
45
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
46
|
-
}
|
|
47
|
-
.title {
|
|
48
|
-
font-size: 16px;
|
|
49
|
-
font-weight: 500;
|
|
50
|
-
margin: 0 0 8px 0;
|
|
51
|
-
color: #212121;
|
|
52
|
-
}
|
|
53
|
-
.message {
|
|
54
|
-
font-size: 14px;
|
|
55
|
-
line-height: 1.4;
|
|
56
|
-
margin: 0 0 16px 0;
|
|
57
|
-
color: #424242;
|
|
58
|
-
}
|
|
59
|
-
.cta-button {
|
|
60
|
-
background-color: #42b040;
|
|
61
|
-
color: white;
|
|
62
|
-
border: none;
|
|
63
|
-
border-radius: 4px;
|
|
64
|
-
padding: 8px 16px;
|
|
65
|
-
font-size: 12px;
|
|
66
|
-
font-weight: 500;
|
|
67
|
-
cursor: pointer;
|
|
68
|
-
width: 100%;
|
|
69
|
-
}
|
|
70
|
-
</style>
|
|
71
|
-
</head>
|
|
72
|
-
<body>
|
|
73
|
-
<div class="notification">
|
|
74
|
-
<h2 class="title">Sample template</h2>
|
|
75
|
-
<p class="message">This is a sample template for in-app notification content. This can be triggered on any behavioural event while the user is on the app</p>
|
|
76
|
-
<button class="cta-button">Add to cart</button>
|
|
77
|
-
</div>
|
|
78
|
-
</body>
|
|
79
|
-
</html>`,
|
|
80
|
-
[DEVICE_TYPES.IOS]: `<!DOCTYPE html>
|
|
81
|
-
<html lang="en">
|
|
82
|
-
<head>
|
|
83
|
-
<meta charset="UTF-8">
|
|
84
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
85
|
-
<title>In-App Notification</title>
|
|
86
|
-
<style>
|
|
87
|
-
body {
|
|
88
|
-
margin: 0;
|
|
89
|
-
padding: 16px;
|
|
90
|
-
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;
|
|
91
|
-
background-color: #ffffff;
|
|
92
|
-
color: #000000;
|
|
93
|
-
}
|
|
94
|
-
.notification {
|
|
95
|
-
max-width: 100%;
|
|
96
|
-
background: white;
|
|
97
|
-
border-radius: 12px;
|
|
98
|
-
padding: 16px;
|
|
99
|
-
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
|
100
|
-
}
|
|
101
|
-
.title {
|
|
102
|
-
font-size: 17px;
|
|
103
|
-
font-weight: 600;
|
|
104
|
-
margin: 0 0 8px 0;
|
|
105
|
-
color: #000000;
|
|
106
|
-
}
|
|
107
|
-
.message {
|
|
108
|
-
font-size: 15px;
|
|
109
|
-
line-height: 1.4;
|
|
110
|
-
margin: 0 0 16px 0;
|
|
111
|
-
color: #3c3c43;
|
|
112
|
-
}
|
|
113
|
-
.cta-button {
|
|
114
|
-
background-color: #007AFF;
|
|
115
|
-
color: white;
|
|
116
|
-
border: none;
|
|
117
|
-
border-radius: 8px;
|
|
118
|
-
padding: 12px 16px;
|
|
119
|
-
font-size: 16px;
|
|
120
|
-
font-weight: 600;
|
|
121
|
-
cursor: pointer;
|
|
122
|
-
width: 100%;
|
|
123
|
-
}
|
|
124
|
-
</style>
|
|
125
|
-
</head>
|
|
126
|
-
<body>
|
|
127
|
-
<div class="notification">
|
|
128
|
-
<h2 class="title">Sample template</h2>
|
|
129
|
-
<p class="message">This is a sample template for in-app notification content. This can be triggered on any behavioural event while the user is on the app</p>
|
|
130
|
-
<button class="cta-button">Add to cart</button>
|
|
131
|
-
</div>
|
|
132
|
-
</body>
|
|
133
|
-
</html>`
|
|
29
|
+
[DEVICE_TYPES.ANDROID]: '',
|
|
30
|
+
[DEVICE_TYPES.IOS]: '',
|
|
134
31
|
};
|
|
135
32
|
|
|
136
33
|
/**
|
|
@@ -150,7 +47,7 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
150
47
|
autoSave = AUTO_SAVE_CONFIG.DEFAULT_ENABLED,
|
|
151
48
|
autoSaveInterval = AUTO_SAVE_CONFIG.DEFAULT_INTERVAL,
|
|
152
49
|
onSave,
|
|
153
|
-
onChange
|
|
50
|
+
onChange,
|
|
154
51
|
} = options;
|
|
155
52
|
|
|
156
53
|
// Destructure device types for cleaner code
|
|
@@ -159,7 +56,7 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
159
56
|
// Device-specific content state with optional chaining
|
|
160
57
|
const [deviceContent, setDeviceContent] = useState(() => ({
|
|
161
58
|
[ANDROID]: initialContent?.[ANDROID] || DEFAULT_INAPP_CONTENT[ANDROID],
|
|
162
|
-
[IOS]: initialContent?.[IOS] || DEFAULT_INAPP_CONTENT[IOS]
|
|
59
|
+
[IOS]: initialContent?.[IOS] || DEFAULT_INAPP_CONTENT[IOS],
|
|
163
60
|
}));
|
|
164
61
|
|
|
165
62
|
// Current active device
|
|
@@ -189,15 +86,36 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
189
86
|
deviceContentRef.current = deviceContent;
|
|
190
87
|
}, [deviceContent]);
|
|
191
88
|
|
|
89
|
+
// Update content when initialContent prop changes (for edit mode)
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const newAndroidContent = initialContent?.[ANDROID];
|
|
92
|
+
const newIosContent = initialContent?.[IOS];
|
|
93
|
+
|
|
94
|
+
// Only update if initialContent has changed and is different from current content
|
|
95
|
+
setDeviceContent((prev) => {
|
|
96
|
+
let updated = false;
|
|
97
|
+
const updatedContent = { ...prev };
|
|
98
|
+
|
|
99
|
+
if (newAndroidContent !== undefined && newAndroidContent !== prev[ANDROID]) {
|
|
100
|
+
updatedContent[ANDROID] = newAndroidContent;
|
|
101
|
+
updated = true;
|
|
102
|
+
}
|
|
103
|
+
if (newIosContent !== undefined && newIosContent !== prev[IOS]) {
|
|
104
|
+
updatedContent[IOS] = newIosContent;
|
|
105
|
+
updated = true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return updated ? updatedContent : prev;
|
|
109
|
+
});
|
|
110
|
+
}, [initialContent, ANDROID, IOS]);
|
|
111
|
+
|
|
192
112
|
// Get current active content
|
|
193
|
-
const currentContent = useMemo(() =>
|
|
194
|
-
return deviceContent[activeDevice] || '';
|
|
195
|
-
}, [deviceContent, activeDevice]);
|
|
113
|
+
const currentContent = useMemo(() => deviceContent?.[activeDevice] || '', [deviceContent, activeDevice]);
|
|
196
114
|
|
|
197
115
|
// Update content for current device
|
|
198
116
|
const updateContent = useCallback((newContent) => {
|
|
199
117
|
// Validate input
|
|
200
|
-
if (typeof newContent !== CONTENT_VALIDATION
|
|
118
|
+
if (typeof newContent !== CONTENT_VALIDATION?.DEFAULT_CONTENT_TYPE) {
|
|
201
119
|
console.warn('useInAppContent: newContent must be a string');
|
|
202
120
|
return;
|
|
203
121
|
}
|
|
@@ -209,14 +127,14 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
209
127
|
// When sync is enabled, update both devices with the same content
|
|
210
128
|
updatedDeviceContent = {
|
|
211
129
|
[ANDROID]: newContent,
|
|
212
|
-
[IOS]: newContent
|
|
130
|
+
[IOS]: newContent,
|
|
213
131
|
};
|
|
214
132
|
} else {
|
|
215
133
|
// When sync is disabled, update only the current device
|
|
216
|
-
setDeviceContent(prev => {
|
|
134
|
+
setDeviceContent((prev) => {
|
|
217
135
|
updatedDeviceContent = {
|
|
218
136
|
...prev,
|
|
219
|
-
[activeDevice]: newContent
|
|
137
|
+
[activeDevice]: newContent,
|
|
220
138
|
};
|
|
221
139
|
return updatedDeviceContent;
|
|
222
140
|
});
|
|
@@ -301,7 +219,7 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
301
219
|
const currentActiveContent = deviceContent[activeDevice];
|
|
302
220
|
const syncedContent = {
|
|
303
221
|
[ANDROID]: currentActiveContent,
|
|
304
|
-
[IOS]: currentActiveContent
|
|
222
|
+
[IOS]: currentActiveContent,
|
|
305
223
|
};
|
|
306
224
|
|
|
307
225
|
setDeviceContent(syncedContent);
|
|
@@ -325,22 +243,16 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
325
243
|
}, [deviceContent, onSave]);
|
|
326
244
|
|
|
327
245
|
// Check if content exists for current device
|
|
328
|
-
const hasContent = useMemo(() =>
|
|
329
|
-
|
|
330
|
-
currentContent.trim().length > CONTENT_VALIDATION.MIN_CONTENT_LENGTH;
|
|
331
|
-
}, [currentContent]);
|
|
246
|
+
const hasContent = useMemo(() => typeof currentContent === CONTENT_VALIDATION.DEFAULT_CONTENT_TYPE
|
|
247
|
+
&& currentContent?.trim()?.length > CONTENT_VALIDATION.MIN_CONTENT_LENGTH, [currentContent]);
|
|
332
248
|
|
|
333
249
|
// Get content size for current device
|
|
334
|
-
const getContentSize = useCallback(() =>
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
: CONTENT_VALIDATION.MIN_CONTENT_LENGTH;
|
|
338
|
-
}, [currentContent]);
|
|
250
|
+
const getContentSize = useCallback(() => typeof currentContent === CONTENT_VALIDATION.DEFAULT_CONTENT_TYPE
|
|
251
|
+
? currentContent?.length
|
|
252
|
+
: CONTENT_VALIDATION?.MIN_CONTENT_LENGTH, [currentContent]);
|
|
339
253
|
|
|
340
254
|
// Get content for specific device
|
|
341
|
-
const getDeviceContent = useCallback((device) =>
|
|
342
|
-
return deviceContent[device] || '';
|
|
343
|
-
}, [deviceContent]);
|
|
255
|
+
const getDeviceContent = useCallback((device) => deviceContent[device] || '', [deviceContent]);
|
|
344
256
|
|
|
345
257
|
// Set content for specific device
|
|
346
258
|
const setDeviceContent_ = useCallback((device, content) => {
|
|
@@ -360,25 +272,23 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
360
272
|
// Update both devices when sync is enabled
|
|
361
273
|
setDeviceContent({
|
|
362
274
|
[ANDROID]: content,
|
|
363
|
-
[IOS]: content
|
|
275
|
+
[IOS]: content,
|
|
364
276
|
});
|
|
365
277
|
} else {
|
|
366
278
|
// Update specific device
|
|
367
|
-
setDeviceContent(prev => ({
|
|
279
|
+
setDeviceContent((prev) => ({
|
|
368
280
|
...prev,
|
|
369
|
-
[device]: content
|
|
281
|
+
[device]: content,
|
|
370
282
|
}));
|
|
371
283
|
}
|
|
372
284
|
setIsDirty(true);
|
|
373
285
|
}, [keepContentSame, ANDROID, IOS]);
|
|
374
286
|
|
|
375
287
|
// Cleanup on unmount
|
|
376
|
-
useEffect(() => {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
};
|
|
288
|
+
useEffect(() => () => {
|
|
289
|
+
if (autoSaveTimerRef.current) {
|
|
290
|
+
clearTimeout(autoSaveTimerRef.current);
|
|
291
|
+
}
|
|
382
292
|
}, []);
|
|
383
293
|
|
|
384
294
|
return {
|
|
@@ -402,6 +312,6 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
402
312
|
toggleContentSync,
|
|
403
313
|
|
|
404
314
|
// Save management
|
|
405
|
-
markAsSaved
|
|
315
|
+
markAsSaved,
|
|
406
316
|
};
|
|
407
317
|
};
|