feat(integrations-page): add the MyYoast connection card and OAuth client controls#23395
feat(integrations-page): add the MyYoast connection card and OAuth client controls#23395diedexx wants to merge 28 commits into
Conversation
…ions Reintroduces Rate_Limited_Exception and Registration_Not_Found_Exception (both extending Registration_Failed_Exception) and maps the relevant HTTP statuses onto them in the registration client: a 429 from DCR, read, or RFC 7592 update becomes a Rate_Limited_Exception carrying the parsed Retry-After, and a 401/404 on read/update becomes a Registration_Not_Found_Exception after clearing the local registration. This gives the REST layer the retry-after countdown and a distinct "registration gone" path that the user-facing connection card relies on, which the generic Registration_Failed_Exception alone could not provide. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a dedicated OAuth callback endpoint on admin-post.php and registers it as the site's redirect URI through the client's redirect-URI provider filters, replacing the base client's placeholder admin-page default. The endpoint is reachable from any page the flow may have started on; the per-flow return URL sends the user back to where they began. On the returning request it exchanges the authorization code — which the authorization-code handler now also marks the redirect URI validated for — and stashes a one-shot per-user outcome the connection card surfaces. Adds Authorization_Code_Handler::discard_flow_state() to clear a pending flow when the provider returns an error (e.g. the user denied consent). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ent API Updates the REST management route and status presenter for the reworked client: registration takes no caller-supplied redirect URIs (the client resolves them itself), so register and the URL-change recovery both call ensure_registered(), and authorize no longer accepts or validates a redirect_uri — the client picks the registered one. The status presenter now reports real per-URI verification state via the registration's is_uri_validated(), and derives redirect_uris_match from the redirect-URI provider instead of a standalone builder. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the MyYoast connection card to the Recommended integrations section: a standalone wp.data store seeded from the localized status, the card UI (connect, per-state notices for connection-lost and verification-needed, disconnect via a confirm modal), and the entry/recommended-integrations wiring. Connecting continues straight into the authorization-code flow. The verify/authorize action no longer sends a redirect URI — the client resolves the registered one — and heroicons are imported per-icon to satisfy the current lint rule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The remote registration liveness check was named "verify", which collided with the cryptographic Jwt_Signer::verify and, more confusingly, with the site-verification product feature in the integrations UI (isVerified, verificationNeeded, handleVerify). Renaming to refresh_registration_status / refresh-status disambiguates the two and better reflects that the call reconciles local state with the server, self-healing by forgetting the registration on 401/404 rather than performing a pure check. Renames the application facade method, the REST route (POST /myyoast/refresh-status) and its callback, the WP-CLI subcommand (wp yoast auth refresh-status), the JS store endpoint/action, and the corresponding unit tests. The crypto verify and all site-verification identifiers are left untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…he App layer Move the authorization-code callback use-case out of the UI integration and into a new App-layer OAuth_Callback_Handler plus a Callback_Outcome value object. The handler does the transport-agnostic work — discard pending flow state on a provider error, exchange the code, persist the outcome for one-shot surfacing, and report it in neutral OAuth terms — so any consumer (admin-post endpoint, REST route, WP-CLI) can drive it and translate the outcome onto its own surface. OAuth_Callback_Integration now only extracts the request parameters and delegates; Integrations_Page_Script_Data consumes the stored outcome through the handler and maps it to the front-end message keys. This keeps the onion dependency rule intact: the orchestration lives in App, the WordPress-specific adapters stay in User_Interface. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…he OAuth return URL Two additions to the MyYoast management route: Refresh throttle — a successful upstream status refresh now sets a short-lived, issuer-scoped marker that suppresses further upstream reads for an hour, so the integrations page reloading does not hit MyYoast's aggressive RFC 7592 rate limit. Only the fact that we checked is stored, never the response body, so the endpoint's no-store contract holds. Connect, re-sync, and disconnect clear the marker so a deliberate state change is reflected on the next read. A failed or rate-limited attempt does not set the marker, leaving the next retry free. Caller-supplied return URL — POST /myyoast/authorize accepts an optional return_url, since the flow can be started from admin pages other than the integrations page. It is validated against the site's own host and dropped when off-site or invalid (the callback re-validates before redirecting, so this is the first of two open-redirect gates). When absent, null flows through and the callback surfaces a standalone outcome instead of redirecting. Also narrows the over-broad Throwable catches to the exception types each endpoint can actually raise, and renames respond_with_status to respond_with_connection_status for clarity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…re folder Restructure the MyYoast connection from loose files plus a flat `myyoast-store.js` into a self-contained feature folder, matching the `ai-generator` and `introductions` layout where a feature directory is named for the store it owns. - Split the store into a `store/` directory: a `myyoast-connection` slice module and an `index.js` that builds and registers it. - Register through `createReduxStore` + `register` rather than the deprecated `registerStore`. - Centralize the store name in `constants.js` so the store and its consumers share one address without a circular import. - Add unit tests for the slice, generator actions, controls-driven branches, selectors, and the status transform. - Surface backend message codes through a `messageFor` switch so only the matched string is translated at call time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The authorize action accepted a return URL, but the card never passed one,
so after the OAuth round-trip the backend fell back to its default instead
of returning the user to the integrations page.
Pass the current page as the return URL from both the connect and verify
flows. The backend validates it and ignores it when off-site or invalid.
Take the return URL as a named option (`{ returnUrl }`) so the call site is
self-documenting and the action can grow more options without a signature
change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…necting Connecting registers the site as an OAuth client and then redirects to MyYoast to complete the authorization-code grant. Registration leaves the site registered-but-unverified, and `window.location.assign` navigates asynchronously, so the card kept rendering — and briefly showed the "Verification needed" notice — while the MyYoast page loaded. Track the connect flow with a local `isConnecting` flag that suppresses the notice, and keep it set through the redirect tail. It is cleared only when we stay on the page (registration failed, or authorize returned no URL) so the notice and error still surface there. Scoped to the connect auto-flow, so a deliberate verify click still shows its context. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The OAuth callback emits one success notice for both first-time setup and a standalone re-verify, so "This site is now verified" read as jargon right after connecting. Describe the end state instead: the connection is active. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the trailing underscore from the prefix constant and the redundant rtrim() that stripped it, so the throttle key is built in one step. The resulting transient key is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reword the card heading and description to lead with the offline/firewall benefit, and replace the terse connection-lost message with an actionable one explaining that the site's URL changed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y_after_seconds The parameter was only documented in PHPDoc; add the native type declaration to match the rest of the class and enable static analysis. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ith no fallback AI_Request_Sender::send rethrew the primary strategy's exception silently when no fallback was configured, unlike the fallback paths which log. Add a matching warning before the rethrow so the failure is traceable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nection Drop the top border and padding on the registered-state section so the site-connection details no longer sit under a horizontal rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oasts Move the transient connect/verify/reconnect/disconnect and OAuth callback outcomes from an inline card Alert to a bottom-left toast, matching the notification pattern used elsewhere in the plugin. Each outcome carries a fresh id so the toast remounts and re-animates. Success toasts auto-dismiss after five seconds; errors persist until dismissed. The connection-state alerts (connection lost, verification needed, not provisioned) stay inline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Coverage Report for CI Build 0Warning No base build found for commit Coverage: 44.544%Details
Uncovered Changes
Coverage RegressionsRequires a base build to compare against. How to fix this → Coverage Stats💛 - Coveralls |
…the card The verification-needed tooltip used a centered top position on an icon pinned to the card's right edge, so it overflowed the card's overflow-hidden boundary and clipped its text. Anchor it to the right (top-left) and narrow it so it stacks taller and stays within the card. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds the MyYoast connection surface and supporting backend/REST/OAuth callback plumbing needed for site admins to connect, verify, and disconnect a site from MyYoast via OAuth, exposed on the Integrations page and backed by a small orchestration layer (status presenter, management route, callback integration + outcome persistence).
Changes:
- Adds MyYoast connection status/script-data providers and wires them into the Integrations page localized payload, plus a new React “MyYoast” card and data store.
- Introduces REST management endpoints for registration/refresh/authorize/deregister, and an
admin-post.phpOAuth callback handler that records one-shot outcomes for UI toasts. - Extends MyYoast client registration handling with typed exceptions for “registration gone” and “rate limited” (including
Retry-Afterparsing), plus additional tests.
Reviewed changes
Copilot reviewed 36 out of 37 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Unit/MyYoast_Client/User_Interface/Status_Presenter_Test.php | Unit coverage for the status payload shaping (provisioned/registered/redirect URI state). |
| tests/Unit/MyYoast_Client/User_Interface/OAuth_Callback_Integration_Test.php | Tests for the admin-post callback integration wiring and return-url behavior. |
| tests/Unit/MyYoast_Client/User_Interface/Management_Route_Test.php | Tests for the MyYoast management REST endpoints and exception mapping. |
| tests/Unit/MyYoast_Client/User_Interface/Integrations_Page_Script_Data_Test.php | Tests for Integrations page script-data shaping (status/profile URL/callback outcome). |
| tests/Unit/MyYoast_Client/Infrastructure/WordPress/Redirect_URI_Provider_Test.php | Updates redirect URI expectations to the admin-post callback endpoint. |
| tests/Unit/MyYoast_Client/Infrastructure/Registration/Client_Registration_Test.php | Adds coverage for 401/404 “gone” and 429 rate-limiting behaviors. |
| tests/Unit/MyYoast_Client/Application/Registration_Not_Found_Exception_Test.php | Unit coverage for the new typed “registration gone” exception. |
| tests/Unit/MyYoast_Client/Application/Rate_Limited_Exception_Test.php | Unit coverage for the new rate-limit exception and Retry-After parsing. |
| tests/Unit/MyYoast_Client/Application/OAuth_Callback_Handler_Test.php | Tests the callback handler orchestration and outcome persistence/consumption. |
| tests/Unit/MyYoast_Client/Application/Callback_Outcome_Test.php | Unit tests for the callback outcome value object and array round-trip. |
| tests/Unit/Integrations/Admin/Integrations_Page_Integration_Test.php | Updates integrations-page localization payload tests to include myyoast_connection. |
| tests/Unit/AI/Authentication/Application/AI_Request_Sender/AI_Request_Sender_Test.php | Adds test coverage for new warning logging when no fallback is configured. |
| src/myyoast-client/user-interface/status-presenter.php | New presenter building the minimal status payload for the connection card/UI. |
| src/myyoast-client/user-interface/oauth-callback-integration.php | New admin-post OAuth callback integration that drives the callback handler and redirects back. |
| src/myyoast-client/user-interface/management-route.php | New REST management endpoints for status/refresh/register/update/authorize/deregister. |
| src/myyoast-client/user-interface/integrations-page-script-data.php | New script-data provider for Integrations page boot payload (status, profile URL, callback toast outcome). |
| src/myyoast-client/user-interface/auth-command.php | Renames/adjusts WP-CLI subcommand to “refresh-status” and updates messaging. |
| src/myyoast-client/infrastructure/wordpress/redirect-uri-provider.php | Switches canonical redirect URI to the admin-post callback endpoint. |
| src/myyoast-client/infrastructure/registration/client-registration.php | Adds typed exceptions for 401/404 and 429 handling + Retry-After extraction. |
| src/myyoast-client/application/oauth-callback-handler.php | New callback orchestration that exchanges code and persists a one-shot outcome per user. |
| src/myyoast-client/application/myyoast-client.php | Renames verify method to refresh_registration_status() and updates doc. |
| src/myyoast-client/application/exceptions/registration-not-found-exception.php | New typed exception for “registration no longer exists”. |
| src/myyoast-client/application/exceptions/rate-limited-exception.php | New typed exception for rate limiting with retry-after support. |
| src/myyoast-client/application/callback-outcome.php | New value object describing callback outcomes in OAuth terms. |
| src/myyoast-client/application/authorization-code-handler.php | Refactors flow-state deletion into a reusable discard_flow_state() method. |
| src/integrations/admin/integrations-page.php | Injects/exports MyYoast connection script-data into the Integrations page payload. |
| src/integrations/admin/helpscout-beacon.php | Switches integrations page reference to Integrations_Page::PAGE. |
| src/ai/authentication/application/ai-request-sender.php | Adds warning log when primary strategy fails and no fallback exists. |
| packages/js/tests/integrations-page/myyoast-connection/store/myyoast-connection.test.js | Jest coverage for MyYoast connection store reducers/selectors/actions. |
| packages/js/src/integrations-page/recommended-integrations.js | Prepends the MyYoast card when the localized payload is present (feature flag on). |
| packages/js/src/integrations-page/myyoast-connection/store/myyoast-connection.js | Implements the MyYoast connection slice + generator actions + REST controls. |
| packages/js/src/integrations-page/myyoast-connection/store/index.js | Registers the standalone WP data store seeded from wpseoIntegrationsData. |
| packages/js/src/integrations-page/myyoast-connection/myyoast-integration.js | Adds the MyYoast connection card UI, action flows, and toast surfacing. |
| packages/js/src/integrations-page/myyoast-connection/myyoast-disconnect-modal.js | Adds the disconnect confirmation modal UI. |
| packages/js/src/integrations-page/myyoast-connection/constants.js | Adds store-name constant to avoid circular imports. |
| packages/js/src/integrations-page.js | Registers the MyYoast store on page load before rendering. |
| packages/js/images/myyoast-logo.svg | Adds the MyYoast logo asset for the card header. |
…es it The authorize endpoint returned invalid_user with HTTP 401, but the JS store reads error_code only from successful (2xx) bodies; api-fetch rejects non-2xx, so the dedicated invalid_user message was masked as a generic unexpected_error. Return it as HTTP 200 with the error_code in the body, like every other failure on this endpoint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ference The redirect-URI provider and OAuth callback integration docblocks still described the canonical redirect URI as an admin page reached via provider filters, but it is now the admin-post.php callback endpoint resolved directly from get_callback_url(). Also fixes a @Covers annotation that referenced the renamed respond_with_connection_status method. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… UTM-tag outbound links The integrations page never registered the yoast-seo/settings store, so the selectLink machinery the other cards rely on is unavailable here. Surface the short-link query params through the MyYoast script data and store instead, so the card can append the same attribution params the other cards carry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…other cards Switch the bespoke anchor to the shared Link component, the RTL-aware ArrowSmRightIcon, and the same markup the SimpleIntegration cards use, and add the missing screen-reader 'opens in a new tab' text. Point the link at the real integrations-myyoast URL and append the localized UTM params so it carries the same attribution as the other cards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…f showing a fake error A double-click on "Connect your site" before the button re-rendered as disabled let the second click hit runAction's in-flight guard, which returns the action_in_flight sentinel. handleConnect routed that through resolveErrorMessage, where messageFor has no matching case and falls back to the generic "Something went wrong" string, surfacing a spurious error toast for an action that was deliberately dropped. Extract the sentinel to a named constant and detect it in handleConnect so the dropped action is ignored silently. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ectors
The profileUrl was computed in PHP (admin_url('profile.php')), localized, stored,
and exposed via a selector, but no component ever read it; likewise
selectMyyoastConnectionProfileUrl, selectHasMyyoastConnection, and
selectMyyoastConnectionActionError were referenced only by their own tests. Remove
the field, its localization, the PHP computation, and the three unused selectors
along with their assertions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ction The error_message_key() docblock referenced buildMessages() in myyoast-integration.js, but the front-end maps message keys to copy in messageFor(). Correct the reference so a maintainer adding an error code can find the matching JS handler. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… unconnected MyYoast card The design specs a 2px yoast-purple/200 (#e0b3cc) border on the MyYoast connection card, which the initial build missed. Add an optional className prop to the shared integrations Card (appended so a consumer can override the default border) and pass yst-border-2 yst-border-primary-200 from the MyYoast card only while the site is not yet connected; once registered the outline is dropped so the card blends in with the others. The other integration cards are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tokens' of github.com:Yoast/wordpress-seo into DE/rt1168-oauth-client-controls
…tokens' of github.com:Yoast/wordpress-seo into DE/rt1168-oauth-client-controls
Context
Authenticating Yoast AI requests with MyYoast OAuth tokens (the parent feature, PR #23265) needs a way for site admins to actually connect their site to MyYoast: register the site as an OAuth client, complete the authorization-code grant, see the connection state, and disconnect. This PR adds that user-facing surface on the Integrations page, plus the client-side controls and REST/orchestration layer behind it.
This branch is stacked on top of
1207-authenticate-yoast-ai-requests-with-myyoast-oauth-tokensand targets it (nottrunk), so the diff is limited to the connection-card work.Summary
This PR can be summarized in the following changelog entry:
Relevant technical choices:
admin-post.phpvia theadmin_post_*hook (logged-in only, no_nopriv), with CSRF protection viahash_equalson thestateparameter and open-redirect prevention viawp_validate_redirectat both the management route and the callback. admin-post is used as a neutral spot in the wp-admin that is accessible to all that can access wp-admin. This helps when implementing a per-user login down the line (see yoast/reserved-tasks#1166)wpseo_manage_optionsthrough a sharedpermission_callback.Test instructions
Test instructions for the acceptance test before the PR gets merged
This PR can be acceptance tested by following these steps:
Setup
Switch to AI staging APIDomain Dropdownselectstaging-5as your MyYoast testing domain.MyYoast OAuth overridesenter your PAT from an admin user (sourced from staging MyYoast), save, and click 'Fetch credentials'. Make sure it now saysStored credentials: yes.Regression
YOAST_SEO_MYYOAST_CONNECTIONis NOT defined inwp-config.php(or isfalse).Connect
define( 'YOAST_SEO_MYYOAST_CONNECTION', true );towp-config.php.Relevant test scenarios
Test instructions for QA when the code is in the RC
QA can test this PR by following these steps:
Impact check
This PR affects the following parts of the plugin, which may require extra testing:
Other environments
[shopify-seo], added test instructions for Shopify and attached theShopifylabel to this PR.[yoast-doc-extension], added test instructions for Yoast SEO for Google Docs and attached theGoogle Docs Add-onlabel to this PR.Documentation
Quality assurance
grunt build:imagesand committed the results, if my PR introduces or edits images or SVGs.Innovation
innovationlabel.Fixes Yoast/reserved-tasks#1168