@automattic/jetpack-shared-extension-utils 0.16.4 → 0.17.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.
@@ -0,0 +1 @@
1
+ export * from './connection';
@@ -0,0 +1,194 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import apiFetch from '@wordpress/api-fetch';
5
+ /**
6
+ * Types & Constants
7
+ */
8
+ import {
9
+ ACTION_DECREASE_NEW_ASYNC_REQUEST_COUNTDOWN,
10
+ ACTION_DEQUEUE_ASYNC_REQUEST,
11
+ ACTION_ENQUEUE_ASYNC_REQUEST,
12
+ ACTION_FETCH_FROM_API,
13
+ ACTION_INCREASE_AI_ASSISTANT_REQUESTS_COUNT,
14
+ ACTION_REQUEST_AI_ASSISTANT_FEATURE,
15
+ ACTION_SET_PLANS,
16
+ ACTION_SET_AI_ASSISTANT_FEATURE_REQUIRE_UPGRADE,
17
+ ACTION_STORE_AI_ASSISTANT_FEATURE,
18
+ ENDPOINT_AI_ASSISTANT_FEATURE,
19
+ NEW_ASYNC_REQUEST_TIMER_INTERVAL,
20
+ ACTION_SET_TIER_PLANS_ENABLED,
21
+ } from './constants.js';
22
+ import type { Plan, AiFeatureProps, SiteAIAssistantFeatureEndpointResponseProps } from './types.js';
23
+
24
+ /**
25
+ * Map the response from the `sites/$site/ai-assistant-feature`
26
+ * endpoint to the AI Assistant feature props.
27
+ * @param { SiteAIAssistantFeatureEndpointResponseProps } response - The response from the endpoint.
28
+ * @return { AiFeatureProps } The AI Assistant feature props.
29
+ */
30
+ export function mapAiFeatureResponseToAiFeatureProps(
31
+ response: SiteAIAssistantFeatureEndpointResponseProps
32
+ ): AiFeatureProps {
33
+ return {
34
+ hasFeature: !! response[ 'has-feature' ],
35
+ isOverLimit: !! response[ 'is-over-limit' ],
36
+ requestsCount: response[ 'requests-count' ],
37
+ requestsLimit: response[ 'requests-limit' ],
38
+ requireUpgrade: !! response[ 'site-require-upgrade' ],
39
+ errorMessage: response[ 'error-message' ],
40
+ errorCode: response[ 'error-code' ],
41
+ upgradeType: response[ 'upgrade-type' ],
42
+ usagePeriod: {
43
+ currentStart: response[ 'usage-period' ]?.[ 'current-start' ],
44
+ nextStart: response[ 'usage-period' ]?.[ 'next-start' ],
45
+ requestsCount: response[ 'usage-period' ]?.[ 'requests-count' ] || 0,
46
+ },
47
+ currentTier: response[ 'current-tier' ],
48
+ nextTier: response[ 'next-tier' ],
49
+ tierPlansEnabled: !! response[ 'tier-plans-enabled' ],
50
+ costs: response.costs,
51
+ featuresControl: response[ 'features-control' ],
52
+ };
53
+ }
54
+
55
+ const actions = {
56
+ setPlans( plans: Array< Plan > ) {
57
+ return {
58
+ type: ACTION_SET_PLANS,
59
+ plans,
60
+ };
61
+ },
62
+
63
+ fetchFromAPI( url: string ) {
64
+ return {
65
+ type: ACTION_FETCH_FROM_API,
66
+ url,
67
+ };
68
+ },
69
+
70
+ storeAiAssistantFeature( feature: AiFeatureProps ) {
71
+ return {
72
+ type: ACTION_STORE_AI_ASSISTANT_FEATURE,
73
+ feature,
74
+ };
75
+ },
76
+
77
+ /**
78
+ * Thunk action to fetch the AI Assistant feature from the API.
79
+ *
80
+ * @return {Function} The thunk action.
81
+ */
82
+ fetchAiAssistantFeature() {
83
+ return async ( { dispatch } ) => {
84
+ // Dispatch isFetching action.
85
+ dispatch( { type: ACTION_REQUEST_AI_ASSISTANT_FEATURE } );
86
+
87
+ try {
88
+ const response: SiteAIAssistantFeatureEndpointResponseProps = await apiFetch( {
89
+ path: ENDPOINT_AI_ASSISTANT_FEATURE,
90
+ } );
91
+
92
+ // Store the feature in the store.
93
+ dispatch(
94
+ actions.storeAiAssistantFeature( mapAiFeatureResponseToAiFeatureProps( response ) )
95
+ );
96
+ } catch ( err ) {
97
+ // @todo: Handle error.
98
+ console.error( err ); // eslint-disable-line no-console
99
+ }
100
+ };
101
+ },
102
+
103
+ /**
104
+ * This thunk action is used to increase
105
+ * the requests count for the current usage period.
106
+ * @param {number} count - The number of requests to increase. Default is 1.
107
+ * @return {Function} The thunk action.
108
+ */
109
+ increaseAiAssistantRequestsCount( count: number = 1 ) {
110
+ return ( { dispatch } ) => {
111
+ dispatch( {
112
+ type: ACTION_INCREASE_AI_ASSISTANT_REQUESTS_COUNT,
113
+ count,
114
+ } );
115
+
116
+ // Every time the requests count is increased, decrease the countdown
117
+ dispatch( actions.decreaseAsyncRequestCountdownValue() );
118
+ };
119
+ },
120
+
121
+ /**
122
+ * This thunk action is used to decrease
123
+ * the countdown value for the new async request.
124
+ * When the countdown reaches 0, enqueue a new async request.
125
+ *
126
+ * @return {Function} The thunk action.
127
+ */
128
+ decreaseAsyncRequestCountdownValue() {
129
+ return async ( { dispatch, select } ) => {
130
+ dispatch( { type: ACTION_DECREASE_NEW_ASYNC_REQUEST_COUNTDOWN } );
131
+
132
+ const asyncCoundown = select.getAsyncRequestCountdownValue();
133
+ if ( asyncCoundown <= 0 ) {
134
+ dispatch( actions.enqueueAiAssistantFeatureAsyncRequest() );
135
+ }
136
+ };
137
+ },
138
+
139
+ /**
140
+ * This thunk action is used to enqueue a new async request.
141
+ * If already exist an enqueue request, clear it and enqueue a new one.
142
+ *
143
+ * @return {Function} The thunk action.
144
+ */
145
+ enqueueAiAssistantFeatureAsyncRequest() {
146
+ return ( { dispatch } ) => {
147
+ // Check if there is already a timer running
148
+ dispatch.dequeueAiAssistantFeatureAsyncRequest();
149
+
150
+ const contdownTimerId = setTimeout( () => {
151
+ dispatch( actions.fetchAiAssistantFeature() );
152
+ }, NEW_ASYNC_REQUEST_TIMER_INTERVAL ); // backend process requires a delay to be able to see the new value
153
+
154
+ dispatch( { type: ACTION_ENQUEUE_ASYNC_REQUEST, timerId: contdownTimerId } );
155
+ };
156
+ },
157
+
158
+ /**
159
+ * This thunk action is used to dequeue a new async request.
160
+ * It will clear the timer if there is one,
161
+ * canceling the enqueue async request.
162
+ *
163
+ * @return {Function} The thunk action.
164
+ */
165
+ dequeueAiAssistantFeatureAsyncRequest() {
166
+ return ( { dispatch, select } ) => {
167
+ dispatch( { type: ACTION_DEQUEUE_ASYNC_REQUEST, timerId: 0 } );
168
+
169
+ const timerId = select.getAsyncRequestCountdownTimerId();
170
+ // If there is no timer, there is nothing to clear
171
+ if ( ! timerId ) {
172
+ return;
173
+ }
174
+
175
+ window?.clearTimeout( timerId );
176
+ };
177
+ },
178
+
179
+ setAiAssistantFeatureRequireUpgrade( requireUpgrade: boolean = true ) {
180
+ return {
181
+ type: ACTION_SET_AI_ASSISTANT_FEATURE_REQUIRE_UPGRADE,
182
+ requireUpgrade,
183
+ };
184
+ },
185
+
186
+ setTierPlansEnabled( tierPlansEnabled: boolean = true ) {
187
+ return {
188
+ type: ACTION_SET_TIER_PLANS_ENABLED,
189
+ tierPlansEnabled,
190
+ };
191
+ },
192
+ };
193
+
194
+ export default actions;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Plans actions
3
+ */
4
+ export const ACTION_SET_PLANS = 'SET_PLANS';
5
+ export const ACTION_FETCH_FROM_API = 'FETCH_FROM_API';
6
+
7
+ /**
8
+ * AI Assistant feature Actions
9
+ */
10
+ export const ACTION_STORE_AI_ASSISTANT_FEATURE = 'STORE_AI_ASSISTANT_FEATURE';
11
+ export const ACTION_REQUEST_AI_ASSISTANT_FEATURE = 'REQUEST_AI_ASSISTANT_FEATURE';
12
+ export const ACTION_INCREASE_AI_ASSISTANT_REQUESTS_COUNT = 'INCREASE_AI_ASSISTANT_REQUESTS_COUNT';
13
+ export const ACTION_SET_AI_ASSISTANT_FEATURE_REQUIRE_UPGRADE =
14
+ 'SET_AI_ASSISTANT_FEATURE_REQUIRE_UPGRADE';
15
+ export const ACTION_SET_TIER_PLANS_ENABLED = 'SET_TIER_PLANS_ENABLED';
16
+
17
+ /**
18
+ * Endpoints
19
+ */
20
+ export const ENDPOINT_AI_ASSISTANT_FEATURE = '/wpcom/v2/jetpack-ai/ai-assistant-feature';
21
+
22
+ /**
23
+ * New AI Assistant feature async request
24
+ */
25
+ export const FREE_PLAN_REQUESTS_LIMIT = 20;
26
+ export const UNLIMITED_PLAN_REQUESTS_LIMIT = 3000;
27
+ export const ASYNC_REQUEST_COUNTDOWN_INIT_VALUE = 3;
28
+ export const NEW_ASYNC_REQUEST_TIMER_INTERVAL = 5000;
29
+ export const ACTION_DECREASE_NEW_ASYNC_REQUEST_COUNTDOWN = 'DECREASE_NEW_ASYNC_REQUEST_COUNTDOWN';
30
+ export const ACTION_ENQUEUE_ASYNC_REQUEST = 'ENQUEUE_ASYNC_COUNTDOWN_REQUEST';
31
+ export const ACTION_DEQUEUE_ASYNC_REQUEST = 'DEQUEUE_ASYNC_COUNTDOWN_REQUEST';
@@ -0,0 +1,108 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { createReduxStore, register } from '@wordpress/data';
5
+ /**
6
+ * Internal dependencies
7
+ */
8
+ import actions from './actions.js';
9
+ import reducer from './reducer.js';
10
+ /**
11
+ * Types
12
+ */
13
+ import type { AiFeatureProps, PlanStateProps } from './types.js';
14
+
15
+ const store = 'wordpress-com/plans';
16
+
17
+ export const selectors = {
18
+ /*
19
+ * Return the plan with the given slug.
20
+ *
21
+ * @param {Object} state - The Plans state tree.
22
+ * @param {string} planSlug - The plan slug to find.
23
+ * @return {Object} The plan.
24
+ */
25
+ getPlan( state: PlanStateProps, planSlug: string ) {
26
+ return state.plans.find( plan => plan.product_slug === planSlug );
27
+ },
28
+
29
+ /**
30
+ * Return the AI Assistant feature.
31
+ *
32
+ * @param {PlanStateProps} state - The Plans state tree.
33
+ * @return {AiFeatureProps} The AI Assistant feature data.
34
+ */
35
+ getAiAssistantFeature( state: PlanStateProps ): AiFeatureProps {
36
+ // Clean up the _meta property.
37
+ const data = { ...state.features.aiAssistant };
38
+ delete data._meta;
39
+
40
+ return data;
41
+ },
42
+
43
+ /**
44
+ * Get the isRequesting flag for the AI Assistant feature.
45
+ *
46
+ * @param {PlanStateProps} state - The Plans state tree.
47
+ * @return {boolean} The isRequesting flag.
48
+ */
49
+ getIsRequestingAiAssistantFeature( state: PlanStateProps ): boolean {
50
+ return state.features.aiAssistant?._meta?.isRequesting;
51
+ },
52
+
53
+ getAsyncRequestCountdownValue( state: PlanStateProps ): number {
54
+ return state.features.aiAssistant?._meta?.asyncRequestCountdown;
55
+ },
56
+
57
+ getAsyncRequestCountdownTimerId( state: PlanStateProps ): number {
58
+ return state.features.aiAssistant?._meta?.asyncRequestTimerId;
59
+ },
60
+ };
61
+
62
+ export const wordpressPlansStore = createReduxStore( store, {
63
+ actions,
64
+
65
+ reducer,
66
+
67
+ selectors,
68
+
69
+ controls: {
70
+ FETCH_FROM_API( { url } ) {
71
+ // We cannot use `@wordpress/api-fetch` here since it unconditionally sends
72
+ // the `X-WP-Nonce` header, which is disallowed by WordPress.com.
73
+ // (To reproduce, note that you need to call `apiFetch` with `
74
+ // `{ credentials: 'same-origin', mode: 'cors' }`, since its defaults are
75
+ // different from `fetch`'s.)
76
+ return fetch( url ).then( response => response.json() );
77
+ },
78
+ },
79
+
80
+ resolvers: {
81
+ *getPlan() {
82
+ const url = 'https://public-api.wordpress.com/rest/v1.5/plans';
83
+ const plans = yield actions.fetchFromAPI( url );
84
+ return actions.setPlans( plans );
85
+ },
86
+
87
+ getAiAssistantFeature: ( state: PlanStateProps ) => {
88
+ if ( state?.features?.aiAssistant ) {
89
+ return;
90
+ }
91
+
92
+ return actions.fetchAiAssistantFeature();
93
+ },
94
+ },
95
+ } );
96
+
97
+ register( wordpressPlansStore );
98
+
99
+ // Types
100
+
101
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
102
+ type OmitFirstArg< F > = F extends ( _: any, ...args: infer P ) => infer R
103
+ ? ( ...args: P ) => R
104
+ : never;
105
+
106
+ export type WordPressPlansSelectors = {
107
+ [ key in keyof typeof selectors ]: OmitFirstArg< ( typeof selectors )[ key ] >;
108
+ };
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Types & Constants
3
+ */
4
+ import {
5
+ ACTION_DECREASE_NEW_ASYNC_REQUEST_COUNTDOWN,
6
+ ACTION_ENQUEUE_ASYNC_REQUEST,
7
+ ACTION_INCREASE_AI_ASSISTANT_REQUESTS_COUNT,
8
+ ACTION_REQUEST_AI_ASSISTANT_FEATURE,
9
+ ACTION_SET_PLANS,
10
+ ACTION_SET_AI_ASSISTANT_FEATURE_REQUIRE_UPGRADE,
11
+ ACTION_STORE_AI_ASSISTANT_FEATURE,
12
+ ASYNC_REQUEST_COUNTDOWN_INIT_VALUE,
13
+ FREE_PLAN_REQUESTS_LIMIT,
14
+ UNLIMITED_PLAN_REQUESTS_LIMIT,
15
+ ACTION_SET_TIER_PLANS_ENABLED,
16
+ } from './constants.js';
17
+ import type { PlanStateProps, TierLimitProp } from './types.js';
18
+
19
+ const INITIAL_STATE: PlanStateProps = {
20
+ plans: [],
21
+ features: {
22
+ aiAssistant: {
23
+ hasFeature: true,
24
+ isOverLimit: false,
25
+ requestsCount: 0,
26
+ requestsLimit: FREE_PLAN_REQUESTS_LIMIT,
27
+ requireUpgrade: false,
28
+ errorMessage: '',
29
+ errorCode: '',
30
+ upgradeType: 'default',
31
+ currentTier: {
32
+ slug: 'ai-assistant-tier-free',
33
+ value: 0,
34
+ limit: 20,
35
+ },
36
+ usagePeriod: {
37
+ currentStart: '',
38
+ nextStart: '',
39
+ requestsCount: 0,
40
+ },
41
+ nextTier: null,
42
+ tierPlansEnabled: false,
43
+ _meta: {
44
+ isRequesting: false,
45
+ asyncRequestCountdown: ASYNC_REQUEST_COUNTDOWN_INIT_VALUE,
46
+ asyncRequestTimerId: 0,
47
+ },
48
+ },
49
+ },
50
+ };
51
+
52
+ /**
53
+ * The reducer of the plan state
54
+ * @param {PlanStateProps} state - The plan state.
55
+ * @param {object} action - The action.
56
+ * @return {PlanStateProps} - The plan state.
57
+ */
58
+ export default function reducer( state = INITIAL_STATE, action ) {
59
+ switch ( action.type ) {
60
+ case ACTION_SET_PLANS:
61
+ return {
62
+ ...state,
63
+ plans: action.plans,
64
+ };
65
+
66
+ case ACTION_REQUEST_AI_ASSISTANT_FEATURE:
67
+ return {
68
+ ...state,
69
+ features: {
70
+ ...state.features,
71
+ aiAssistant: {
72
+ ...state.features.aiAssistant,
73
+ _meta: {
74
+ ...state.features.aiAssistant._meta,
75
+ isRequesting: true,
76
+ asyncRequestCountdown: ASYNC_REQUEST_COUNTDOWN_INIT_VALUE, // restore the countdown
77
+ asyncRequestTimerId: 0, // reset the timer id
78
+ },
79
+ },
80
+ },
81
+ };
82
+
83
+ case ACTION_STORE_AI_ASSISTANT_FEATURE: {
84
+ return {
85
+ ...state,
86
+ features: {
87
+ ...state.features,
88
+ aiAssistant: {
89
+ ...action.feature,
90
+ _meta: {
91
+ ...state.features.aiAssistant._meta,
92
+ isRequesting: false,
93
+ },
94
+ },
95
+ },
96
+ };
97
+ }
98
+
99
+ case ACTION_INCREASE_AI_ASSISTANT_REQUESTS_COUNT: {
100
+ // Usage Period data
101
+ const usagePeriod = state.features.aiAssistant.usagePeriod || { requestsCount: 0 };
102
+
103
+ // Increase requests counters
104
+ const requestsCount = state.features.aiAssistant.requestsCount + action.count;
105
+ usagePeriod.requestsCount += action.count;
106
+
107
+ // Current tier value
108
+ const currentTierValue = state.features.aiAssistant.currentTier?.value;
109
+
110
+ const isFreeTierPlan =
111
+ ( typeof currentTierValue === 'undefined' && ! state.features.aiAssistant.hasFeature ) ||
112
+ currentTierValue === 0;
113
+
114
+ const isUnlimitedTierPlan =
115
+ ( typeof currentTierValue === 'undefined' && state.features.aiAssistant.hasFeature ) ||
116
+ currentTierValue === 1;
117
+
118
+ // Request limit defined with the current tier limit by default.
119
+ let requestsLimit = state.features.aiAssistant.currentTier?.limit;
120
+
121
+ if ( isUnlimitedTierPlan ) {
122
+ requestsLimit = UNLIMITED_PLAN_REQUESTS_LIMIT;
123
+ } else if ( isFreeTierPlan ) {
124
+ requestsLimit = state.features.aiAssistant.requestsLimit as TierLimitProp;
125
+ }
126
+
127
+ const currentCount = isFreeTierPlan
128
+ ? requestsCount // Free tier plan counts all time requests
129
+ : state.features.aiAssistant.usagePeriod?.requestsCount; // Unlimited tier plan counts usage period requests
130
+
131
+ /**
132
+ * Compute the AI Assistant Feature data optimistically,
133
+ * based on the Jetpack_AI_Helper::get_ai_assistance_feature() helper.
134
+ *
135
+ * @see _inc/lib/class-jetpack-ai-helper.php
136
+ */
137
+ const isOverLimit = currentCount >= requestsLimit;
138
+
139
+ // highest tier holds a soft limit so requireUpgrade is false on that case (nextTier null means highest tier)
140
+ const requireUpgrade = isOverLimit && state.features.aiAssistant.nextTier !== null;
141
+
142
+ return {
143
+ ...state,
144
+ features: {
145
+ ...state.features,
146
+ aiAssistant: {
147
+ ...state.features.aiAssistant,
148
+ isOverLimit,
149
+ requestsCount,
150
+ requireUpgrade,
151
+ usagePeriod: { ...usagePeriod },
152
+ },
153
+ },
154
+ };
155
+ }
156
+
157
+ case ACTION_DECREASE_NEW_ASYNC_REQUEST_COUNTDOWN: {
158
+ return {
159
+ ...state,
160
+ features: {
161
+ ...state.features,
162
+ aiAssistant: {
163
+ ...state.features.aiAssistant,
164
+ _meta: {
165
+ ...state.features.aiAssistant._meta,
166
+ asyncRequestCountdown: state.features.aiAssistant._meta.asyncRequestCountdown - 1,
167
+ },
168
+ },
169
+ },
170
+ };
171
+ }
172
+
173
+ case ACTION_ENQUEUE_ASYNC_REQUEST: {
174
+ return {
175
+ ...state,
176
+ features: {
177
+ ...state.features,
178
+ aiAssistant: {
179
+ ...state.features.aiAssistant,
180
+ _meta: {
181
+ ...state.features.aiAssistant._meta,
182
+ asyncRequestTimerId: action.timerId,
183
+ },
184
+ },
185
+ },
186
+ };
187
+ }
188
+
189
+ case ACTION_SET_AI_ASSISTANT_FEATURE_REQUIRE_UPGRADE: {
190
+ /*
191
+ * If we require an upgrade, we are also over the limit;
192
+ * The opposite is not true, we can be over the limit without
193
+ * requiring an upgrade, for example when we are on the highest tier.
194
+ * In this case, we don't want to set isOverLimit to false.
195
+ */
196
+ return {
197
+ ...state,
198
+ features: {
199
+ ...state.features,
200
+ aiAssistant: {
201
+ ...state.features.aiAssistant,
202
+ requireUpgrade: action.requireUpgrade,
203
+ ...( action.requireUpgrade ? { isOverLimit: true } : {} ),
204
+ },
205
+ },
206
+ };
207
+ }
208
+
209
+ case ACTION_SET_TIER_PLANS_ENABLED: {
210
+ return {
211
+ ...state,
212
+ features: {
213
+ ...state.features,
214
+ aiAssistant: {
215
+ ...state.features.aiAssistant,
216
+ tierPlansEnabled: action.tierPlansEnabled,
217
+ },
218
+ },
219
+ };
220
+ }
221
+ }
222
+
223
+ return state;
224
+ }