mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-08 21:52:55 +08:00
Merge pull request #2940 from woocommerce/PCP-3930-Make-the-webhook-resubscribe/simulate-logic-usable-in-React-application
This commit is contained in:
commit
38e268c68f
20 changed files with 637 additions and 176 deletions
|
@ -1 +1,3 @@
|
||||||
export { default as openSignup } from './Icons/open-signup';
|
export { default as openSignup } from './Icons/open-signup';
|
||||||
|
export const NOTIFICATION_SUCCESS = '✔️';
|
||||||
|
export const NOTIFICATION_ERROR = '❌';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Button } from '@wordpress/components';
|
import { Button } from '@wordpress/components';
|
||||||
import SettingsBlock from './SettingsBlock';
|
import SettingsBlock from './SettingsBlock';
|
||||||
import { Header, Title, Action, Description } from './SettingsBlockElements';
|
import { Action, Description, Header, Title } from './SettingsBlockElements';
|
||||||
|
|
||||||
const ButtonSettingsBlock = ( { title, description, ...props } ) => (
|
const ButtonSettingsBlock = ( { title, description, ...props } ) => (
|
||||||
<SettingsBlock { ...props } className="ppcp-r-settings-block__button">
|
<SettingsBlock { ...props } className="ppcp-r-settings-block__button">
|
||||||
|
@ -10,6 +10,7 @@ const ButtonSettingsBlock = ( { title, description, ...props } ) => (
|
||||||
</Header>
|
</Header>
|
||||||
<Action>
|
<Action>
|
||||||
<Button
|
<Button
|
||||||
|
isBusy={ props.actionProps?.isBusy }
|
||||||
variant={ props.actionProps?.buttonType }
|
variant={ props.actionProps?.buttonType }
|
||||||
onClick={
|
onClick={
|
||||||
props.actionProps?.callback
|
props.actionProps?.callback
|
||||||
|
|
|
@ -1,168 +0,0 @@
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
import {
|
|
||||||
Header,
|
|
||||||
Title,
|
|
||||||
Description,
|
|
||||||
AccordionSettingsBlock,
|
|
||||||
ToggleSettingsBlock,
|
|
||||||
ButtonSettingsBlock,
|
|
||||||
} from '../../../../ReusableComponents/SettingsBlocks';
|
|
||||||
import SettingsBlock from '../../../../ReusableComponents/SettingsBlocks/SettingsBlock';
|
|
||||||
|
|
||||||
const Troubleshooting = ( { updateFormValue, settings } ) => {
|
|
||||||
return (
|
|
||||||
<AccordionSettingsBlock
|
|
||||||
className="ppcp-r-settings-block--troubleshooting"
|
|
||||||
title={ __( 'Troubleshooting', 'woocommerce-paypal-payments' ) }
|
|
||||||
description={ __(
|
|
||||||
'Access tools to help debug and resolve issues.',
|
|
||||||
'woocommerce-paypal-payments'
|
|
||||||
) }
|
|
||||||
actionProps={ {
|
|
||||||
callback: updateFormValue,
|
|
||||||
key: 'payNowExperience',
|
|
||||||
value: settings.payNowExperience,
|
|
||||||
} }
|
|
||||||
>
|
|
||||||
<ToggleSettingsBlock
|
|
||||||
title={ __( 'Logging', 'woocommerce-paypal-payments' ) }
|
|
||||||
description={ __(
|
|
||||||
'Log additional debugging information in the WooCommerce logs that can assist technical staff to determine issues.',
|
|
||||||
'woocommerce-paypal-payments'
|
|
||||||
) }
|
|
||||||
actionProps={ {
|
|
||||||
callback: updateFormValue,
|
|
||||||
key: 'logging',
|
|
||||||
value: settings.logging,
|
|
||||||
} }
|
|
||||||
/>
|
|
||||||
<SettingsBlock>
|
|
||||||
<Header>
|
|
||||||
<Title>
|
|
||||||
{ __(
|
|
||||||
'Subscribed PayPal webhooks',
|
|
||||||
'woocommerce-paypal-payments'
|
|
||||||
) }
|
|
||||||
</Title>
|
|
||||||
<Description>
|
|
||||||
{ __(
|
|
||||||
'The following PayPal webhooks are subscribed. More information about the webhooks is available in the',
|
|
||||||
'woocommerce-paypal-payments'
|
|
||||||
) }{ ' ' }
|
|
||||||
<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#webhook-status">
|
|
||||||
{ __(
|
|
||||||
'Webhook Status documentation',
|
|
||||||
'woocommerce-paypal-payments'
|
|
||||||
) }
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</Description>
|
|
||||||
</Header>
|
|
||||||
<HooksTable data={ hooksExampleData() } />
|
|
||||||
</SettingsBlock>
|
|
||||||
|
|
||||||
<ButtonSettingsBlock
|
|
||||||
title={ __(
|
|
||||||
'Resubscribe webhooks',
|
|
||||||
'woocommerce-paypal-payments'
|
|
||||||
) }
|
|
||||||
description={ __(
|
|
||||||
'Click to remove the current webhook subscription and subscribe again, for example, if the website domain or URL structure changed.',
|
|
||||||
'woocommerce-paypal-payments'
|
|
||||||
) }
|
|
||||||
actionProps={ {
|
|
||||||
buttonType: 'secondary',
|
|
||||||
callback: () =>
|
|
||||||
console.log(
|
|
||||||
'Resubscribe webhooks',
|
|
||||||
'woocommerce-paypal-payments'
|
|
||||||
),
|
|
||||||
value: __(
|
|
||||||
'Resubscribe webhooks',
|
|
||||||
'woocommerce-paypal-payments'
|
|
||||||
),
|
|
||||||
} }
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ButtonSettingsBlock
|
|
||||||
title={ __(
|
|
||||||
'Simulate webhooks',
|
|
||||||
'woocommerce-paypal-payments'
|
|
||||||
) }
|
|
||||||
actionProps={ {
|
|
||||||
buttonType: 'secondary',
|
|
||||||
callback: () =>
|
|
||||||
console.log(
|
|
||||||
'Simulate webhooks',
|
|
||||||
'woocommerce-paypal-payments'
|
|
||||||
),
|
|
||||||
value: __(
|
|
||||||
'Simulate webhooks',
|
|
||||||
'woocommerce-paypal-payments'
|
|
||||||
),
|
|
||||||
} }
|
|
||||||
/>
|
|
||||||
</AccordionSettingsBlock>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hooksExampleData = () => {
|
|
||||||
return {
|
|
||||||
url: 'https://www.rt3.tech/wordpress/paypal-ux-testin/index.php?rest_route=/paypal/v1/incoming',
|
|
||||||
hooks: [
|
|
||||||
'billing plan pricing-change activated',
|
|
||||||
'billing plan updated',
|
|
||||||
'billing subscription cancelled',
|
|
||||||
'catalog product updated',
|
|
||||||
'checkout order approved',
|
|
||||||
'checkout order completed',
|
|
||||||
'checkout payment-approval reversed',
|
|
||||||
'payment authorization voided',
|
|
||||||
'payment capture completed',
|
|
||||||
'payment capture denied',
|
|
||||||
'payment capture pending',
|
|
||||||
'payment capture refunded',
|
|
||||||
'payment capture reversed',
|
|
||||||
'payment order cancelled',
|
|
||||||
'payment sale completed',
|
|
||||||
'payment sale refunded',
|
|
||||||
'vault payment-token created',
|
|
||||||
'vault payment-token deleted',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const HooksTable = ( { data } ) => {
|
|
||||||
return (
|
|
||||||
<table className="ppcp-r-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="ppcp-r-table__hooks-url">
|
|
||||||
{ __( 'URL', 'woocommerce-paypal-payments' ) }
|
|
||||||
</th>
|
|
||||||
<th className="ppcp-r-table__hooks-events">
|
|
||||||
{ __(
|
|
||||||
'Tracked events',
|
|
||||||
'woocommerce-paypal-payments'
|
|
||||||
) }
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td className="ppcp-r-table__hooks-url">{ data?.url }</td>
|
|
||||||
<td className="ppcp-r-table__hooks-events">
|
|
||||||
{ data.hooks.map( ( hook, index ) => (
|
|
||||||
<span key={ hook }>
|
|
||||||
{ hook }{ ' ' }
|
|
||||||
{ index !== data.hooks.length - 1 && ',' }
|
|
||||||
</span>
|
|
||||||
) ) }
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Troubleshooting;
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { CommonHooks } from '../../../../../../data';
|
||||||
|
|
||||||
|
const HooksTableBlock = () => {
|
||||||
|
const { webhooks } = CommonHooks.useWebhooks();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className="ppcp-r-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="ppcp-r-table__hooks-url">
|
||||||
|
{ __( 'URL', 'woocommerce-paypal-payments' ) }
|
||||||
|
</th>
|
||||||
|
<th className="ppcp-r-table__hooks-events">
|
||||||
|
{ __(
|
||||||
|
'Tracked events',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
) }
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td className="ppcp-r-table__hooks-url">
|
||||||
|
{ webhooks?.url }
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="ppcp-r-table__hooks-events"
|
||||||
|
dangerouslySetInnerHTML={ { __html: webhooks?.events } }
|
||||||
|
></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HooksTableBlock;
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { useState } from '@wordpress/element';
|
||||||
|
import { STORE_NAME } from '../../../../../../data/common';
|
||||||
|
import { ButtonSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { useDispatch } from '@wordpress/data';
|
||||||
|
import { store as noticesStore } from '@wordpress/notices';
|
||||||
|
import {
|
||||||
|
NOTIFICATION_ERROR,
|
||||||
|
NOTIFICATION_SUCCESS,
|
||||||
|
} from '../../../../../ReusableComponents/Icons';
|
||||||
|
|
||||||
|
const ResubscribeBlock = () => {
|
||||||
|
const { createSuccessNotice, createErrorNotice } =
|
||||||
|
useDispatch( noticesStore );
|
||||||
|
const [ resubscribing, setResubscribing ] = useState( false );
|
||||||
|
|
||||||
|
const { resubscribeWebhooks } = useDispatch( STORE_NAME );
|
||||||
|
|
||||||
|
const startResubscribingWebhooks = async () => {
|
||||||
|
setResubscribing( true );
|
||||||
|
try {
|
||||||
|
await resubscribeWebhooks();
|
||||||
|
} catch ( error ) {
|
||||||
|
setResubscribing( false );
|
||||||
|
createErrorNotice(
|
||||||
|
__(
|
||||||
|
'Operation failed. Check WooCommerce logs for more details.',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
icon: NOTIFICATION_ERROR,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResubscribing( false );
|
||||||
|
createSuccessNotice(
|
||||||
|
__(
|
||||||
|
'Webhooks were successfully re-subscribed.',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
icon: NOTIFICATION_SUCCESS,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonSettingsBlock
|
||||||
|
title={ __(
|
||||||
|
'Resubscribe webhooks',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
) }
|
||||||
|
description={ __(
|
||||||
|
'Click to remove the current webhook subscription and subscribe again, for example, if the website domain or URL structure changed.',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
) }
|
||||||
|
actionProps={ {
|
||||||
|
buttonType: 'secondary',
|
||||||
|
isBusy: resubscribing,
|
||||||
|
callback: () => startResubscribingWebhooks(),
|
||||||
|
value: __(
|
||||||
|
'Resubscribe webhooks',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
),
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResubscribeBlock;
|
|
@ -0,0 +1,129 @@
|
||||||
|
import { useState } from '@wordpress/element';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { ButtonSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks';
|
||||||
|
import { useDispatch } from '@wordpress/data';
|
||||||
|
import { store as noticesStore } from '@wordpress/notices';
|
||||||
|
import { CommonHooks } from '../../../../../../data';
|
||||||
|
import {
|
||||||
|
NOTIFICATION_ERROR,
|
||||||
|
NOTIFICATION_SUCCESS,
|
||||||
|
} from '../../../../../ReusableComponents/Icons';
|
||||||
|
|
||||||
|
const SimulationBlock = () => {
|
||||||
|
const {
|
||||||
|
createSuccessNotice,
|
||||||
|
createInfoNotice,
|
||||||
|
createErrorNotice,
|
||||||
|
removeNotice,
|
||||||
|
} = useDispatch( noticesStore );
|
||||||
|
const { startWebhookSimulation, checkWebhookSimulationState } =
|
||||||
|
CommonHooks.useWebhooks();
|
||||||
|
const [ simulating, setSimulating ] = useState( false );
|
||||||
|
const sleep = ( ms ) => {
|
||||||
|
return new Promise( ( resolve ) => setTimeout( resolve, ms ) );
|
||||||
|
};
|
||||||
|
const startSimulation = async ( maxRetries ) => {
|
||||||
|
const webhookInfoNoticeId = 'paypal-webhook-simulation-info-notice';
|
||||||
|
const triggerWebhookInfoNotice = () => {
|
||||||
|
createInfoNotice(
|
||||||
|
__(
|
||||||
|
'Waiting for the webhook to arrive…',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: webhookInfoNoticeId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopSimulation = () => {
|
||||||
|
removeNotice( webhookInfoNoticeId );
|
||||||
|
setSimulating( false );
|
||||||
|
};
|
||||||
|
|
||||||
|
setSimulating( true );
|
||||||
|
|
||||||
|
triggerWebhookInfoNotice();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await startWebhookSimulation();
|
||||||
|
} catch ( error ) {
|
||||||
|
console.error( error );
|
||||||
|
setSimulating( false );
|
||||||
|
createErrorNotice(
|
||||||
|
__(
|
||||||
|
'Operation failed. Check WooCommerce logs for more details.',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
icon: NOTIFICATION_ERROR,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( let i = 0; i < maxRetries; i++ ) {
|
||||||
|
await sleep( 2000 );
|
||||||
|
|
||||||
|
const simulationStateResponse = await checkWebhookSimulationState();
|
||||||
|
try {
|
||||||
|
if ( ! simulationStateResponse.success ) {
|
||||||
|
console.error(
|
||||||
|
'Simulation state query failed: ' +
|
||||||
|
simulationStateResponse?.data
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( simulationStateResponse?.data?.state === 'received' ) {
|
||||||
|
createSuccessNotice(
|
||||||
|
__(
|
||||||
|
'The webhook was received successfully.',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
icon: NOTIFICATION_SUCCESS,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
stopSimulation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeNotice( webhookInfoNoticeId );
|
||||||
|
triggerWebhookInfoNotice();
|
||||||
|
} catch ( error ) {
|
||||||
|
console.error( error );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopSimulation();
|
||||||
|
createErrorNotice(
|
||||||
|
__(
|
||||||
|
'Looks like the webhook cannot be received. Check that your website is accessible from the internet.',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
icon: NOTIFICATION_ERROR,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ButtonSettingsBlock
|
||||||
|
title={ __(
|
||||||
|
'Simulate webhooks',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
) }
|
||||||
|
actionProps={ {
|
||||||
|
buttonType: 'secondary',
|
||||||
|
isBusy: simulating,
|
||||||
|
callback: () => startSimulation( 30 ),
|
||||||
|
value: __(
|
||||||
|
'Simulate webhooks',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
),
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SimulationBlock;
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import {
|
||||||
|
AccordionSettingsBlock,
|
||||||
|
Description,
|
||||||
|
Header,
|
||||||
|
Title,
|
||||||
|
ToggleSettingsBlock,
|
||||||
|
} from '../../../../../ReusableComponents/SettingsBlocks';
|
||||||
|
import SettingsBlock from '../../../../../ReusableComponents/SettingsBlocks/SettingsBlock';
|
||||||
|
|
||||||
|
import SimulationBlock from './SimulationBlock';
|
||||||
|
import ResubscribeBlock from './ResubscribeBlock';
|
||||||
|
import HooksTableBlock from './HooksTableBlock';
|
||||||
|
|
||||||
|
const Troubleshooting = ( { updateFormValue, settings } ) => {
|
||||||
|
return (
|
||||||
|
<AccordionSettingsBlock
|
||||||
|
className="ppcp-r-settings-block--troubleshooting"
|
||||||
|
title={ __( 'Troubleshooting', 'woocommerce-paypal-payments' ) }
|
||||||
|
description={ __(
|
||||||
|
'Access tools to help debug and resolve issues.',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
) }
|
||||||
|
actionProps={ {
|
||||||
|
callback: updateFormValue,
|
||||||
|
key: 'payNowExperience',
|
||||||
|
value: settings.payNowExperience,
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
<ToggleSettingsBlock
|
||||||
|
title={ __( 'Logging', 'woocommerce-paypal-payments' ) }
|
||||||
|
description={ __(
|
||||||
|
'Log additional debugging information in the WooCommerce logs that can assist technical staff to determine issues.',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
) }
|
||||||
|
actionProps={ {
|
||||||
|
callback: updateFormValue,
|
||||||
|
key: 'logging',
|
||||||
|
value: settings.logging,
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
<SettingsBlock>
|
||||||
|
<Header>
|
||||||
|
<Title>
|
||||||
|
{ __(
|
||||||
|
'Subscribed PayPal webhooks',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
) }
|
||||||
|
</Title>
|
||||||
|
<Description>
|
||||||
|
{ __(
|
||||||
|
'The following PayPal webhooks are subscribed. More information about the webhooks is available in the',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
) }{ ' ' }
|
||||||
|
<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#webhook-status">
|
||||||
|
{ __(
|
||||||
|
'Webhook Status documentation',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
) }
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</Description>
|
||||||
|
</Header>
|
||||||
|
<HooksTableBlock />
|
||||||
|
<ResubscribeBlock />
|
||||||
|
<SimulationBlock />
|
||||||
|
</SettingsBlock>
|
||||||
|
</AccordionSettingsBlock>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Troubleshooting;
|
|
@ -5,7 +5,7 @@ import {
|
||||||
ContentWrapper,
|
ContentWrapper,
|
||||||
} from '../../../ReusableComponents/SettingsBlocks';
|
} from '../../../ReusableComponents/SettingsBlocks';
|
||||||
import Sandbox from './Blocks/Sandbox';
|
import Sandbox from './Blocks/Sandbox';
|
||||||
import Troubleshooting from './Blocks/Troubleshooting';
|
import Troubleshooting from './Blocks/Troubleshooting/Troubleshooting';
|
||||||
import PaypalSettings from './Blocks/PaypalSettings';
|
import PaypalSettings from './Blocks/PaypalSettings';
|
||||||
import OtherSettings from './Blocks/OtherSettings';
|
import OtherSettings from './Blocks/OtherSettings';
|
||||||
|
|
||||||
|
|
|
@ -24,4 +24,8 @@ export default {
|
||||||
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',
|
DO_REFRESH_FEATURES: 'DO_REFRESH_FEATURES',
|
||||||
|
DO_RESUBSCRIBE_WEBHOOKS: 'COMMON:DO_RESUBSCRIBE_WEBHOOKS',
|
||||||
|
DO_START_WEBHOOK_SIMULATION: 'COMMON:DO_START_WEBHOOK_SIMULATION',
|
||||||
|
DO_CHECK_WEBHOOK_SIMULATION_STATE:
|
||||||
|
'COMMON:DO_CHECK_WEBHOOK_SIMULATION_STATE',
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
* @file
|
* @file
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { dispatch, select } from '@wordpress/data';
|
import { 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';
|
||||||
|
@ -214,3 +214,48 @@ export const refreshFeatureStatuses = function* () {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persistent. Changes the "webhooks" value.
|
||||||
|
*
|
||||||
|
* @param {string} webhooks
|
||||||
|
* @return {Action} The action.
|
||||||
|
*/
|
||||||
|
export const setWebhooks = ( webhooks ) => ( {
|
||||||
|
type: ACTION_TYPES.SET_PERSISTENT,
|
||||||
|
payload: { webhooks },
|
||||||
|
} );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Side effect
|
||||||
|
* Refreshes subscribed webhooks via a REST request
|
||||||
|
*
|
||||||
|
* @return {Action} The action.
|
||||||
|
*/
|
||||||
|
export const resubscribeWebhooks = function* () {
|
||||||
|
const result = yield { type: ACTION_TYPES.DO_RESUBSCRIBE_WEBHOOKS };
|
||||||
|
|
||||||
|
if ( result && result.success ) {
|
||||||
|
yield hydrate( result );
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Side effect. Starts webhook simulation.
|
||||||
|
*
|
||||||
|
* @return {Action} The action.
|
||||||
|
*/
|
||||||
|
export const startWebhookSimulation = function* () {
|
||||||
|
return yield { type: ACTION_TYPES.DO_START_WEBHOOK_SIMULATION };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Side effect. Checks webhook simulation.
|
||||||
|
*
|
||||||
|
* @return {Action} The action.
|
||||||
|
*/
|
||||||
|
export const checkWebhookSimulationState = function* () {
|
||||||
|
return yield { type: ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION_STATE };
|
||||||
|
};
|
||||||
|
|
|
@ -54,6 +54,26 @@ export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual';
|
||||||
*/
|
*/
|
||||||
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 fetch webhooks data or resubscribe webhooks,
|
||||||
|
*
|
||||||
|
* Used by: Controls
|
||||||
|
* See: WebhookSettingsEndpoint.php
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
export const REST_WEBHOOKS = '/wc/v3/wc_paypal/webhook_settings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST path to start webhook simulation and observe the state,
|
||||||
|
*
|
||||||
|
* Used by: Controls
|
||||||
|
* See: WebhookSettingsEndpoint.php
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
export const REST_WEBHOOKS_SIMULATE = '/wc/v3/wc_paypal/webhook_simulate';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* REST path to refresh the feature status.
|
* REST path to refresh the feature status.
|
||||||
*
|
*
|
||||||
|
|
|
@ -10,11 +10,13 @@
|
||||||
import apiFetch from '@wordpress/api-fetch';
|
import apiFetch from '@wordpress/api-fetch';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
REST_PERSIST_PATH,
|
|
||||||
REST_MANUAL_CONNECTION_PATH,
|
|
||||||
REST_CONNECTION_URL_PATH,
|
REST_CONNECTION_URL_PATH,
|
||||||
REST_HYDRATE_MERCHANT_PATH,
|
REST_HYDRATE_MERCHANT_PATH,
|
||||||
|
REST_MANUAL_CONNECTION_PATH,
|
||||||
|
REST_PERSIST_PATH,
|
||||||
REST_REFRESH_FEATURES_PATH,
|
REST_REFRESH_FEATURES_PATH,
|
||||||
|
REST_WEBHOOKS,
|
||||||
|
REST_WEBHOOKS_SIMULATE,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import ACTION_TYPES from './action-types';
|
import ACTION_TYPES from './action-types';
|
||||||
|
|
||||||
|
@ -115,4 +117,24 @@ export const controls = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async [ ACTION_TYPES.DO_RESUBSCRIBE_WEBHOOKS ]() {
|
||||||
|
return await apiFetch( {
|
||||||
|
method: 'POST',
|
||||||
|
path: REST_WEBHOOKS,
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
|
||||||
|
async [ ACTION_TYPES.DO_START_WEBHOOK_SIMULATION ]() {
|
||||||
|
return await apiFetch( {
|
||||||
|
method: 'POST',
|
||||||
|
path: REST_WEBHOOKS_SIMULATE,
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
|
||||||
|
async [ ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION_STATE ]() {
|
||||||
|
return await apiFetch( {
|
||||||
|
path: REST_WEBHOOKS_SIMULATE,
|
||||||
|
} );
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
|
|
||||||
import { useDispatch, useSelect } from '@wordpress/data';
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
import { useCallback } from '@wordpress/element';
|
import { useCallback } from '@wordpress/element';
|
||||||
|
|
||||||
import { STORE_NAME } from './constants';
|
import { STORE_NAME } from './constants';
|
||||||
|
|
||||||
const useTransient = ( key ) =>
|
const useTransient = ( key ) =>
|
||||||
|
@ -34,6 +33,8 @@ const useHooks = () => {
|
||||||
connectToSandbox,
|
connectToSandbox,
|
||||||
connectToProduction,
|
connectToProduction,
|
||||||
connectViaIdAndSecret,
|
connectViaIdAndSecret,
|
||||||
|
startWebhookSimulation,
|
||||||
|
checkWebhookSimulationState,
|
||||||
} = useDispatch( STORE_NAME );
|
} = useDispatch( STORE_NAME );
|
||||||
|
|
||||||
// Transient accessors.
|
// Transient accessors.
|
||||||
|
@ -44,7 +45,7 @@ const useHooks = () => {
|
||||||
const clientSecret = usePersistent( 'clientSecret' );
|
const clientSecret = usePersistent( 'clientSecret' );
|
||||||
const isSandboxMode = usePersistent( 'useSandbox' );
|
const isSandboxMode = usePersistent( 'useSandbox' );
|
||||||
const isManualConnectionMode = usePersistent( 'useManualConnection' );
|
const isManualConnectionMode = usePersistent( 'useManualConnection' );
|
||||||
|
const webhooks = usePersistent( 'webhooks' );
|
||||||
const merchant = useSelect(
|
const merchant = useSelect(
|
||||||
( select ) => select( STORE_NAME ).merchant(),
|
( select ) => select( STORE_NAME ).merchant(),
|
||||||
[]
|
[]
|
||||||
|
@ -82,6 +83,9 @@ const useHooks = () => {
|
||||||
connectViaIdAndSecret,
|
connectViaIdAndSecret,
|
||||||
merchant,
|
merchant,
|
||||||
wooSettings,
|
wooSettings,
|
||||||
|
webhooks,
|
||||||
|
startWebhookSimulation,
|
||||||
|
checkWebhookSimulationState,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -125,6 +129,22 @@ export const useWooSettings = () => {
|
||||||
return wooSettings;
|
return wooSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useWebhooks = () => {
|
||||||
|
const {
|
||||||
|
webhooks,
|
||||||
|
setWebhooks,
|
||||||
|
registerWebhooks,
|
||||||
|
startWebhookSimulation,
|
||||||
|
checkWebhookSimulationState,
|
||||||
|
} = useHooks();
|
||||||
|
return {
|
||||||
|
webhooks,
|
||||||
|
setWebhooks,
|
||||||
|
registerWebhooks,
|
||||||
|
startWebhookSimulation,
|
||||||
|
checkWebhookSimulationState,
|
||||||
|
};
|
||||||
|
};
|
||||||
export const useMerchantInfo = () => {
|
export const useMerchantInfo = () => {
|
||||||
const { merchant } = useHooks();
|
const { merchant } = useHooks();
|
||||||
const { refreshMerchantData } = useDispatch( STORE_NAME );
|
const { refreshMerchantData } = useDispatch( STORE_NAME );
|
||||||
|
|
|
@ -35,6 +35,7 @@ const defaultPersistent = Object.freeze( {
|
||||||
useManualConnection: false,
|
useManualConnection: false,
|
||||||
clientId: '',
|
clientId: '',
|
||||||
clientSecret: '',
|
clientSecret: '',
|
||||||
|
webhooks: [],
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// Reducer logic.
|
// Reducer logic.
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { dispatch } from '@wordpress/data';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { apiFetch } from '@wordpress/data-controls';
|
import { apiFetch } from '@wordpress/data-controls';
|
||||||
|
|
||||||
import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
|
import { STORE_NAME, REST_HYDRATE_PATH, REST_WEBHOOKS } from './constants';
|
||||||
|
|
||||||
export const resolvers = {
|
export const resolvers = {
|
||||||
/**
|
/**
|
||||||
|
@ -21,6 +21,9 @@ export const resolvers = {
|
||||||
*persistentData() {
|
*persistentData() {
|
||||||
try {
|
try {
|
||||||
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
|
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
|
||||||
|
const webhooks = yield apiFetch( { path: REST_WEBHOOKS } );
|
||||||
|
|
||||||
|
result.data = { ...result.data, ...webhooks.data };
|
||||||
|
|
||||||
yield dispatch( STORE_NAME ).hydrate( result );
|
yield dispatch( STORE_NAME ).hydrate( result );
|
||||||
yield dispatch( STORE_NAME ).setIsReady( true );
|
yield dispatch( STORE_NAME ).setIsReady( true );
|
||||||
|
|
|
@ -33,3 +33,7 @@ export const merchant = ( state ) => {
|
||||||
export const wooSettings = ( state ) => {
|
export const wooSettings = ( state ) => {
|
||||||
return getState( state ).wooSettings || EMPTY_OBJ;
|
return getState( state ).wooSettings || EMPTY_OBJ;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const webhooks = ( state ) => {
|
||||||
|
return getState( state ).webhooks || EMPTY_OBJ;
|
||||||
|
};
|
||||||
|
|
|
@ -19,6 +19,7 @@ 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\RefreshFeatureStatusEndpoint;
|
||||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
|
use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
|
||||||
|
use WooCommerce\PayPalCommerce\Settings\Endpoint\WebhookSettingsEndpoint;
|
||||||
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
|
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
|
||||||
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
|
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
|
||||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||||
|
@ -91,6 +92,13 @@ return array(
|
||||||
$container->get( 'settings.service.connection-url-generators' ),
|
$container->get( 'settings.service.connection-url-generators' ),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
'settings.rest.webhooks' => static function ( ContainerInterface $container ) : WebhookSettingsEndpoint {
|
||||||
|
return new WebhookSettingsEndpoint(
|
||||||
|
$container->get( 'api.endpoint.webhook' ),
|
||||||
|
$container->get( 'webhook.registrar' ),
|
||||||
|
$container->get( 'webhook.status.simulation' )
|
||||||
|
);
|
||||||
|
},
|
||||||
'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array {
|
'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array {
|
||||||
return array(
|
return array(
|
||||||
'AR',
|
'AR',
|
||||||
|
|
|
@ -58,6 +58,9 @@ class CommonRestEndpoint extends RestEndpoint {
|
||||||
'js_name' => 'clientSecret',
|
'js_name' => 'clientSecret',
|
||||||
'sanitize' => 'sanitize_text_field',
|
'sanitize' => 'sanitize_text_field',
|
||||||
),
|
),
|
||||||
|
'webhooks' => array(
|
||||||
|
'js_name' => 'webhooks',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
185
modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php
Normal file
185
modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* REST endpoint to manage the onboarding module.
|
||||||
|
*
|
||||||
|
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types = 1 );
|
||||||
|
|
||||||
|
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
|
||||||
|
|
||||||
|
use stdClass;
|
||||||
|
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
|
||||||
|
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhookSimulation;
|
||||||
|
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_REST_Server;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class WebhookSettingsEndpoint
|
||||||
|
*
|
||||||
|
* Note: Endpoint for webhook related requests
|
||||||
|
*/
|
||||||
|
class WebhookSettingsEndpoint extends RestEndpoint {
|
||||||
|
/**
|
||||||
|
* Endpoint base to fetch webhook settings and resubscribe
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rest_base = 'webhook_settings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint base to start webhook simulation and check the state
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected string $rest_simulate_base = 'webhook_simulate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application webhook endpoint
|
||||||
|
*
|
||||||
|
* @var WebhookEndpoint
|
||||||
|
*/
|
||||||
|
private WebhookEndpoint $webhook_endpoint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that allows resubscribing webhooks
|
||||||
|
*
|
||||||
|
* @var WebhookRegistrar
|
||||||
|
*/
|
||||||
|
private WebhookRegistrar $webhook_registrar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that allows webhook simulations
|
||||||
|
*
|
||||||
|
* @var WebhookSimulation
|
||||||
|
*/
|
||||||
|
private WebhookSimulation $webhook_simulation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebhookSettingsEndpoint constructor.
|
||||||
|
*
|
||||||
|
* @param WebhookEndpoint $webhook_endpoint A list of subscribed webhooks and a webhook endpoint URL.
|
||||||
|
* @param WebhookRegistrar $webhook_registrar A service that allows resubscribing webhooks.
|
||||||
|
* @param WebhookSimulation $webhook_simulation A service that allows webhook simulations.
|
||||||
|
*/
|
||||||
|
public function __construct( WebhookEndpoint $webhook_endpoint, WebhookRegistrar $webhook_registrar, WebhookSimulation $webhook_simulation ) {
|
||||||
|
$this->webhook_endpoint = $webhook_endpoint;
|
||||||
|
$this->webhook_registrar = $webhook_registrar;
|
||||||
|
$this->webhook_simulation = $webhook_simulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure REST API routes.
|
||||||
|
*/
|
||||||
|
public function register_routes() {
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base,
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_webhooks' ),
|
||||||
|
'permission_callback' => array( $this, 'check_permission' ),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => array( $this, 'resubscribe_webhooks' ),
|
||||||
|
'permission_callback' => array( $this, 'check_permission' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_simulate_base,
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'check_simulated_webhook_state' ),
|
||||||
|
'permission_callback' => array( $this, 'check_permission' ),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => array( $this, 'simulate_webhooks_start' ),
|
||||||
|
'permission_callback' => array( $this, 'check_permission' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a webhook endpoint URL and list of subscribed webhooks
|
||||||
|
*
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function get_webhooks(): WP_REST_Response {
|
||||||
|
try {
|
||||||
|
$webhook_list = ( $this->webhook_endpoint->list() )[0];
|
||||||
|
$webhook_events = array_map(
|
||||||
|
function ( stdClass $webhook ) {
|
||||||
|
return strtolower( $webhook->name );
|
||||||
|
},
|
||||||
|
$webhook_list->event_types()
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->return_success(
|
||||||
|
array(
|
||||||
|
'webhooks' => array(
|
||||||
|
'url' => $webhook_list->url(),
|
||||||
|
'events' => implode( ', ', $webhook_events ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch ( \Exception $error ) {
|
||||||
|
return $this->return_error( 'Problem while fetching webhooks data' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-subscribes webhooks and returns webhooks
|
||||||
|
*
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function resubscribe_webhooks(): WP_REST_Response {
|
||||||
|
if ( ! $this->webhook_registrar->register() ) {
|
||||||
|
return $this->return_error( 'Webhook subscription failed.' );
|
||||||
|
}
|
||||||
|
return $this->get_webhooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts webhook simulation
|
||||||
|
*
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function simulate_webhooks_start(): WP_REST_Response {
|
||||||
|
try {
|
||||||
|
$this->webhook_simulation->start();
|
||||||
|
return $this->return_success( array() );
|
||||||
|
} catch ( \Exception $error ) {
|
||||||
|
return $this->return_error( $error->getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks webhook simulation state
|
||||||
|
*
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function check_simulated_webhook_state(): WP_REST_Response {
|
||||||
|
try {
|
||||||
|
$state = $this->webhook_simulation->get_state();
|
||||||
|
|
||||||
|
return $this->return_success(
|
||||||
|
array(
|
||||||
|
'state' => $state,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch ( \Exception $error ) {
|
||||||
|
return $this->return_error( $error->getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.webhooks' ),
|
||||||
$container->get( 'settings.rest.refresh_feature_status' ),
|
$container->get( 'settings.rest.refresh_feature_status' ),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue