Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions inc/Abilities/AuthAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand Down
89 changes: 86 additions & 3 deletions inc/Core/Admin/Settings/assets/react/SettingsApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 (
<div className="datamachine-settings-app">
{ oauthNotice && (
<Notice
status={ oauthNotice.type }
isDismissible
onRemove={ () => setOauthNotice( null ) }
>
{ oauthNotice.message }
</Notice>
) }
<TabPanel
className="datamachine-tabs"
tabs={ TABS }
initialTabName={ getInitialTab() }
initialTabName={ activeTab }
onSelect={ handleSelect }
>
{ ( tab ) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 (
<p className="description">
Expand Down
Loading