Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
4dfb412
Refactor the Request class to specify the http method to use
pls78 May 26, 2026
e82cdd1
Pass the method instead of the $is_post bool
pls78 May 26, 2026
a5158d6
Refactor to use the http method passed as parameter
pls78 May 26, 2026
1d5cb47
Mirror the changes made in the main folder structure
pls78 May 26, 2026
50e815a
Update tests
pls78 May 26, 2026
d0de53e
Store user consent in Yoast AI
pls78 May 26, 2026
eb22e0d
Mirror changes in the main folder structure
pls78 May 26, 2026
a2074da
Update tests
pls78 May 26, 2026
750b386
Catch all the possible remote exceptions
pls78 Jun 16, 2026
4b5e405
Remove the try-catch block around revoking consent as we're now defer…
pls78 Jun 16, 2026
9716e36
Add the admin url to allow the fetching of links from the store
pls78 Jun 16, 2026
9aa22ee
Duplicate the changes here in the old ai folders structure
pls78 Jun 16, 2026
232ae76
Remove (broken) error handling as we want this to silently fail (but …
pls78 Jun 16, 2026
65a7be0
Add anything needed to get the admin url
pls78 Jun 16, 2026
f482cda
Initialize the adminUrl name
pls78 Jun 16, 2026
2b803a0
Refactor the component to get a store name
pls78 Jun 16, 2026
17bc00b
Pass along the response from the remote fetch
pls78 Jun 16, 2026
03a4c9a
Show a generic error when anything goes wrong
pls78 Jun 16, 2026
f512b87
Update the consent button regardless of errors
pls78 Jun 16, 2026
320e582
We want to show both the error and the disabled consent button
pls78 Jun 17, 2026
262266c
Remove unused class
pls78 Jun 17, 2026
f513f81
Uniform $user_id type to be int
pls78 Jun 17, 2026
2628006
Mirror the changes in the main folder structure
pls78 Jun 17, 2026
8a804e6
Update tests
pls78 Jun 17, 2026
a82b8c3
Re-instate the PSR-3 logger
pls78 Jun 17, 2026
194598e
Mirror the changes in the main folder structure
pls78 Jun 17, 2026
ccdb941
Update tests
pls78 Jun 17, 2026
1f8ba61
Fix cs
pls78 Jun 17, 2026
2372e99
Remove unused component
pls78 Jun 17, 2026
694f5b1
Covers the case where a user is not found
pls78 Jun 17, 2026
1f3a00b
Update unit tests
pls78 Jun 17, 2026
7004bb6
Adapt to the new AI API contract
pls78 Jun 18, 2026
8781ad6
Invalidate tokens regardless of the remote status
pls78 Jun 18, 2026
25b38a6
Use the real error code/stsatus
pls78 Jun 18, 2026
c90b8f8
Mirror the changes in the main folder structure
pls78 Jun 18, 2026
c6ac321
Adapt unit tests
pls78 Jun 18, 2026
9dc6aa5
Fix unit test
pls78 Jun 18, 2026
b8fb8de
Make the GenericAlert component accept the className parameter
pls78 Jun 18, 2026
d40bbeb
Merge branch 'trunk' into 23302-manage-user-consent-through-yoast-ai
leonidasmi Jun 24, 2026
c4a7159
Fix lintJS
leonidasmi Jun 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 7 additions & 17 deletions packages/js/src/ai-consent/components/revoke-consent.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import ExclamationIcon from "@heroicons/react/outline/ExclamationIcon";
import { useDispatch, useSelect } from "@wordpress/data";
import { useCallback, useState } from "@wordpress/element";
import { __ } from "@wordpress/i18n";
import { Alert, Button, Modal } from "@yoast/ui-library";
import { Button, Modal } from "@yoast/ui-library";

import { STORE_NAME_AI_CONSENT } from "../constants";

Expand All @@ -14,26 +14,22 @@ import { STORE_NAME_AI_CONSENT } from "../constants";
* @returns {JSX.Element} The element.
*/
export const RevokeConsent = ( { onClose } ) => {
const { storeAiGeneratorConsent } = useDispatch( STORE_NAME_AI_CONSENT );
const { storeAiGeneratorConsent, giveAiGeneratorConsent } = useDispatch( STORE_NAME_AI_CONSENT );
const endpoint = useSelect( select => select( STORE_NAME_AI_CONSENT ).selectAiGeneratorConsentEndpoint(), [] );

const [ isLoading, setIsLoading ] = useState( false );
const [ error, setError ] = useState( false );

const handleRevokeConsent = useCallback( async() => {
setError( false );
setIsLoading( true );

const response = await storeAiGeneratorConsent( false, endpoint );
if ( response.consent === false ) {
setError( true );
setIsLoading( false );
return;
}
await storeAiGeneratorConsent( false, endpoint );
// The backend always revokes consent locally, even when the remote call fails, so the
// local store must reflect that regardless of the request's outcome.
giveAiGeneratorConsent( false );
onClose();

setIsLoading( false );
}, [ storeAiGeneratorConsent, setIsLoading, onClose, endpoint ] );
}, [ storeAiGeneratorConsent, giveAiGeneratorConsent, setIsLoading, onClose, endpoint ] );

