@automattic/jetpack-shared-extension-utils 0.9.1 → 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,7 +5,15 @@ 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.9.1] - 2023-03-07
8
+ ## [0.10.0] - 2023-03-27
9
+ ### Added
10
+ - useModuleStatus: Add new hook to enable or disable Jetpack modules. [#29044]
11
+
12
+ ## [0.9.2] - 2023-03-23
13
+ ### Changed
14
+ - Updated package dependencies.
15
+
16
+ ## [0.9.1] - 2023-03-08
9
17
  ### Changed
10
18
  - Updated package dependencies. [#29216]
11
19
 
@@ -177,6 +185,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
177
185
  ### Changed
178
186
  - Core: prepare utility for release
179
187
 
188
+ [0.10.0]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.9.2...0.10.0
189
+ [0.9.2]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.9.1...0.9.2
180
190
  [0.9.1]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.9.0...0.9.1
181
191
  [0.9.0]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.8.4...0.9.0
182
192
  [0.8.4]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.8.3...0.8.4
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.1",
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
+ } );
package/src/plan-utils.js CHANGED
@@ -22,45 +22,50 @@ export function getUpgradeUrl( { planSlug, plan, postId, postType } ) {
22
22
  const planPathSlug = startsWith( planSlug, 'jetpack_' ) ? planSlug : get( plan, [ 'path_slug' ] );
23
23
 
24
24
  // The full site editor has no set post type.
25
- const redirect_to = ( undefined === postType
26
- ? () => {
27
- const queryParams = new URLSearchParams( window.location.search );
28
-
29
- return addQueryArgs(
30
- window.location.protocol +
31
- `//${ getSiteFragment().replace( '::', '/' ) }/wp-admin/site-editor.php`,
32
- {
33
- postId: queryParams.get( 'postId' ),
34
- postType: queryParams.get( 'postType' ),
35
- plan_upgraded: 1,
36
- }
37
- );
38
- }
39
- : () => {
40
- // The editor for CPTs has an `edit/` route fragment prefixed.
41
- const postTypeEditorRoutePrefix = [ 'page', 'post' ].includes( postType ) ? '' : 'edit';
42
-
43
- // Post-checkout: redirect back here.
44
- return isSimpleSite()
45
- ? addQueryArgs(
46
- '/' +
47
- compact( [ postTypeEditorRoutePrefix, postType, getSiteFragment(), postId ] ).join(
48
- '/'
49
- ),
50
- {
51
- plan_upgraded: 1,
52
- }
53
- )
54
- : addQueryArgs(
55
- window.location.protocol +
56
- `//${ getSiteFragment().replace( '::', '/' ) }/wp-admin/post.php`,
57
- {
58
- action: 'edit',
59
- post: postId,
60
- plan_upgraded: 1,
61
- }
62
- );
63
- } )();
25
+ const redirect_to = (
26
+ undefined === postType
27
+ ? () => {
28
+ const queryParams = new URLSearchParams( window.location.search );
29
+
30
+ return addQueryArgs(
31
+ window.location.protocol +
32
+ `//${ getSiteFragment().replace( '::', '/' ) }/wp-admin/site-editor.php`,
33
+ {
34
+ postId: queryParams.get( 'postId' ),
35
+ postType: queryParams.get( 'postType' ),
36
+ plan_upgraded: 1,
37
+ }
38
+ );
39
+ }
40
+ : () => {
41
+ // The editor for CPTs has an `edit/` route fragment prefixed.
42
+ const postTypeEditorRoutePrefix = [ 'page', 'post' ].includes( postType ) ? '' : 'edit';
43
+
44
+ // Post-checkout: redirect back here.
45
+ return isSimpleSite()
46
+ ? addQueryArgs(
47
+ '/' +
48
+ compact( [
49
+ postTypeEditorRoutePrefix,
50
+ postType,
51
+ getSiteFragment(),
52
+ postId,
53
+ ] ).join( '/' ),
54
+ {
55
+ plan_upgraded: 1,
56
+ }
57
+ )
58
+ : addQueryArgs(
59
+ window.location.protocol +
60
+ `//${ getSiteFragment().replace( '::', '/' ) }/wp-admin/post.php`,
61
+ {
62
+ action: 'edit',
63
+ post: postId,
64
+ plan_upgraded: 1,
65
+ }
66
+ );
67
+ }
68
+ )();
64
69
 
65
70
  // Redirect to calypso plans page for WoC sites.
66
71
  if ( isAtomicSite() ) {
@@ -10,11 +10,12 @@ import './style.scss';
10
10
  // We thus add a new `is-interactive` class to be able to override that behavior.
11
11
  export default name =>
12
12
  createHigherOrderComponent(
13
- BlockListBlock => props => (
14
- <BlockListBlock
15
- { ...props }
16
- className={ props.name === name ? 'has-warning is-interactive' : props.className }
17
- />
18
- ),
13
+ BlockListBlock => props =>
14
+ (
15
+ <BlockListBlock
16
+ { ...props }
17
+ className={ props.name === name ? 'has-warning is-interactive' : props.className }
18
+ />
19
+ ),
19
20
  'withHasWarningIsInteractiveClassNames'
20
21
  );