@automattic/jetpack-shared-extension-utils 0.9.2 → 0.10.0

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,10 @@ 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.0] - 2023-03-27
9
+ ### Added
10
+ - useModuleStatus: Add new hook to enable or disable Jetpack modules. [#29044]
11
+
8
12
  ## [0.9.2] - 2023-03-23
9
13
  ### Changed
10
14
  - Updated package dependencies.
@@ -181,6 +185,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
181
185
  ### Changed
182
186
  - Core: prepare utility for release
183
187
 
188
+ [0.10.0]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.9.2...0.10.0
184
189
  [0.9.2]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.9.1...0.9.2
185
190
  [0.9.1]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.9.0...0.9.1
186
191
  [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.0",
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
+ } );