diff --git a/inc/Abilities/AuthAbilities.php b/inc/Abilities/AuthAbilities.php index a43fd7a28..38a67d1cb 100644 --- a/inc/Abilities/AuthAbilities.php +++ b/inc/Abilities/AuthAbilities.php @@ -431,6 +431,7 @@ public function executeListProviders( array $input ): array { 'is_configured' => method_exists( $instance, 'is_configured' ) ? $instance->is_configured() : false, 'is_authenticated' => $is_authenticated, 'auth_fields' => method_exists( $instance, 'get_config_fields' ) ? $instance->get_config_fields() : array(), + 'config_values' => method_exists( $instance, 'get_config' ) ? $instance->get_config() : array(), 'callback_url' => null, 'account_details' => null, ); diff --git a/inc/Core/Admin/Settings/assets/react/SettingsApp.jsx b/inc/Core/Admin/Settings/assets/react/SettingsApp.jsx index 585a88da1..5eabf8863 100644 --- a/inc/Core/Admin/Settings/assets/react/SettingsApp.jsx +++ b/inc/Core/Admin/Settings/assets/react/SettingsApp.jsx @@ -2,13 +2,15 @@ * SettingsApp Component * * Root container for the Settings admin page with tabbed navigation. + * Handles OAuth callback feedback by reading query params and showing notices. */ /** * WordPress dependencies */ -import { useCallback } from '@wordpress/element'; -import { TabPanel } from '@wordpress/components'; +import { useCallback, useState, useEffect } from '@wordpress/element'; +import { TabPanel, Notice } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -34,17 +36,98 @@ const getInitialTab = () => { : 'general'; }; +/** + * Get OAuth callback feedback from URL query params. + * + * @returns {Object|null} Feedback object or null if no OAuth params present. + */ +const getOAuthFeedbackFromUrl = () => { + const urlParams = new URLSearchParams( window.location.search ); + const authSuccess = urlParams.get( 'auth_success' ); + const authError = urlParams.get( 'auth_error' ); + const provider = urlParams.get( 'provider' ); + + if ( ! authSuccess && ! authError ) { + return null; + } + + if ( authSuccess ) { + return { + type: 'success', + message: provider + ? /* translators: %s: provider name (e.g., Pinterest) */ + sprintf( __( 'Successfully connected to %s.', 'data-machine' ), provider ) + : __( 'Successfully connected.', 'data-machine' ), + }; + } + + // Map error codes to user-friendly messages. + const errorMessages = { + denied: __( 'Authorization was denied or cancelled.', 'data-machine' ), + invalid_state: __( 'Invalid OAuth state. Please try again.', 'data-machine' ), + token_exchange_failed: __( 'Failed to exchange authorization code for token.', 'data-machine' ), + token_transform_failed: __( 'Failed to process authentication token.', 'data-machine' ), + account_fetch_failed: __( 'Failed to retrieve account details.', 'data-machine' ), + storage_failed: __( 'Failed to save authentication data.', 'data-machine' ), + missing_config: __( 'OAuth credentials not configured.', 'data-machine' ), + }; + + return { + type: 'error', + message: errorMessages[ authError ] || + /* translators: %s: error code */ + sprintf( __( 'Authentication failed: %s', 'data-machine' ), authError ), + }; +}; + +/** + * Clean up OAuth query params from URL without reloading. + */ +const cleanOAuthParamsFromUrl = () => { + const url = new URL( window.location.href ); + url.searchParams.delete( 'auth_success' ); + url.searchParams.delete( 'auth_error' ); + url.searchParams.delete( 'provider' ); + window.history.replaceState( {}, '', url.toString() ); +}; + const SettingsApp = () => { + const [ activeTab, setActiveTab ] = useState( getInitialTab() ); + const [ oauthNotice, setOauthNotice ] = useState( null ); + + // Handle OAuth callback feedback on mount. + useEffect( () => { + const feedback = getOAuthFeedbackFromUrl(); + if ( feedback ) { + setOauthNotice( feedback ); + // Auto-switch to Auth Providers tab. + setActiveTab( 'auth-providers' ); + localStorage.setItem( STORAGE_KEY, 'auth-providers' ); + // Clean up URL. + cleanOAuthParamsFromUrl(); + } + }, [] ); + const handleSelect = useCallback( ( tabName ) => { + setActiveTab( tabName ); localStorage.setItem( STORAGE_KEY, tabName ); }, [] ); return (
+ { oauthNotice && ( + setOauthNotice( null ) } + > + { oauthNotice.message } + + ) } { ( tab ) => { diff --git a/inc/Core/Admin/Settings/assets/react/components/tabs/AuthProvidersTab.jsx b/inc/Core/Admin/Settings/assets/react/components/tabs/AuthProvidersTab.jsx index 1010e4f8a..7f56b103e 100644 --- a/inc/Core/Admin/Settings/assets/react/components/tabs/AuthProvidersTab.jsx +++ b/inc/Core/Admin/Settings/assets/react/components/tabs/AuthProvidersTab.jsx @@ -11,7 +11,7 @@ /** * WordPress dependencies */ -import { useState, useCallback } from '@wordpress/element'; +import { useState, useCallback, useEffect } from '@wordpress/element'; import { Button, Notice } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; @@ -112,15 +112,26 @@ const InlineAccountDetails = ( { account } ) => { const ConfigForm = ( { provider, onSave, isSaving } ) => { const fields = provider.auth_fields || {}; const fieldEntries = Object.entries( fields ); + const savedConfig = provider.config_values || {}; const [ values, setValues ] = useState( () => { const initial = {}; fieldEntries.forEach( ( [ key, field ] ) => { - initial[ key ] = field.default || ''; + // Use saved config value, fall back to field default or empty string. + initial[ key ] = savedConfig[ key ] ?? field.default ?? ''; } ); return initial; } ); + // Sync values when savedConfig changes (e.g., after save/refetch). + useEffect( () => { + const updated = {}; + fieldEntries.forEach( ( [ key, field ] ) => { + updated[ key ] = savedConfig[ key ] ?? field.default ?? ''; + } ); + setValues( updated ); + }, [ savedConfig, fieldEntries ] ); + if ( fieldEntries.length === 0 ) { return (