Merge pull request #2925 from woocommerce/PCP-3931-overview-features-and-refresh

New Settings UI: Add feature status to REST data, implement refresh button logic (3931)
This commit is contained in:
Philipp Stracker 2024-12-17 13:54:28 +01:00 committed by GitHub
commit 4949a4fd5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 383 additions and 48 deletions

View file

@ -182,6 +182,26 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
2 2
); );
add_filter(
'woocommerce_paypal_payments_rest_common_merchant_data',
function( array $merchant_data ) use ( $c ): array {
if ( ! isset( $merchant_data['features'] ) ) {
$merchant_data['features'] = array();
}
$product_status = $c->get( 'applepay.apple-product-status' );
assert( $product_status instanceof AppleProductStatus );
$apple_pay_enabled = $product_status->is_active();
$merchant_data['features']['apple_pay'] = array(
'enabled' => $apple_pay_enabled,
);
return $merchant_data;
}
);
return true; return true;
} }

View file

@ -232,6 +232,26 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul
2 2
); );
add_filter(
'woocommerce_paypal_payments_rest_common_merchant_data',
function ( array $merchant_data ) use ( $c ): array {
if ( ! isset( $merchant_data['features'] ) ) {
$merchant_data['features'] = array();
}
$product_status = $c->get( 'googlepay.helpers.apm-product-status' );
assert( $product_status instanceof ApmProductStatus );
$google_pay_enabled = $product_status->is_active();
$merchant_data['features']['google_pay'] = array(
'enabled' => $google_pay_enabled,
);
return $merchant_data;
}
);
return true; return true;
} }
} }

View file

@ -11,42 +11,56 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
} }
return ( return (
<span className="ppcp-r-feature-item__notes"> <>
{ notes.map( ( note, index ) => ( <span className="ppcp-r-feature-item__notes">
<span key={ index }>{ note }</span> { notes.map( ( note, index ) => (
) ) } <span key={ index }>{ note }</span>
</span> ) ) }
</span>
</>
); );
}; };
return ( return (
<SettingsBlock { ...props } className="ppcp-r-settings-block__feature"> <SettingsBlock
<Header> { ...props }
<Title> className="ppcp-r-settings-block__feature"
{ title } components={ [
{ props.actionProps?.featureStatus && ( () => (
<TitleBadge { ...props.actionProps?.badge } /> <>
) } <Header>
</Title> <Title>
<Description className="ppcp-r-settings-block__feature__description"> { title }
{ description } { props.actionProps?.enabled && (
{ printNotes() } <TitleBadge
</Description> { ...props.actionProps?.badge }
</Header> />
<Action> ) }
<div className="ppcp-r-feature-item__buttons"> </Title>
{ props.actionProps?.buttons.map( ( button ) => ( <Description className="ppcp-r-settings-block__feature__description">
<Button { description }
href={ button.url } { printNotes() }
key={ button.text } </Description>
variant={ button.type } </Header>
> <Action>
{ button.text } <div className="ppcp-r-feature-item__buttons">
</Button> { props.actionProps?.buttons.map(
) ) } ( button ) => (
</div> <Button
</Action> href={ button.url }
</SettingsBlock> key={ button.text }
variant={ button.type }
>
{ button.text }
</Button>
)
) }
</div>
</Action>
</>
),
] }
/>
); );
}; };

View file