return (
<div className="yst-flex yst-flex-row">
Expand All @@ -52,12 +48,6 @@ export const RevokeConsent = ( { onClose } ) => {
>
{ __( "Revoke AI consent", "wordpress-seo" ) }
</Modal.Title>
{ error && <Alert
className="yst-mt-2"
variant="error"
>
{ __( "Something went wrong, please try again later.", "wordpress-seo" ) }
</Alert> }
<p className="yst-mt-2 yst-text-slate-600">
{ }
{ __( "By revoking your consent, you will no longer have access to Yoast AI features. Are you sure you want to revoke your consent?", "wordpress-seo" ) }
Expand Down
3 changes: 2 additions & 1 deletion packages/js/src/ai-consent/initialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { __ } from "@wordpress/i18n";
import { Modal, useToggleState } from "@yoast/ui-library";
import classNames from "classnames";
import { get } from "lodash";
import { HAS_AI_GENERATOR_CONSENT_NAME, PLUGIN_URL_NAME, LINK_PARAMS_NAME } from "../shared-admin/store";
import { ADMIN_URL_NAME, HAS_AI_GENERATOR_CONSENT_NAME, PLUGIN_URL_NAME, LINK_PARAMS_NAME } from "../shared-admin/store";
import { GrantConsent } from "./components/grant-consent";
import { RevokeConsent } from "./components/revoke-consent";
import { STORE_NAME_AI_CONSENT } from "./constants";
Expand All @@ -19,6 +19,7 @@ domReady( () => {
},
[ PLUGIN_URL_NAME ]: get( window, "wpseoAiConsent.pluginUrl", "" ),
[ LINK_PARAMS_NAME ]: get( window, "wpseoAiConsent.linkParams", {} ),
[ ADMIN_URL_NAME ]: get( window, "wpseoAiConsent.adminUrl", "" ),
} );

/**
Expand Down
9 changes: 9 additions & 0 deletions packages/js/src/ai-consent/store/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { combineReducers, createReduxStore, register } from "@wordpress/data";
import { merge } from "lodash";
import {
ADMIN_URL_NAME,
adminUrlActions,
adminUrlReducer,
adminUrlSelectors,
getInitialAdminUrlState,
getInitialHasAiGeneratorConsentState,
getInitialLinkParamsState,
getInitialPluginUrlState,
Expand Down Expand Up @@ -32,25 +37,29 @@ const createStore = ( initialState ) => {
...hasAiGeneratorConsentActions,
...pluginUrlActions,
...linkParamsActions,
...adminUrlActions,
},
selectors: {
...hasAiGeneratorConsentSelectors,
...pluginUrlSelectors,
...linkParamsSelectors,
...adminUrlSelectors,
},
initialState: merge(
{},
{
[ HAS_AI_GENERATOR_CONSENT_NAME ]: getInitialHasAiGeneratorConsentState(),
[ PLUGIN_URL_NAME ]: getInitialPluginUrlState(),
[ LINK_PARAMS_NAME ]: getInitialLinkParamsState(),
[ ADMIN_URL_NAME ]: getInitialAdminUrlState(),
},
initialState
),
reducer: combineReducers( {
[ HAS_AI_GENERATOR_CONSENT_NAME ]: hasAiGeneratorConsentReducer,
[ PLUGIN_URL_NAME ]: pluginUrlReducer,
[ LINK_PARAMS_NAME ]: linkParamsReducer,
[ ADMIN_URL_NAME ]: adminUrlReducer,
} ),
controls: {
...hasAiGeneratorConsentControls,
Expand Down
19 changes: 15 additions & 4 deletions packages/js/src/ai-generator/components/errors/generic-alert.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { useSelect } from "@wordpress/data";
import { __, sprintf } from "@wordpress/i18n";
import { Alert } from "@yoast/ui-library";
import PropTypes from "prop-types";
import { safeCreateInterpolateElement } from "../../../helpers/i18n";
import { OutboundLink } from "../../../shared-admin/components";
import { STORE_NAME_EDITOR } from "../../constants";

/**
* @param {string} [linkStoreName] The store to read the common-errors and support links from.
* Defaults to the block editor's store; pass a different store
* name when rendering outside the block editor (e.g. the AI
* consent screen on the user profile page).
* @param {string} [className] An optional class name.
*
* @returns {JSX.Element} The element.
*/
export const GenericAlert = () => {
const commonErrorsLink = useSelect( select => select( STORE_NAME_EDITOR ).selectLink( "https://yoa.st/ai-common-errors" ), [] );
const supportLink = useSelect( select => select( STORE_NAME_EDITOR ).selectAdminLink( "?page=wpseo_page_support" ), [] );
export const GenericAlert = ( { linkStoreName = STORE_NAME_EDITOR, className = "" } ) => {
const commonErrorsLink = useSelect( select => select( linkStoreName ).selectLink( "https://yoa.st/ai-common-errors" ), [ linkStoreName ] );
const supportLink = useSelect( select => select( linkStoreName ).selectAdminLink( "?page=wpseo_page_support" ), [ linkStoreName ] );

return (
<Alert variant="error">
<Alert variant="error" className={ className }>
<span className="yst-block yst-font-medium">{ __( "Something went wrong", "wordpress-seo" ) }</span>
<p className="yst-mt-2">
{ safeCreateInterpolateElement(
Expand All @@ -35,3 +42,7 @@ export const GenericAlert = () => {
</Alert>
);
};
GenericAlert.propTypes = {
linkStoreName: PropTypes.string,
className: PropTypes.string,
};
19 changes: 15 additions & 4 deletions packages/js/src/shared-admin/components/ai-consent.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ArrowNarrowRightIcon from "@heroicons/react/solid/ArrowNarrowRightIcon";
import { useMemo, useCallback } from "@wordpress/element";
import { useMemo, useCallback, useState } from "@wordpress/element";
import { __, sprintf } from "@wordpress/i18n";
import { GenericAlert } from "../../ai-generator/components/errors";
import { Button, useModalContext, useToggleState, Spinner } from "@yoast/ui-library";
import PropTypes from "prop-types";
import { OutboundLink } from ".";
Expand All @@ -14,6 +15,7 @@ import { safeCreateInterpolateElement } from "../../helpers/i18n";
* @param {string} privacyPolicyLink The privacy policy link.
* @param {string} termsOfServiceLink The terms of service link.
* @param {Object} imageLink The thumbnail: img props.
* @param {string} linkStoreName The store to read the error alert's links from.
*
* @returns {JSX.Element} The element.
*/
Expand All @@ -23,9 +25,11 @@ export const AiConsent = ( {
privacyPolicyLink,
termsOfServiceLink,
imageLink,
linkStoreName,
} ) => {
const { onClose, initialFocus } = useModalContext();
const [ consent, toggleConsent ] = useToggleState( false );
const [ error, setError ] = useState( false );

const thumbnail = useMemo( () => ( {
src: imageLink,
Expand Down Expand Up @@ -55,7 +59,10 @@ export const AiConsent = ( {
const [ loading, toggleLoading ] = useToggleState( false );
const handleConsentChange = useCallback( async() => {
toggleLoading();
await onGiveConsent();

const response = await onGiveConsent();
setError( response === false );

toggleLoading();
}, [ onGiveConsent ] );

Expand Down Expand Up @@ -128,15 +135,18 @@ export const AiConsent = ( {
{ checkboxLabel }
</label>
</div>
{ error &&
<GenericAlert className="yst-mt-3" linkStoreName={ linkStoreName } />
}
Comment on lines +138 to +140

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

This probably needs some margin between the text and the alert. Can we get an approval from the UX team for this?

<div className="yst-w-full yst-flex yst-mt-4">
<Button
as="button"
className="yst-grow"
size="large"
disabled={ ! consent }
disabled={ ! consent || error }
onClick={ handleConsentChange }
>
{ loading && (
{ loading && ! error && (
<Spinner className="yst-me-2" />
) }
{ __( "Grant consent", "wordpress-seo" ) }
Expand All @@ -160,4 +170,5 @@ AiConsent.propTypes = {
privacyPolicyLink: PropTypes.string.isRequired,
termsOfServiceLink: PropTypes.string.isRequired,
imageLink: PropTypes.string.isRequired,
linkStoreName: PropTypes.string.isRequired,
};
9 changes: 7 additions & 2 deletions packages/js/src/shared-admin/components/ai-grant-consent.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,12 @@ export const AiGrantConsent = ( { storeName, onConsentGranted, linkStoreName, li

const { storeAiGeneratorConsent } = useDispatch( storeName );
const onGiveConsent = useCallback( async() => {
await storeAiGeneratorConsent( true, endpoint );
onConsentGranted();
const response = await storeAiGeneratorConsent( true, endpoint );

if ( response !== false ) {
onConsentGranted();
}
return response;
}, [ storeAiGeneratorConsent, onConsentGranted, endpoint ] );

return (
Expand All @@ -50,6 +54,7 @@ export const AiGrantConsent = ( { storeName, onConsentGranted, linkStoreName, li
learnMoreLink={ learnMoreLink }
imageLink={ imageLink }
onGiveConsent={ onGiveConsent }
linkStoreName={ linkStoreName }
/>
);
};
Expand Down
4 changes: 2 additions & 2 deletions src/ai-authorization/application/token-manager-interface.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface Token_Manager_Interface {
/**
* Invalidates the access token.
*
* @param string $user_id The user ID.
* @param int $user_id The user ID.
*
* @return void
*
Expand All @@ -35,7 +35,7 @@ interface Token_Manager_Interface {
* @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
* @throws RuntimeException Unable to retrieve the access token.
*/
public function token_invalidate( string $user_id ): void;
public function token_invalidate( int $user_id ): void;

/**
* Requests a new set of JWT tokens.
Expand Down
4 changes: 2 additions & 2 deletions src/ai-authorization/application/token-manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public function __construct(
/**
* Invalidates the access token.
*
* @param string $user_id The user ID.
* @param int $user_id The user ID.
*
* @return void
*
Expand All @@ -126,7 +126,7 @@ public function __construct(
* @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
* @throws RuntimeException Unable to retrieve the access token.
*/
public function token_invalidate( string $user_id ): void {
public function token_invalidate( int $user_id ): void {
try {
$access_jwt = $this->access_token_repository->get_token( $user_id );
} catch ( RuntimeException $e ) {
Expand Down
Loading
Loading