Add react-powered main payments settings screen (#50825)

* Fix payment store selector type

* Add changelog

* Add react-powered payment settings main screen

* Add changelog

* Update style

* Revert changes

* Fix enable payment gateway error

* Fix wcpay install busy state

* Check if Nonce exist or not

* Fix extra payment methods

* Fix untranslated texts

* Fix lint
This commit is contained in:
Chi-Hsuan Huang 2024-08-27 14:30:01 +08:00 committed by GitHub
parent cc07a5f902
commit c63bb88e0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 548 additions and 10 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix payment store selector type

View File

@ -13,7 +13,7 @@ import * as resolvers from './resolvers';
import * as selectors from './selectors';
import reducer from './reducer';
import { STORE_KEY } from './constants';
import { WPDataActions } from '../types';
import { WPDataSelectors } from '../types';
import { PromiseifySelectors } from '../types/promiseify-selectors';
export const PAYMENT_GATEWAYS_STORE_NAME = STORE_KEY;
@ -33,7 +33,7 @@ declare module '@wordpress/data' {
): DispatchFromMap< typeof actions >;
function select(
key: typeof STORE_KEY
): SelectFromMap< typeof selectors > & WPDataActions;
): SelectFromMap< typeof selectors > & WPDataSelectors;
function resolveSelect(
key: typeof STORE_KEY
): PromiseifySelectors< SelectFromMap< typeof selectors > >;

View File

@ -0,0 +1,155 @@
/**
* External dependencies
*/
import React, { useMemo } from '@wordpress/element';
import { Button } from '@wordpress/components';
import ExternalIcon from 'gridicons/dist/external';
import { __, _x } from '@wordpress/i18n';
import {
ONBOARDING_STORE_NAME,
PAYMENT_GATEWAYS_STORE_NAME,
SETTINGS_STORE_NAME,
} from '@woocommerce/data';
import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { getCountryCode } from '~/dashboard/utils';
import {
getEnrichedPaymentGateways,
getIsGatewayWCPay,
getIsWCPayOrOtherCategoryDoneSetup,
getSplitGateways,
} from '~/task-lists/fills/PaymentGatewaySuggestions/utils';
type PaymentGateway = {
id: string;
image_72x72: string;
title: string;
enabled: boolean;
needsSetup: boolean;
// Add other properties as needed...
};
const usePaymentGatewayData = () => {
return useSelect( ( select ) => {
const { getSettings } = select( SETTINGS_STORE_NAME );
const { general: settings = {} } = getSettings( 'general' );
return {
getPaymentGateway: select( PAYMENT_GATEWAYS_STORE_NAME )
.getPaymentGateway,
installedPaymentGateways: select(
PAYMENT_GATEWAYS_STORE_NAME
).getPaymentGateways(),
isResolving: select( ONBOARDING_STORE_NAME ).isResolving(
'getPaymentGatewaySuggestions'
),
paymentGatewaySuggestions: select(
ONBOARDING_STORE_NAME
).getPaymentGatewaySuggestions(),
countryCode: getCountryCode( settings.woocommerce_default_country ),
};
}, [] );
};
const AdditionalGatewayImages = ( {
additionalGateways,
}: {
additionalGateways: PaymentGateway[];
} ) => (
<>
{ additionalGateways.map( ( gateway ) => (
<img
key={ gateway.id }
src={ gateway.image_72x72 }
alt={ gateway.title }
width="24"
height="24"
className="other-payment-methods__image"
/>
) ) }
{ _x( '& more.', 'More payment providers to discover', 'woocommerce' ) }
</>
);
export const OtherPaymentMethods = () => {
const {
paymentGatewaySuggestions,
installedPaymentGateways,
isResolving,
countryCode,
} = usePaymentGatewayData();
const paymentGateways = useMemo(
() =>
getEnrichedPaymentGateways(
installedPaymentGateways,
paymentGatewaySuggestions
),
[ installedPaymentGateways, paymentGatewaySuggestions ]
);
const isWCPayOrOtherCategoryDoneSetup = useMemo(
() =>
getIsWCPayOrOtherCategoryDoneSetup( paymentGateways, countryCode ),
[ countryCode, paymentGateways ]
);
const isWCPaySupported = Array.from( paymentGateways.values() ).some(
getIsGatewayWCPay
);
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const [ wcPayGateway, _offlineGateways, additionalGateways ] = useMemo(
() =>
getSplitGateways(
paymentGateways,
countryCode ?? '',
isWCPaySupported,
isWCPayOrOtherCategoryDoneSetup
),
[
paymentGateways,
countryCode,
isWCPaySupported,
isWCPayOrOtherCategoryDoneSetup,
]
);
if ( isResolving || ! wcPayGateway ) {
return null;
}
const hasWcPaySetup = wcPayGateway.enabled && ! wcPayGateway.needsSetup;
return (
<>
<Button
className="is-tertiary"
href="https://woocommerce.com/product-category/woocommerce-extensions/payment-gateways/?utm_source=payments_recommendations"
target="_blank"
value="tertiary"
rel="noreferrer"
>
<span className="other-payment-methods__button-text">
{ hasWcPaySetup
? __(
'Discover additional payment providers',
'woocommerce'
)
: __(
'Discover other payment providers',
'woocommerce'
) }
</span>
<ExternalIcon size={ 18 } />
</Button>
{ additionalGateways.length > 0 && (
<AdditionalGatewayImages
additionalGateways={ additionalGateways }
/>
) }
</>
);
};