@ -6,10 +6,42 @@ import TodoSettingsBlock from '../../ReusableComponents/SettingsBlocks/TodoSetti
import FeatureSettingsBlock from '../../ReusableComponents/SettingsBlocks/FeatureSettingsBlock'; import FeatureSettingsBlock from '../../ReusableComponents/SettingsBlocks/FeatureSettingsBlock';
import { TITLE_BADGE_POSITIVE } from '../../ReusableComponents/TitleBadge'; import { TITLE_BADGE_POSITIVE } from '../../ReusableComponents/TitleBadge';
import data from '../../../utils/data'; import data from '../../../utils/data';
import { useMerchantInfo } from '../../../data/common/hooks';
import { useDispatch } from '@wordpress/data';
import { STORE_NAME } from '../../../data/common';
const TabOverview = () => { const TabOverview = () => {
const [ todos, setTodos ] = useState( [] ); const [ todos, setTodos ] = useState( [] );
const [ todosData, setTodosData ] = useState( todosDataDefault ); const [ todosData, setTodosData ] = useState( todosDataDefault );
const [ isRefreshing, setIsRefreshing ] = useState( false );
const { merchant } = useMerchantInfo();
const { refreshFeatureStatuses } = useDispatch( STORE_NAME );
const features = featuresDefault.map( ( feature ) => {
const merchantFeature = merchant?.features?.[ feature.id ];
return {
...feature,
enabled: merchantFeature?.enabled ?? false,
};
} );
const refreshHandler = async () => {
setIsRefreshing( true );
const result = await refreshFeatureStatuses();
if ( result && ! result.success ) {
console.error(
'Failed to refresh features:',
result.message || 'Unknown error'
);
} else {
console.log( 'Features refreshed successfully.' );
}
setIsRefreshing( false );
};
return ( return (
<div className="ppcp-r-tab-overview"> <div className="ppcp-r-tab-overview">
@ -39,30 +71,54 @@ const TabOverview = () => {
title={ __( 'Features', 'woocommerce-paypal-payments' ) } title={ __( 'Features', 'woocommerce-paypal-payments' ) }
description={ description={
<div> <div>
<p>{ __( 'Enable additional features…' ) }</p> <p>
<p>{ __( 'Click Refresh…' ) }</p> { __(
<Button variant="tertiary"> 'Enable additional features…',
'woocommerce-paypal-payments'
) }
</p>
<p>
{ __(
'Click Refresh…',
'woocommerce-paypal-payments'
) }
</p>
<Button
variant="tertiary"
onClick={ refreshHandler }
disabled={ isRefreshing }
>
{ data().getImage( 'icon-refresh.svg' ) } { data().getImage( 'icon-refresh.svg' ) }
{ __( 'Refresh', 'woocommerce-paypal-payments' ) } { isRefreshing
? __(
'Refreshing…',
'woocommerce-paypal-payments'
)
: __(
'Refresh',
'woocommerce-paypal-payments'
) }
</Button> </Button>
</div> </div>
} }
contentItems={ featuresDefault.map( ( feature ) => ( contentItems={ features.map( ( feature ) => (
<FeatureSettingsBlock <FeatureSettingsBlock
key={ feature.id } key={ feature.id }
title={ feature.title } title={ feature.title }
description={ feature.description } description={ feature.description }
actionProps={ { actionProps={ {
buttons: feature.buttons, buttons: feature.buttons,
featureStatus: feature.featureStatus, enabled: feature.enabled,
notes: feature.notes, notes: feature.notes,
badge: { badge: feature.enabled
text: __( ? {
'Active', text: __(
'woocommerce-paypal-payments' 'Active',
), 'woocommerce-paypal-payments'
type: TITLE_BADGE_POSITIVE, ),
}, type: TITLE_BADGE_POSITIVE,
}
: undefined,
} } } }
/> />
) ) } ) ) }
@ -133,7 +189,6 @@ const featuresDefault = [
'Advanced Credit and Debit Cards', 'Advanced Credit and Debit Cards',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
featureStatus: true,
description: __( description: __(
'Process major credit and debit cards including Visa, Mastercard, American Express and Discover.', 'Process major credit and debit cards including Visa, Mastercard, American Express and Discover.',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
@ -181,7 +236,6 @@ const featuresDefault = [
'Let customers pay using their Google Pay wallet.', 'Let customers pay using their Google Pay wallet.',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
featureStatus: true,
buttons: [ buttons: [
{ {
type: 'secondary', type: 'secondary',

View file

@ -23,4 +23,5 @@ export default {
DO_SANDBOX_LOGIN: 'COMMON:DO_SANDBOX_LOGIN', DO_SANDBOX_LOGIN: 'COMMON:DO_SANDBOX_LOGIN',
DO_PRODUCTION_LOGIN: 'COMMON:DO_PRODUCTION_LOGIN', DO_PRODUCTION_LOGIN: 'COMMON:DO_PRODUCTION_LOGIN',
DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT', DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT',
DO_REFRESH_FEATURES: 'DO_REFRESH_FEATURES',
}; };

View file

@ -7,7 +7,7 @@
* @file * @file
*/ */
import { select } from '@wordpress/data'; import { dispatch, select } from '@wordpress/data';
import ACTION_TYPES from './action-types'; import ACTION_TYPES from './action-types';
import { STORE_NAME } from './constants'; import { STORE_NAME } from './constants';
@ -189,3 +189,20 @@ export const connectViaIdAndSecret = function* () {
export const refreshMerchantData = function* () { export const refreshMerchantData = function* () {
return yield { type: ACTION_TYPES.DO_REFRESH_MERCHANT }; return yield { type: ACTION_TYPES.DO_REFRESH_MERCHANT };
}; };
/**
* Side effect.
* Purges all feature status data via a REST request.
* Refreshes the merchant data via a REST request.
*
* @return {Action} The action.
*/
export const refreshFeatureStatuses = function* () {
const result = yield { type: ACTION_TYPES.DO_REFRESH_FEATURES };
if ( result && result.success ) {
return yield dispatch( STORE_NAME ).refreshMerchantData();
}
return result;
};

View file

@ -53,3 +53,14 @@ export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual';
* @type {string} * @type {string}
*/ */
export const REST_CONNECTION_URL_PATH = '/wc/v3/wc_paypal/login_link'; export const REST_CONNECTION_URL_PATH = '/wc/v3/wc_paypal/login_link';
/**
* REST path to refresh the feature status.
*
* Used by: Controls
* See: RefreshFeatureStatusEndpoint.php
*
* @type {string}
*/
export const REST_REFRESH_FEATURES_PATH =
'/wc/v3/wc_paypal/refresh-feature-status';

View file

@ -16,6 +16,7 @@ import {
REST_MANUAL_CONNECTION_PATH, REST_MANUAL_CONNECTION_PATH,
REST_CONNECTION_URL_PATH, REST_CONNECTION_URL_PATH,
REST_HYDRATE_MERCHANT_PATH, REST_HYDRATE_MERCHANT_PATH,
REST_REFRESH_FEATURES_PATH,
} from './constants'; } from './constants';
import ACTION_TYPES from './action-types'; import ACTION_TYPES from './action-types';
@ -121,4 +122,27 @@ export const controls = {
return result; return result;
}, },
async [ ACTION_TYPES.DO_REFRESH_FEATURES ]() {
let result = null;
try {
result = await apiFetch( {
path: REST_REFRESH_FEATURES_PATH,
method: 'POST',
} );
if ( result.success ) {
result = await dispatch( STORE_NAME ).refreshMerchantData();
}
} catch ( e ) {
result = {
success: false,
error: e,
message: e.message,
};
}
return result;
},
}; };

View file

@ -17,6 +17,7 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
@ -70,6 +71,13 @@ return array(
'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint { 'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint {
return new CommonRestEndpoint( $container->get( 'settings.data.common' ) ); return new CommonRestEndpoint( $container->get( 'settings.data.common' ) );
}, },
'settings.rest.refresh_feature_status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint {
return new RefreshFeatureStatusEndpoint(
$container->get( 'wcgateway.settings' ),
new Cache( 'ppcp-timeout' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'settings.rest.connect_manual' => static function ( ContainerInterface $container ) : ConnectManualRestEndpoint { 'settings.rest.connect_manual' => static function ( ContainerInterface $container ) : ConnectManualRestEndpoint {
return new ConnectManualRestEndpoint( return new ConnectManualRestEndpoint(
$container->get( 'api.paypal-host-production' ), $container->get( 'api.paypal-host-production' ),

View file

@ -206,6 +206,11 @@ class CommonRestEndpoint extends RestEndpoint {
$this->merchant_info_map $this->merchant_info_map
); );
$extra_data['merchant'] = apply_filters(
'woocommerce_paypal_payments_rest_common_merchant_data',
$extra_data['merchant'],
);
return $extra_data; return $extra_data;
} }

View file

@ -0,0 +1,132 @@
<?php
/**
* REST endpoint to refresh feature status.
*
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
use WP_REST_Server;
use WP_REST_Response;
use WP_REST_Request;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
* REST controller for refreshing feature status.
*/
class RefreshFeatureStatusEndpoint extends RestEndpoint {
/**
* The base path for this REST controller.
*
* @var string
*/
protected $rest_base = 'refresh-feature-status';
/**
* Cache timeout in seconds.
*
* @var int
*/
private const TIMEOUT = 60;
/**
* Cache key for tracking request timeouts.
*
* @var string
*/
private const CACHE_KEY = 'refresh_feature_status_timeout';
/**
* The settings.
*
* @var ContainerInterface
*/
protected ContainerInterface $settings;
/**
* The cache.
*
* @var Cache
*/
protected Cache $cache;
/**
* The logger.
*
* @var LoggerInterface
*/
protected LoggerInterface $logger;
/**
* Constructor.
*
* @param ContainerInterface $settings The settings.
* @param Cache $cache The cache.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
ContainerInterface $settings,
Cache $cache,
LoggerInterface $logger
) {
$this->settings = $settings;
$this->cache = $cache;
$this->logger = $logger;
}
/**
* Configure REST API routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'refresh_status' ),
'permission_callback' => array( $this, 'check_permission' ),
),
)
);
}
/**
* Handles the refresh status request.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response
*/
public function refresh_status( WP_REST_Request $request ): WP_REST_Response {
$now = time();
$last_request_time = $this->cache->get( self::CACHE_KEY ) ?: 0;
$seconds_missing = $last_request_time + self::TIMEOUT - $now;
if ( $seconds_missing > 0 ) {
return $this->return_error(
sprintf(
// translators: %1$s is the number of seconds remaining.
__( 'Wait %1$s seconds before trying again.', 'woocommerce-paypal-payments' ),
$seconds_missing
)
);
}
$this->cache->set( self::CACHE_KEY, $now, self::TIMEOUT );
do_action( 'woocommerce_paypal_payments_clear_apm_product_status', $this->settings );
$this->logger->info( 'Feature status refreshed successfully' );
return $this->return_success(
array(
'message' => __( 'Feature status refreshed successfully.', 'woocommerce-paypal-payments' ),
)
);
}
}

View file

@ -181,6 +181,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
$container->get( 'settings.rest.common' ), $container->get( 'settings.rest.common' ),
$container->get( 'settings.rest.connect_manual' ), $container->get( 'settings.rest.connect_manual' ),
$container->get( 'settings.rest.login_link' ), $container->get( 'settings.rest.login_link' ),
$container->get( 'settings.rest.refresh_feature_status' ),
); );
foreach ( $endpoints as $endpoint ) { foreach ( $endpoints as $endpoint ) {

View file

@ -13,6 +13,7 @@ use Exception;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Throwable; use Throwable;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message; use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
@ -547,6 +548,33 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
} }
); );
add_filter(
'woocommerce_paypal_payments_rest_common_merchant_data',
function( array $merchant_data ) use ( $c ): array {
if ( ! isset( $merchant_data['features'] ) ) {
$merchant_data['features'] = array();
}
$billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' );
assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint );
$reference_transactions_enabled = $billing_agreements_endpoint->reference_transaction_enabled();
$merchant_data['features']['save_paypal_and_venmo'] = array(
'enabled' => $reference_transactions_enabled,
);
$dcc_product_status = $c->get( 'wcgateway.helper.dcc-product-status' );
assert( $dcc_product_status instanceof DCCProductStatus );
$dcc_enabled = $dcc_product_status->dcc_is_active();
$merchant_data['features']['advanced_credit_and_debit_cards'] = array(
'enabled' => $dcc_enabled,
);
return $merchant_data;
}
);
return true; return true;
} }