@automattic/jetpack-shared-extension-utils 0.9.2 → 0.10.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/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.10.1] - 2023-03-28
9
+ ### Changed
10
+ - Minor internal updates.
11
+
12
+ ## [0.10.0] - 2023-03-27
13
+ ### Added
14
+ - useModuleStatus: Add new hook to enable or disable Jetpack modules. [#29044]
15
+
8
16
  ## [0.9.2] - 2023-03-23
9
17
  ### Changed
10
18
  - Updated package dependencies.
@@ -181,6 +189,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
181
189
  ### Changed
182
190
  - Core: prepare utility for release
183
191
 
192
+ [0.10.0]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.9.2...0.10.0
184
193
  [0.9.2]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.9.1...0.9.2
185
194
  [0.9.1]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.9.0...0.9.1
186
195
  [0.9.0]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.8.4...0.9.0
package/index.js CHANGED
@@ -15,3 +15,4 @@ export {
15
15
  } from './src/plan-utils';
16
16
  export { default as isCurrentUserConnected } from './src/is-current-user-connected';
17
17
  export { default as useAnalytics } from './src/hooks/use-analytics';
18
+ export { default as useModuleStatus } from './src/hooks/use-module-status';
@@ -0,0 +1,12 @@
1
+ if ( ! window.matchMedia ) {
2
+ window.matchMedia = query => ( {
3
+ matches: false,
4
+ media: query,
5
+ onchange: null,
6
+ addListener: jest.fn(), // deprecated
7
+ removeListener: jest.fn(), // deprecated
8
+ addEventListener: jest.fn(),
9
+ removeEventListener: jest.fn(),
10
+ dispatchEvent: jest.fn(),
11
+ } );
12
+ }
package/jest.config.js ADDED
@@ -0,0 +1,13 @@
1
+ const baseConfig = require( 'jetpack-js-tools/jest/config.base.js' );
2
+
3
+ module.exports = {
4
+ ...baseConfig,
5
+ roots: [ '<rootDir>/src' ],
6
+ setupFiles: [ ...baseConfig.setupFiles, '<rootDir>/jest-globals.js' ],
7
+ transform: {
8
+ ...baseConfig.transform,
9
+ '\\.[jt]sx?$': require( 'jetpack-js-tools/jest/babel-jest-config-factory.js' )(
10
+ require.resolve
11
+ ),
12
+ },
13
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/jetpack-shared-extension-utils",
3
- "version": "0.9.2",
3
+ "version": "0.10.1",
4
4
  "description": "Utility functions used by the block editor extensions",
5
5
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/shared-extension-utils/#readme",
6
6
  "bugs": {
@@ -13,10 +13,13 @@
13
13
  },
14
14
  "license": "GPL-2.0-or-later",
15
15
  "author": "Automattic",
16
- "scripts": {},
16
+ "scripts": {
17
+ "test": "jest"
18
+ },
17
19
  "dependencies": {
18
20
  "@automattic/jetpack-analytics": "workspace:*",
19
21
  "@automattic/jetpack-connection": "workspace:*",
22
+ "@wordpress/api-fetch": "6.23.0",
20
23
  "@wordpress/compose": "6.5.0",
21
24
  "@wordpress/element": "5.5.0",
22
25
  "@wordpress/i18n": "4.28.0",
@@ -27,7 +30,18 @@
27
30
  "devDependencies": {
28
31
  "@babel/core": "7.20.12",
29
32
  "@babel/preset-react": "7.18.6",
30
- "react": "18.2.0"
33
+ "babel-jest": "29.3.1",
34
+ "jest": "29.3.1",
35
+ "jest-environment-jsdom": "29.3.1",
36
+ "react": "18.2.0",
37
+ "react-dom": "18.2.0",
38
+ "jetpack-js-tools": "workspace:*",
39
+ "@automattic/jetpack-webpack-config": "workspace:*",
40
+ "@wordpress/babel-plugin-import-jsx-pragma": "4.9.0",
41
+ "@testing-library/dom": "8.19.1",
42
+ "@testing-library/react": "13.4.0",
43
+ "@testing-library/user-event": "14.4.3",
44
+ "@babel/plugin-transform-react-jsx": "7.20.13"
31
45
  },
32
46
  "exports": {
33
47
  ".": "./index.js"
@@ -0,0 +1,114 @@
1
+ import apiFetch from '@wordpress/api-fetch';
2
+ import { useEffect, useState, useMemo, useCallback } from '@wordpress/element';
3
+ import { isSimpleSite } from '../../site-type-utils';
4
+
5
+ /**
6
+ * Fetch information about all Jetpack modules.
7
+ *
8
+ * @returns {Promise<object>} Details about all available modules on the site.
9
+ */
10
+ async function fetchModules() {
11
+ try {
12
+ const result = await apiFetch( {
13
+ path: `/jetpack/v4/module/all`,
14
+ method: 'GET',
15
+ } );
16
+ return result;
17
+ } catch ( error ) {
18
+ return error.message;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Update a Jetpack module's status.
24
+ *
25
+ * @param {*} name - The module's name.
26
+ * @param {*} toggle - New module status.
27
+ * @returns {Promise<boolean>} Promise that resolves to the new module status.
28
+ */
29
+ async function changeModuleStatus( name, toggle ) {
30
+ const result = await apiFetch( {
31
+ path: `/jetpack/v4/module/${ name }/active`,
32
+ method: 'POST',
33
+ data: {
34
+ active: toggle,
35
+ },
36
+ } );
37
+ return result;
38
+ }
39
+
40
+ /**
41
+ * Determine whethher a Jetpack module is active.
42
+ *
43
+ * @param {string} name - The module's name
44
+ * @returns {Promise<boolean>} Whether the module is active.
45
+ */
46
+ async function isJetpackModuleActive( name ) {
47
+ // On WordPress.com Simple sites, all modules are always active.
48
+ if ( isSimpleSite() ) {
49
+ return true;
50
+ }
51
+
52
+ // Fetch module info.
53
+ try {
54
+ // Check if module is active.
55
+ const modulesInfo = await fetchModules();
56
+ if ( ! modulesInfo || ! modulesInfo.hasOwnProperty( name ) ) {
57
+ return false;
58
+ }
59
+ return !! modulesInfo[ name ].activated;
60
+ } catch ( e ) {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Manage a Jetpack module's status (get and set).
67
+ *
68
+ * @param {string} name - The module's name.
69
+ * @returns {boolean} Whether the module is active.
70
+ */
71
+ const useModuleStatus = name => {
72
+ const [ isLoadingModules, setIsLoadingModules ] = useState( Boolean( name ) );
73
+ const [ isChangingStatus, setIsChangingStatus ] = useState( false );
74
+ const [ isModuleActive, setModuleStatus ] = useState( false );
75
+
76
+ // Get module status.
77
+ useEffect( () => {
78
+ if ( ! name ) {
79
+ return;
80
+ }
81
+
82
+ setIsLoadingModules( true );
83
+
84
+ isJetpackModuleActive( name ).then( moduleStatus => {
85
+ setModuleStatus( moduleStatus );
86
+ setIsLoadingModules( false );
87
+ } );
88
+ }, [ name ] );
89
+
90
+ const changeStatus = useCallback(
91
+ newModuleStatus => {
92
+ if ( ! name || isModuleActive === newModuleStatus ) {
93
+ return;
94
+ }
95
+ setIsChangingStatus( true );
96
+ changeModuleStatus( name, newModuleStatus )
97
+ .then( () => {
98
+ setModuleStatus( newModuleStatus );
99
+ setIsChangingStatus( false );
100
+ } )
101
+ .catch( () => {
102
+ setIsChangingStatus( false );
103
+ } );
104
+ },
105
+ [ name, isModuleActive ]
106
+ );
107
+
108
+ return useMemo(
109
+ () => ( { isLoadingModules, isChangingStatus, isModuleActive, changeStatus } ),
110
+ [ isLoadingModules, isChangingStatus, isModuleActive, changeStatus ]
111
+ );
112
+ };
113
+
114
+ export default useModuleStatus;
@@ -0,0 +1,153 @@
1
+ import { renderHook, waitFor, act } from '@testing-library/react';
2
+ import { isSimpleSite } from '../../../site-type-utils';
3
+ import useModuleStatus from '../index';
4
+
5
+ jest.mock( '../../../site-type-utils' );
6
+
7
+ describe( 'useModuleStatus hook', () => {
8
+ const originalFetch = window.fetch;
9
+
10
+ beforeEach( () => {
11
+ isSimpleSite.mockReset();
12
+ // eslint-disable-next-line jest/prefer-spy-on -- Nothing to spy on.
13
+ window.fetch = jest.fn();
14
+ } );
15
+
16
+ afterEach( () => {
17
+ window.fetch = originalFetch;
18
+ } );
19
+ test( 'should not try to fetch modules if no name is provided', () => {
20
+ const { result } = renderHook( name => useModuleStatus( name ), {
21
+ initialProps: '',
22
+ } );
23
+ const { changeStatus, ...otherProps } = result.current;
24
+
25
+ expect( otherProps ).toStrictEqual( {
26
+ isLoadingModules: false,
27
+ isChangingStatus: false,
28
+ isModuleActive: false,
29
+ } );
30
+ } );
31
+
32
+ test( 'jetpack module is active on not simple sites.', async () => {
33
+ isSimpleSite.mockReturnValueOnce( false );
34
+
35
+ window.fetch.mockReturnValueOnce(
36
+ Promise.resolve( {
37
+ status: 200,
38
+ json: () =>
39
+ Promise.resolve( {
40
+ subscriptions: {
41
+ activated: true,
42
+ },
43
+ } ),
44
+ } )
45
+ );
46
+
47
+ const { result } = renderHook( name => useModuleStatus( name ), {
48
+ initialProps: 'subscriptions',
49
+ } );
50
+
51
+ expect( result.current.isModuleActive ).toBe( false );
52
+ await waitFor( async () => expect( result.current.isModuleActive ).toBe( true ) );
53
+
54
+ expect( window.fetch ).toHaveBeenCalledWith(
55
+ '/jetpack/v4/module/all?_locale=user',
56
+ expect.anything()
57
+ );
58
+
59
+ const { changeStatus, ...otherProps } = result.current;
60
+
61
+ expect( otherProps ).toStrictEqual( {
62
+ isLoadingModules: false,
63
+ isChangingStatus: false,
64
+ isModuleActive: true,
65
+ } );
66
+ } );
67
+
68
+ test( 'jetpack module is active on simple sites.', async () => {
69
+ isSimpleSite.mockReturnValueOnce( true );
70
+
71
+ window.fetch.mockReturnValueOnce(
72
+ Promise.resolve( {
73
+ status: 200,
74
+ json: () =>
75
+ Promise.resolve( {
76
+ subscriptions: {
77
+ activated: false,
78
+ },
79
+ } ),
80
+ } )
81
+ );
82
+
83
+ const { result } = renderHook( name => useModuleStatus( name ), {
84
+ initialProps: 'subscriptions',
85
+ } );
86
+
87
+ expect( result.current.isModuleActive ).toBe( false );
88
+ await waitFor( async () => expect( result.current.isModuleActive ).toBe( true ) );
89
+ expect( window.fetch ).not.toHaveBeenCalled();
90
+
91
+ const { changeStatus, ...otherProps } = result.current;
92
+
93
+ expect( otherProps ).toStrictEqual( {
94
+ isLoadingModules: false,
95
+ isChangingStatus: false,
96
+ isModuleActive: true,
97
+ } );
98
+ } );
99
+
100
+ test( 'change jetpack module status', async () => {
101
+ isSimpleSite.mockReturnValueOnce( false );
102
+
103
+ window.fetch.mockReturnValueOnce(
104
+ Promise.resolve( {
105
+ status: 200,
106
+ json: () =>
107
+ Promise.resolve( {
108
+ subscriptions: {
109
+ activated: true,
110
+ },
111
+ } ),
112
+ } )
113
+ );
114
+
115
+ const { result } = renderHook( name => useModuleStatus( name ), {
116
+ initialProps: 'subscriptions',
117
+ } );
118
+
119
+ await waitFor( async () => expect( result.current.isModuleActive ).toBe( true ) );
120
+ expect( window.fetch ).toHaveBeenCalledWith(
121
+ '/jetpack/v4/module/all?_locale=user',
122
+ expect.anything()
123
+ );
124
+ window.fetch.mockReset();
125
+
126
+ window.fetch.mockReturnValueOnce(
127
+ Promise.resolve( {
128
+ status: 200,
129
+ json: () => Promise.resolve( {} ),
130
+ } )
131
+ );
132
+
133
+ act( () => {
134
+ result.current.changeStatus( false );
135
+ } );
136
+
137
+ expect( result.current.isChangingStatus ).toBe( true );
138
+ expect( result.current.isModuleActive ).toBe( true );
139
+
140
+ await waitFor( async () => expect( result.current.isChangingStatus ).toBe( false ) );
141
+ expect( result.current.isModuleActive ).toBe( false );
142
+
143
+ Promise.resolve( {
144
+ status: 200,
145
+ json: () => Promise.resolve( {} ),
146
+ } );
147
+
148
+ expect( window.fetch ).toHaveBeenCalledWith(
149
+ '/jetpack/v4/module/subscriptions/active?_locale=user',
150
+ expect.anything()
151
+ );
152
+ } );
153
+ } );