View File

@ -0,0 +1,183 @@
/**
* External dependencies
*/
import { useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { PaymentGateway } from '@woocommerce/data';
import { WooPaymentMethodsLogos } from '@woocommerce/onboarding';
/**
* Internal dependencies
*/
import { getAdminSetting } from '~/utils/admin-settings';
import sanitizeHTML from '~/lib/sanitize-html';
import { WCPayInstallButton } from './wcpay-install-button';
export const PaymentMethod = ( {
id,
enabled,
title,
method_title,
method_description,
settings_url,
}: PaymentGateway ) => {
const isWooPayEligible = getAdminSetting( 'isWooPayEligible', false );
const [ isEnabled, setIsEnabled ] = useState( enabled );
const [ isLoading, setIsLoading ] = useState( false );
const toggleEnabled = async ( e: React.MouseEvent ) => {
e.preventDefault();
setIsLoading( true );
if ( ! window.woocommerce_admin.nonces?.gateway_toggle ) {
// eslint-disable-next-line no-console
console.warn( 'Unexpected error: Nonce not found' );
// Redirect to payment setting page if nonce is not found. Users should still be able to toggle the payment method from that page.
window.location.href = settings_url;
return;
}
try {
const response = await fetch( window.woocommerce_admin.ajax_url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams( {
action: 'woocommerce_toggle_gateway_enabled',
security: window.woocommerce_admin.nonces?.gateway_toggle,
gateway_id: id,
} ),
} );
const result = await response.json();
if ( result.success ) {
if ( result.data === true ) {
setIsEnabled( true );
} else if ( result.data === false ) {
setIsEnabled( false );
} else if ( result.data === 'needs_setup' ) {
window.location.href = settings_url;
}
} else {
window.location.href = settings_url;
}
} catch ( error ) {
// eslint-disable-next-line no-console
console.error( 'Error toggling gateway:', error );
} finally {
setIsLoading( false );
}
};
return (
<tr data-gateway_id={ id }>
<td className="sort ui-sortable-handle" width="1%"></td>
<td className="name" width="">
<div className="wc-payment-gateway-method__name">
<a
href={ settings_url }
className="wc-payment-gateway-method-title"
>
{ method_title }
</a>
{ id !== 'pre_install_woocommerce_payments_promotion' &&
method_title !== title && (
<span className="wc-payment-gateway-method-name">
&nbsp;&nbsp;
{ title }
</span>
) }
{ id === 'pre_install_woocommerce_payments_promotion' && (
<div className="pre-install-payment-gateway__subtitle">
<WooPaymentMethodsLogos
isWooPayEligible={ isWooPayEligible }
maxElements={ 5 }
/>
</div>
) }
</div>
</td>
<td className="status" width="1%">
<a
className="wc-payment-gateway-method-toggle-enabled"
href={ settings_url }
onClick={ toggleEnabled }
>
<span
className={ `woocommerce-input-toggle ${
isEnabled
? 'woocommerce-input-toggle--enabled'
: 'woocommerce-input-toggle--disabled'
} ${
isLoading ? 'woocommerce-input-toggle--loading' : ''
}` }
/* translators: %s: payment method title */
aria-label={
isEnabled
? sprintf(
/* translators: %s: payment method title */
__(
'The "%s" payment method is currently enabled',
'woocommerce'
),
method_title
)
: sprintf(
/* translators: %s: payment method title */
__(
'The "%s" payment method is currently disabled',
'woocommerce'
),
method_title
)
}
>
{ isEnabled
? __( 'Yes', 'woocommerce' )
: __( 'No', 'woocommerce' ) }
</span>
</a>
</td>
<td
className="description"
width=""
dangerouslySetInnerHTML={ sanitizeHTML( method_description ) }
/>
<td className="action" width="1%">
{ id === 'pre_install_woocommerce_payments_promotion' ? (
<WCPayInstallButton />
) : (
<a
className="button alignright"
aria-label={
enabled
? sprintf(
/* translators: %s: payment method title */
__(
'Manage the "%s" payment method',
'woocommerce'
),
method_title
)
: sprintf(
/* translators: %s: payment method title */
__(
'Set up the "%s" payment method',
'woocommerce'
),
method_title
)
}
href={ settings_url }
>
{ enabled
? __( 'Manage', 'woocommerce' )
: __( 'Finish setup', 'woocommerce' ) }
</a>
) }
</td>
</tr>
);
};

