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:
parent
cc07a5f902
commit
c63bb88e0e
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Fix payment store selector type
|
|
@ -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 > >;
|
||||
|
|
|
@ -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 }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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">
|
||||
–
|
||||
{ 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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -1,9 +1,29 @@
|
|||
@import "~/wp-admin-scripts/payment-method-promotions/payment-promotion-row.scss";
|
||||
|
||||
.settings-payments-main__container {
|
||||
h1 {
|
||||
color: #fff;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
background: #000;
|
||||
text-align: center;
|
||||
padding: 50px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add react-powered main payments settings screen
|
|
@ -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>';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue