@capillarytech/creatives-library 9.1.6 → 9.1.7-alpha.sprint-bee-free-load-fix.32

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/AppRoot.js CHANGED
@@ -14,6 +14,7 @@ import { translationMessages } from './i18n';
14
14
  import initialState from './initialState';
15
15
  import pathConfig from './config/path';
16
16
  import { isMFEMode } from './utils/mfeDetect';
17
+ import { loadBeePlugin } from './utils/beePluginLoader';
17
18
  import { emitFirstPaintReadyIfNotDataLanding } from './utils/mfeFirstPaintReady';
18
19
  import rebaseHistory from './utils/mfeHistory';
19
20
  import { MFEEventBus } from '@capillarytech/cap-ui-utils';
@@ -86,14 +87,10 @@ const AppRoot = ({ basename: _basename = '/creatives/ui', history: hostHistory }
86
87
  }
87
88
  }, []);
88
89
 
89
- // In MFE mode the HOST's index.html is served, not creatives' — so BeePlugin.js
90
- // (declared in creatives/index.html) is never loaded. Inject it here so BeeEditor works.
90
+ // Preload BeePlugin.js in MFE mode (host index.html lacks it); the editors also load it on demand
91
91
  useEffect(() => {
92
- if (isMFEMode() && !window.BeePlugin && !document.querySelector('script[src*="BeePlugin.js"]')) {
93
- const script = document.createElement('script');
94
- script.src = 'https://app-rsrc.getbee.io/plugin/BeePlugin.js';
95
- script.type = 'text/javascript';
96
- document.head.appendChild(script);
92
+ if (isMFEMode()) {
93
+ loadBeePlugin().catch(() => {});
97
94
  }
98
95
  }, []);
99
96
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.1.6",
4
+ "version": "9.1.7-alpha.sprint-bee-free-load-fix.32",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -0,0 +1,48 @@
1
+ export const BEE_PLUGIN_SCRIPT_URL = 'https://app-rsrc.getbee.io/plugin/BeePlugin.js';
2
+ export const BEE_PLUGIN_LOAD_TIMEOUT_MS = 30000;
3
+
4
+ let beePluginPromise = null;
5
+
6
+ // Hosts consuming creatives as a library or MFE may not ship BeePlugin.js, so load it on demand
7
+ export const loadBeePlugin = () => {
8
+ if (window.BeePlugin) {
9
+ return Promise.resolve(window.BeePlugin);
10
+ }
11
+ if (beePluginPromise) {
12
+ return beePluginPromise;
13
+ }
14
+ beePluginPromise = new Promise((resolve, reject) => {
15
+ const existingScript = document.querySelector('script[src*="BeePlugin.js"]');
16
+ const script = existingScript || document.createElement('script');
17
+ let timeoutId = null;
18
+ const fail = (error) => {
19
+ clearTimeout(timeoutId);
20
+ beePluginPromise = null;
21
+ // Drop the tag so a retry injects a fresh one instead of waiting on a dead script
22
+ script.remove();
23
+ reject(error);
24
+ };
25
+ const onLoad = () => {
26
+ if (window.BeePlugin) {
27
+ clearTimeout(timeoutId);
28
+ resolve(window.BeePlugin);
29
+ return;
30
+ }
31
+ fail(new Error('BeePlugin.js loaded but window.BeePlugin is undefined'));
32
+ };
33
+ const onError = () => fail(new Error(`Failed to load ${BEE_PLUGIN_SCRIPT_URL}`));
34
+ timeoutId = setTimeout(
35
+ () => fail(new Error(`Timed out loading ${BEE_PLUGIN_SCRIPT_URL}`)),
36
+ BEE_PLUGIN_LOAD_TIMEOUT_MS,
37
+ );
38
+ script.addEventListener('load', onLoad, { once: true });
39
+ script.addEventListener('error', onError, { once: true });
40
+ if (!existingScript) {
41
+ script.src = BEE_PLUGIN_SCRIPT_URL;
42
+ script.type = 'text/javascript';
43
+ script.async = true;
44
+ document.head.appendChild(script);
45
+ }
46
+ });
47
+ return beePluginPromise;
48
+ };
@@ -0,0 +1,85 @@
1
+ const SCRIPT_SELECTOR = 'script[src*="BeePlugin.js"]';
2
+
3
+ const getLoader = () => {
4
+ let loader;
5
+ jest.isolateModules(() => {
6
+ loader = require('../beePluginLoader');
7
+ });
8
+ return loader;
9
+ };
10
+
11
+ describe('loadBeePlugin', () => {
12
+ const mockBeePlugin = { create: jest.fn() };
13
+
14
+ beforeEach(() => {
15
+ delete window.BeePlugin;
16
+ document.head.innerHTML = '';
17
+ });
18
+
19
+ afterEach(() => {
20
+ jest.useRealTimers();
21
+ delete window.BeePlugin;
22
+ });
23
+
24
+ it('resolves immediately without injecting a script when BeePlugin is already loaded', async () => {
25
+ window.BeePlugin = mockBeePlugin;
26
+ const { loadBeePlugin } = getLoader();
27
+ await expect(loadBeePlugin()).resolves.toBe(mockBeePlugin);
28
+ expect(document.querySelector(SCRIPT_SELECTOR)).toBeNull();
29
+ });
30
+
31
+ it('injects the script once and resolves when it loads', async () => {
32
+ const { loadBeePlugin, BEE_PLUGIN_SCRIPT_URL } = getLoader();
33
+ const firstLoad = loadBeePlugin();
34
+ const secondLoad = loadBeePlugin();
35
+ const scripts = document.querySelectorAll(SCRIPT_SELECTOR);
36
+ expect(scripts).toHaveLength(1);
37
+ expect(scripts[0].src).toBe(BEE_PLUGIN_SCRIPT_URL);
38
+ expect(secondLoad).toBe(firstLoad);
39
+ window.BeePlugin = mockBeePlugin;
40
+ scripts[0].dispatchEvent(new Event('load'));
41
+ await expect(firstLoad).resolves.toBe(mockBeePlugin);
42
+ });
43
+
44
+ it('waits on a host-provided script instead of injecting another', async () => {
45
+ const hostScript = document.createElement('script');
46
+ hostScript.src = 'https://app-rsrc.getbee.io/plugin/BeePlugin.js';
47
+ document.head.appendChild(hostScript);
48
+ const { loadBeePlugin } = getLoader();
49
+ const load = loadBeePlugin();
50
+ expect(document.querySelectorAll(SCRIPT_SELECTOR)).toHaveLength(1);
51
+ window.BeePlugin = mockBeePlugin;
52
+ hostScript.dispatchEvent(new Event('load'));
53
+ await expect(load).resolves.toBe(mockBeePlugin);
54
+ });
55
+
56
+ it('rejects on script error, removes the tag and allows a retry', async () => {
57
+ const { loadBeePlugin } = getLoader();
58
+ const failedLoad = loadBeePlugin();
59
+ document.querySelector(SCRIPT_SELECTOR).dispatchEvent(new Event('error'));
60
+ await expect(failedLoad).rejects.toThrow('Failed to load');
61
+ expect(document.querySelector(SCRIPT_SELECTOR)).toBeNull();
62
+
63
+ const retryLoad = loadBeePlugin();
64
+ expect(retryLoad).not.toBe(failedLoad);
65
+ window.BeePlugin = mockBeePlugin;
66
+ document.querySelector(SCRIPT_SELECTOR).dispatchEvent(new Event('load'));
67
+ await expect(retryLoad).resolves.toBe(mockBeePlugin);
68
+ });
69
+
70
+ it('rejects when the script loads but does not define BeePlugin', async () => {
71
+ const { loadBeePlugin } = getLoader();
72
+ const load = loadBeePlugin();
73
+ document.querySelector(SCRIPT_SELECTOR).dispatchEvent(new Event('load'));
74
+ await expect(load).rejects.toThrow('window.BeePlugin is undefined');
75
+ });
76
+
77
+ it('rejects after the timeout when the script never settles', async () => {
78
+ jest.useFakeTimers();
79
+ const { loadBeePlugin, BEE_PLUGIN_LOAD_TIMEOUT_MS } = getLoader();
80
+ const load = loadBeePlugin();
81
+ jest.advanceTimersByTime(BEE_PLUGIN_LOAD_TIMEOUT_MS);
82
+ await expect(load).rejects.toThrow('Timed out');
83
+ expect(document.querySelector(SCRIPT_SELECTOR)).toBeNull();
84
+ });
85
+ });
@@ -19,7 +19,9 @@ import CapInput from "@capillarytech/cap-ui-library/CapInput";
19
19
  import CapSelect from "@capillarytech/cap-ui-library/CapSelect";
20
20
  import CapSpin from "@capillarytech/cap-ui-library/CapSpin";
21
21
  import CapNotification from "@capillarytech/cap-ui-library/CapNotification";
22
+ import CapError from "@capillarytech/cap-ui-library/CapError";
22
23
  import config from '../../config/app';
24
+ import { loadBeePlugin } from '../../utils/beePluginLoader';
23
25
  import makeSelectBEE from './selectors';
24
26
  import messages from './messages';
25
27
  import * as beeActions from './actions';
@@ -75,6 +77,9 @@ function BeeEditor(props) {
75
77
  const [rowMetaInfo, setRowMetaInfo] = useState({});
76
78
  const [rowName, setRowName] = useState('');
77
79
  const [rowCategory, setRowCategory] = useState('');
80
+ const [isBeePluginLoading, setIsBeePluginLoading] = useState(false);
81
+ const [hasBeePluginLoadError, setHasBeePluginLoadError] = useState(false);
82
+ const [beePluginLoadAttempt, setBeePluginLoadAttempt] = useState(0);
78
83
  const beePluginInstanceRef = useRef(null);
79
84
  const initializedBeeJsonRef = useRef(null); // Track which beeJson we've initialized for
80
85
  const isInitializingRef = useRef(false); // Track if initialization is in progress
@@ -258,23 +263,37 @@ function BeeEditor(props) {
258
263
  beePluginInstanceRef.current = null;
259
264
  }
260
265
 
261
- try {
262
- window.BeePlugin.create(currentTokenData, beeConfig, (instance) => {
263
- beePluginInstance = instance;
264
- beePluginInstanceRef.current = instance;
265
- const parseJson = typeof beeJson === 'string' ? JSON.parse(beeJson) : beeJson;
266
- beePluginInstance.start(parseJson);
267
- saveBeeInstance(beePluginInstance);
268
- // Mark this beeJson as initialized and clear initialization flag
269
- initializedBeeJsonRef.current = initializationKey;
266
+ let isEffectCancelled = false;
267
+ setHasBeePluginLoadError(false);
268
+ setIsBeePluginLoading(!window.BeePlugin);
269
+ loadBeePlugin()
270
+ .then((BeePlugin) => {
271
+ if (isEffectCancelled) {
272
+ return;
273
+ }
274
+ setIsBeePluginLoading(false);
275
+ BeePlugin.create(currentTokenData, beeConfig, (instance) => {
276
+ beePluginInstance = instance;
277
+ beePluginInstanceRef.current = instance;
278
+ const parseJson = typeof beeJson === 'string' ? JSON.parse(beeJson) : beeJson;
279
+ beePluginInstance.start(parseJson);
280
+ saveBeeInstance(beePluginInstance);
281
+ // Mark this beeJson as initialized and clear initialization flag
282
+ initializedBeeJsonRef.current = initializationKey;
283
+ isInitializingRef.current = false;
284
+ });
285
+ })
286
+ .catch((error) => {
287
+ if (isEffectCancelled) {
288
+ return;
289
+ }
290
+ console.error('[BeeEditor] Error creating BEE plugin:', error);
291
+ setIsBeePluginLoading(false);
292
+ setHasBeePluginLoadError(true);
270
293
  isInitializingRef.current = false;
271
294
  });
272
- } catch (error) {
273
- // Reset flag on error
274
- console.error('[BeeEditor] Error creating BEE plugin:', error);
275
- isInitializingRef.current = false;
276
- }
277
295
  return () => {
296
+ isEffectCancelled = true;
278
297
  clearInterval(intervalTimer);
279
298
  // Reset initialization flag on cleanup
280
299
  isInitializingRef.current = false;
@@ -293,7 +312,12 @@ function BeeEditor(props) {
293
312
  };
294
313
  // Only depend on beeJson, uid, and id - not tokenData
295
314
  // tokenData is accessed via ref to prevent re-initialization when it changes
296
- }, [beeJson, uid, id]);
315
+ }, [beeJson, uid, id, beePluginLoadAttempt]);
316
+
317
+ const onRetryBeePluginLoad = () => {
318
+ setBeePluginLoadAttempt((attempt) => attempt + 1);
319
+ };
320
+
297
321
  const callbackSaveRow = (status, errorMsg) => {
298
322
  if (status === 'success') {
299
323
  CapNotification.success({message: formatMessage(messages.rowCreationSuccessMgs), key: 'create-custom-row-success'});
@@ -367,8 +391,17 @@ function BeeEditor(props) {
367
391
  </>
368
392
  );
369
393
  return (
370
- <CapSpin spinning={saveRowRequest || false}>
371
- <div id="bee-plugin-container" style={{ height: "650px" }}></div>
394
+ <CapSpin spinning={saveRowRequest || isBeePluginLoading}>
395
+ {hasBeePluginLoadError ? (
396
+ <div className="bee-plugin-load-error">
397
+ <CapError type="error">{formatMessage(messages.beePluginLoadError)}</CapError>
398
+ <CapButton type="secondary" onClick={onRetryBeePluginLoad}>
399
+ {formatMessage(messages.retry)}
400
+ </CapButton>
401
+ </div>
402
+ ) : (
403
+ <div id="bee-plugin-container" style={{ height: "650px" }}></div>
404
+ )}
372
405
  <TagList
373
406
  moduleFilterEnabled={moduleFilterEnabled}
374
407
  label={label}
@@ -1,3 +1,11 @@
1
+ .bee-plugin-load-error {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: flex-start;
5
+ gap: 1rem;
6
+ padding: 1.5rem 0;
7
+ }
8
+
1
9
  .custom-row-modal {
2
10
  .ant-modal-footer {
3
11
  text-align: left;
@@ -64,4 +64,12 @@ export default defineMessages({
64
64
  id: `${prefix}.rowCreationFailureMgs`,
65
65
  defaultMessage: 'Error in row creation',
66
66
  },
67
+ beePluginLoadError: {
68
+ id: `${prefix}.beePluginLoadError`,
69
+ defaultMessage: 'Unable to load the drag & drop editor. Check your network connection and try again.',
70
+ },
71
+ retry: {
72
+ id: `${prefix}.retry`,
73
+ defaultMessage: 'Retry',
74
+ },
67
75
  });
@@ -0,0 +1,71 @@
1
+ import React from 'react';
2
+ import '@testing-library/jest-dom';
3
+ import { render, screen, fireEvent, waitFor } from '../../../utils/test-utils';
4
+ import { loadBeePlugin } from '../../../utils/beePluginLoader';
5
+ import BeeEditor from '../index';
6
+
7
+ jest.mock('../../../utils/beePluginLoader', () => ({
8
+ loadBeePlugin: jest.fn(),
9
+ }));
10
+
11
+ jest.mock('../../TagList', () => () => null);
12
+
13
+ const mockBeePluginInstance = { start: jest.fn(), destroy: jest.fn() };
14
+ const mockBeePlugin = { create: jest.fn() };
15
+
16
+ const defaultProps = {
17
+ uid: 'test-uid',
18
+ id: 'bee-editor',
19
+ beeJson: '{"page":{}}',
20
+ tokenData: { access_token: 'token' },
21
+ saveBeeInstance: jest.fn(),
22
+ saveBeeData: jest.fn(),
23
+ tags: [],
24
+ };
25
+
26
+ describe('BeeEditor BeePlugin loading', () => {
27
+ beforeEach(() => {
28
+ mockBeePlugin.create.mockImplementation((tokenData, beeConfig, onCreated) => onCreated(mockBeePluginInstance));
29
+ jest.spyOn(console, 'error').mockImplementation(() => {});
30
+ });
31
+
32
+ afterEach(() => {
33
+ console.error.mockRestore();
34
+ });
35
+
36
+ it('creates the BEE editor once the plugin script has loaded', async () => {
37
+ loadBeePlugin.mockResolvedValue(mockBeePlugin);
38
+ const { container } = render(<BeeEditor {...defaultProps} />);
39
+ await waitFor(() => expect(mockBeePlugin.create).toHaveBeenCalledTimes(1));
40
+ expect(mockBeePluginInstance.start).toHaveBeenCalledWith({ page: {} });
41
+ expect(defaultProps.saveBeeInstance).toHaveBeenCalledWith(mockBeePluginInstance);
42
+ expect(container.querySelector('#bee-plugin-container')).toBeInTheDocument();
43
+ });
44
+
45
+ it('shows an error with retry when the plugin script fails, and recovers on retry', async () => {
46
+ loadBeePlugin
47
+ .mockRejectedValueOnce(new Error('Failed to load'))
48
+ .mockResolvedValueOnce(mockBeePlugin);
49
+ const { container } = render(<BeeEditor {...defaultProps} />);
50
+
51
+ expect(await screen.findByText(/Unable to load the drag & drop editor/)).toBeInTheDocument();
52
+ expect(container.querySelector('#bee-plugin-container')).not.toBeInTheDocument();
53
+ expect(mockBeePlugin.create).not.toHaveBeenCalled();
54
+
55
+ fireEvent.click(screen.getByRole('button', { name: 'Retry' }));
56
+
57
+ await waitFor(() => expect(mockBeePlugin.create).toHaveBeenCalledTimes(1));
58
+ expect(screen.queryByText(/Unable to load the drag & drop editor/)).not.toBeInTheDocument();
59
+ expect(container.querySelector('#bee-plugin-container')).toBeInTheDocument();
60
+ });
61
+
62
+ it('does not create the editor if it unmounts before the plugin loads', async () => {
63
+ let resolveLoad;
64
+ loadBeePlugin.mockReturnValue(new Promise((resolve) => { resolveLoad = resolve; }));
65
+ const { unmount } = render(<BeeEditor {...defaultProps} />);
66
+ unmount();
67
+ resolveLoad(mockBeePlugin);
68
+ await Promise.resolve();
69
+ expect(mockBeePlugin.create).not.toHaveBeenCalled();
70
+ });
71
+ });
@@ -6,6 +6,7 @@ import { connect } from 'react-redux';
6
6
  import { createStructuredSelector } from 'reselect';
7
7
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
8
8
  import { UserIsAuthenticated } from '../../utils/authWrapper';
9
+ import { loadBeePlugin } from '../../utils/beePluginLoader';
9
10
  import TagList from '../TagList';
10
11
  import {
11
12
  ANDROID, BEE_LAYOUT_OPTIONS, MOBILE, UNSUBSCRIBE,
@@ -113,18 +114,29 @@ function BeePopupEditor(props) {
113
114
  saveBeeData(jsonFile, htmlFile, device);
114
115
  },
115
116
  };
116
- window.BeePlugin.create(tokenData, beeConfig, (instance) => {
117
- beePluginInstance = instance;
118
- beeInstanceRef.current = instance;
119
- // Use ref to get the latest beeJson — the closure captures the value at effect time,
120
- // but beeJsonRef.current is always up-to-date (e.g. when template data loads async)
121
- const latestBeeJson = beeJsonRef.current;
122
- const parseJson = typeof latestBeeJson === 'string' ? JSON.parse(latestBeeJson) : latestBeeJson;
123
- beePluginInstance.start(parseJson);
124
- saveBeeInstance(beePluginInstance, device);
125
- isInitializedRef.current = true;
126
- });
117
+ let isEffectCancelled = false;
118
+ loadBeePlugin()
119
+ .then((BeePlugin) => {
120
+ if (isEffectCancelled) {
121
+ return;
122
+ }
123
+ BeePlugin.create(tokenData, beeConfig, (instance) => {
124
+ beePluginInstance = instance;
125
+ beeInstanceRef.current = instance;
126
+ // Use ref to get the latest beeJson — the closure captures the value at effect time,
127
+ // but beeJsonRef.current is always up-to-date (e.g. when template data loads async)
128
+ const latestBeeJson = beeJsonRef.current;
129
+ const parseJson = typeof latestBeeJson === 'string' ? JSON.parse(latestBeeJson) : latestBeeJson;
130
+ beePluginInstance.start(parseJson);
131
+ saveBeeInstance(beePluginInstance, device);
132
+ isInitializedRef.current = true;
133
+ });
134
+ })
135
+ .catch((error) => {
136
+ console.error('[BeePopupEditor] Error creating BEE plugin:', error);
137
+ });
127
138
  return () => {
139
+ isEffectCancelled = true;
128
140
  clearInterval(intervalTimer);
129
141
  // Reset initialization flag when component unmounts or layout changes
130
142
  isInitializedRef.current = false;