View File

@ -0,0 +1,60 @@
/**
* External dependencies
*/
import React, { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import {
PAYMENT_GATEWAYS_STORE_NAME,
PLUGINS_STORE_NAME,
} from '@woocommerce/data';
import { Button } from '@wordpress/components';
import { resolveSelect, useDispatch } from '@wordpress/data';
import { recordEvent } from '@woocommerce/tracks';
const slug = 'woocommerce-payments';
export const WCPayInstallButton = () => {
const [ installing, setInstalling ] = useState( false );
const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME );
const { createNotice } = useDispatch( 'core/notices' );
const redirectToSettings = async () => {
const paymentGateway = await resolveSelect(
PAYMENT_GATEWAYS_STORE_NAME
).getPaymentGateway( slug.replace( /-/g, '_' ) );
if ( paymentGateway?.settings_url ) {
window.location.href = paymentGateway.settings_url;
}
};
const installWooCommercePayments = async () => {
if ( installing ) return;
setInstalling( true );
recordEvent( 'settings_payments_recommendations_setup', {
extension_selected: slug,
} );
try {
await installAndActivatePlugins( [ slug ] );
redirectToSettings();
} catch ( error ) {
if ( error instanceof Error ) {
createNotice( 'error', error.message );
}
setInstalling( false );
}
};
return (
<Button
className="button alignright"
onClick={ installWooCommercePayments }
variant="secondary"
isBusy={ installing }
aria-disabled={ installing }
>
{ __( 'Install', 'woocommerce' ) }
</Button>
);
};

View File

@ -1,9 +1,29 @@
@import "~/wp-admin-scripts/payment-method-promotions/payment-promotion-row.scss";
.settings-payments-main__container {
h1 {
color: #fff;
}
background: #000;
text-align: center;
padding: 50px 0;
.settings-payments-main__spinner {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
width: 100%;
}
table.wc_gateways {
.other-payment-methods__button-text {
margin-right: 4px;
}
td.other-payment-methods-row {
border-top: 1px solid #c3c4c7;
background-color: #fff;
}
.other-payment-methods__image {
vertical-align: middle;
margin-right: 8px;
}
}
}

View File

@ -1,17 +1,108 @@
/**
* External dependencies
*/
import '@wordpress/element';
import { useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { PaymentGateway } from '@woocommerce/data';
/**
* Internal dependencies
*/
import './settings-payments-main.scss';
import { PaymentMethod } from './components/payment-method';
import { OtherPaymentMethods } from './components/other-payment-methods';
import { PaymentsBannerWrapper } from '~/payments/payment-settings-banner';
export const SettingsPaymentsMain: React.FC = () => {
const [ paymentGateways, error ] = useMemo( () => {
const script = document.getElementById(
'experimental_wc_settings_payments_gateways'
);
try {
if ( script && script.textContent ) {
return [
JSON.parse( script.textContent ) as PaymentGateway[],
null,
];
}
throw new Error( 'Could not find payment gateways data' );
} catch ( e ) {
return [ [], e as Error ];
}
}, [] );
if ( error ) {
// This is a temporary error message to be replaced by error boundary.
return (
<div>
<h1>
{ __( 'Error loading payment gateways', 'woocommerce' ) }
</h1>
<p>{ error.message }</p>
</div>
);
}
return (
<div className="settings-payments-main__container">
<h1>Main payments screen</h1>
<div id="wc_payments_settings_slotfill">
<PaymentsBannerWrapper />
</div>
<table className="form-table">
<tbody>
<tr>
<td
className="wc_payment_gateways_wrapper"
colSpan={ 2 }
>
<table
className="wc_gateways widefat"
cellSpacing="0"
aria-describedby="payment_gateways_options-description"
>
<thead>
<tr>
<th className="sort"></th>
<th className="name">
{ __( 'Method', 'woocommerce' ) }
</th>
<th className="status">
{ __( 'Enabled', 'woocommerce' ) }
</th>
<th className="description">
{ __(
'Description',
'woocommerce'
) }
</th>
<th className="action"></th>
</tr>
</thead>
<tbody className="ui-sortable">
{ paymentGateways.map(
( gateway: PaymentGateway ) => (
<PaymentMethod
key={ gateway.id }
{ ...gateway }
/>
)
) }
<tr>
<td
className="other-payment-methods-row"
colSpan={ 5 }
>
<OtherPaymentMethods />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
);
};

View File

@ -88,6 +88,12 @@ declare global {
getUserSetting?: ( name: string ) => string | undefined;
setUserSetting?: ( name: string, value: string ) => void;
deleteUserSetting?: ( name: string ) => void;
woocommerce_admin: {
ajax_url: string;
nonces: {
gateway_toggle?: string;
}
}
}
}

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add react-powered main payments settings screen

View File

@ -57,6 +57,16 @@ class WC_Settings_Payment_Gateways_React extends WC_Settings_Page {
//phpcs:disable WordPress.Security.NonceVerification.Recommended
global $current_section;
// We don't want to output anything from the action for now. So we buffer it and discard it.
ob_start();
/**
* Fires before the payment gateways settings fields are rendered.
*
* @since 1.5.7
*/
do_action( 'woocommerce_admin_field_payment_gateways' );
ob_end_clean();
// Load gateways so we can show any global options they may have.
$payment_gateways = WC()->payment_gateways->payment_gateways();
@ -91,6 +101,11 @@ class WC_Settings_Payment_Gateways_React extends WC_Settings_Page {
global $hide_save_button;
$hide_save_button = true;
echo '<div id="experimental_wc_settings_payments_' . esc_attr( $section ) . '"></div>';
// Output the gateways data to the page so the React app can use it.
$controller = new WC_REST_Payment_Gateways_Controller();
$response = $controller->get_items( new WP_REST_Request( 'GET', '/wc/v3/payment_gateways' ) );
echo '<script type="application/json" id="experimental_wc_settings_payments_gateways">' . wp_json_encode( $response->data ) . '</script>';
}
/**