Merge remote-tracking branch 'origin/trunk' into PCP-3816-Create-payment-placeholder-page-in-new-settings-module

# Conflicts:
#	modules/ppcp-settings/resources/css/style.scss
This commit is contained in:
inpsyde-maticluznar 2024-11-04 13:28:57 +01:00
commit 6b9b024d64
No known key found for this signature in database
GPG key ID: D005973F231309F6
55 changed files with 2943 additions and 1137 deletions

View file

@ -407,6 +407,11 @@ class ApplePayButton {
.querySelectorAll( 'style#ppcp-hide-apple-pay' )
.forEach( ( el ) => el.remove() );
const paymentMethodAppleLi = document.querySelector('.wc_payment_method.payment_method_ppcp-applepay' );
if (paymentMethodAppleLi.style.display === 'none' || paymentMethodAppleLi.style.display === '') {
paymentMethodAppleLi.style.display = 'block';
}
this.allElements.forEach( ( element ) => {
element.style.display = '';
} );

View file

@ -0,0 +1,21 @@
import { useMemo } from '@wordpress/element';
/**
* Custom hook returning the allowed shipping locations based on configuration.
*
* @param {Object} axoConfig - The AXO configuration object.
* @param {Array|undefined} axoConfig.enabled_shipping_locations - The list of enabled shipping locations.
* @return {Array} The final list of allowed shipping locations.
*/
const useAllowedLocations = ( axoConfig ) => {
return useMemo( () => {
const enabledShippingLocations =
axoConfig.enabled_shipping_locations || [];
return Array.isArray( enabledShippingLocations )
? enabledShippingLocations
: [];
}, [ axoConfig.enabled_shipping_locations ] );
};
export default useAllowedLocations;

View file

@ -3,6 +3,7 @@ import { useSelect } from '@wordpress/data';
import Fastlane from '../../../../ppcp-axo/resources/js/Connection/Fastlane';
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
import { useDeleteEmptyKeys } from './useDeleteEmptyKeys';
import useAllowedLocations from './useAllowedLocations';
import { STORE_NAME } from '../stores/axoStore';
/**
@ -30,6 +31,8 @@ const useFastlaneSdk = ( namespace, axoConfig, ppcpConfig ) => {
return deleteEmptyKeys( configRef.current.axoConfig.style_options );
}, [ deleteEmptyKeys ] );
const allowedLocations = useAllowedLocations( axoConfig );
// Effect to initialize Fastlane SDK
useEffect( () => {
const initFastlane = async () => {
@ -52,6 +55,9 @@ const useFastlaneSdk = ( namespace, axoConfig, ppcpConfig ) => {
await fastlane.connect( {
locale: configRef.current.ppcpConfig.locale,
styles: styleOptions,
shippingAddressOptions: {
allowedLocations,
},
} );
// Set locale (hardcoded to 'en_us' for now)
@ -66,7 +72,13 @@ const useFastlaneSdk = ( namespace, axoConfig, ppcpConfig ) => {
};
initFastlane();
}, [ fastlaneSdk, styleOptions, isPayPalLoaded, namespace ] );
}, [
fastlaneSdk,
styleOptions,
isPayPalLoaded,
namespace,
allowedLocations,
] );
// Effect to update the config ref when configs change
useEffect( () => {

View file

@ -37,7 +37,8 @@ return array(
$container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.configuration.dcc' ),
$container->get( 'onboarding.environment' ),
$container->get( 'wcgateway.url' )
$container->get( 'wcgateway.url' ),
$container->get( 'axo.shipping-wc-enabled-locations' )
);
},
);

View file

@ -79,6 +79,13 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
*/
private $wcgateway_module_url;
/**
* The list of WooCommerce enabled shipping locations.
*
* @var array
*/
private array $enabled_shipping_locations;
/**
* AdvancedCardPaymentMethod constructor.
*
@ -91,6 +98,7 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
* @param DCCGatewayConfiguration $dcc_configuration The DCC gateway settings.
* @param Environment $environment The environment object.
* @param string $wcgateway_module_url The WcGateway module URL.
* @param array $enabled_shipping_locations The list of WooCommerce enabled shipping locations.
*/
public function __construct(
string $module_url,
@ -100,18 +108,19 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
Settings $settings,
DCCGatewayConfiguration $dcc_configuration,
Environment $environment,
string $wcgateway_module_url
string $wcgateway_module_url,
array $enabled_shipping_locations
) {
$this->name = AxoGateway::ID;
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->smart_button = $smart_button;
$this->settings = $settings;
$this->dcc_configuration = $dcc_configuration;
$this->environment = $environment;
$this->wcgateway_module_url = $wcgateway_module_url;
$this->name = AxoGateway::ID;
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->smart_button = $smart_button;
$this->settings = $settings;
$this->dcc_configuration = $dcc_configuration;
$this->environment = $environment;
$this->wcgateway_module_url = $wcgateway_module_url;
$this->enabled_shipping_locations = $enabled_shipping_locations;
}
/**
@ -187,13 +196,13 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
}
return array(
'environment' => array(
'environment' => array(
'is_sandbox' => $this->environment->current_environment() === 'sandbox',
),
'widgets' => array(
'widgets' => array(
'email' => 'render',
),
'insights' => array(
'insights' => array(
'enabled' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'client_id' => ( $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : null ),
'session_id' =>
@ -207,7 +216,8 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
: null, // Set to null if WC()->cart is null or get_total doesn't exist.
),
),
'style_options' => array(
'enabled_shipping_locations' => $this->enabled_shipping_locations,
'style_options' => array(
'root' => array(
'backgroundColor' => $this->settings->has( 'axo_style_root_bg_color' ) ? $this->settings->get( 'axo_style_root_bg_color' ) : '',
'errorColor' => $this->settings->has( 'axo_style_root_error_color' ) ? $this->settings->get( 'axo_style_root_error_color' ) : '',
@ -226,23 +236,23 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
'focusBorderColor' => $this->settings->has( 'axo_style_input_focus_border_color' ) ? $this->settings->get( 'axo_style_input_focus_border_color' ) : '',
),
),
'name_on_card' => $this->dcc_configuration->show_name_on_card(),
'woocommerce' => array(
'name_on_card' => $this->dcc_configuration->show_name_on_card(),
'woocommerce' => array(
'states' => array(
'US' => WC()->countries->get_states( 'US' ),
'CA' => WC()->countries->get_states( 'CA' ),
),
),
'icons_directory' => esc_url( $this->wcgateway_module_url ) . 'assets/images/axo/',
'module_url' => untrailingslashit( $this->module_url ),
'ajax' => array(
'icons_directory' => esc_url( $this->wcgateway_module_url ) . 'assets/images/axo/',
'module_url' => untrailingslashit( $this->module_url ),
'ajax' => array(
'frontend_logger' => array(
'endpoint' => \WC_AJAX::get_endpoint( FrontendLoggerEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( FrontendLoggerEndpoint::nonce() ),
),
),
'logging_enabled' => $this->settings->has( 'logging_enabled' ) ? $this->settings->get( 'logging_enabled' ) : '',
'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'logging_enabled' => $this->settings->has( 'logging_enabled' ) ? $this->settings->get( 'logging_enabled' ) : '',
'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
);
}
}

View file

@ -53,7 +53,7 @@ class AxoManager {
cardView = null;
constructor( namespace, axoConfig, ppcpConfig ) {
this.namespace = namespace;
this.namespace = namespace;
this.axoConfig = axoConfig;
this.ppcpConfig = ppcpConfig;
@ -85,6 +85,9 @@ class AxoManager {
},
};
this.enabledShippingLocations =
this.axoConfig.enabled_shipping_locations;
this.registerEventHandlers();
this.shippingView = new ShippingView(
@ -661,6 +664,9 @@ class AxoManager {
await this.fastlane.connect( {
locale: this.locale,
styles: this.styles,
shippingAddressOptions: {
allowedLocations: this.enabledShippingLocations,
},
} );
this.fastlane.setLocale( 'en_us' );

View file

@ -67,7 +67,8 @@ return array(
$container->get( 'wcgateway.settings.status' ),
$container->get( 'api.shop.currency.getter' ),
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'wcgateway.url' )
$container->get( 'wcgateway.url' ),
$container->get( 'axo.shipping-wc-enabled-locations' )
);
},
@ -251,4 +252,46 @@ return array(
$active_plugins_list
);
},
'axo.shipping-wc-enabled-locations' => static function ( ContainerInterface $container ): array {
$default_zone = new \WC_Shipping_Zone( 0 );
$is_method_enabled = fn( \WC_Shipping_Method $method): bool => $method->enabled === 'yes';
$is_default_zone_enabled = ! empty(
array_filter(
$default_zone->get_shipping_methods(),
$is_method_enabled
)
);
if ( $is_default_zone_enabled ) {
return array();
}
$shipping_zones = \WC_Shipping_Zones::get_zones();
$get_zone_locations = fn( \WC_Shipping_Zone $zone): array =>
! empty( array_filter( $zone->get_shipping_methods(), $is_method_enabled ) )
? array_map(
fn( object $location): string => $location->code,
$zone->get_zone_locations()
)
: array();
$enabled_locations = array_unique(
array_merge(
...array_map(
$get_zone_locations,
array_map(
fn( $zone): \WC_Shipping_Zone =>
$zone instanceof \WC_Shipping_Zone ? $zone : new \WC_Shipping_Zone( $zone['id'] ),
$shipping_zones
)
)
)
);
return $enabled_locations;
},
);

View file

@ -87,6 +87,13 @@ class AxoManager {
*/
private $wcgateway_module_url;
/**
* The list of WooCommerce enabled shipping locations.
*
* @var array
*/
private array $enabled_shipping_locations;
/**
* AxoManager constructor.
*
@ -99,6 +106,7 @@ class AxoManager {
* @param CurrencyGetter $currency The getter of the 3-letter currency code of the shop.
* @param LoggerInterface $logger The logger.
* @param string $wcgateway_module_url The WcGateway module URL.
* @param array $enabled_shipping_locations The list of WooCommerce enabled shipping locations.
*/
public function __construct(
string $module_url,
@ -109,18 +117,20 @@ class AxoManager {
SettingsStatus $settings_status,
CurrencyGetter $currency,
LoggerInterface $logger,
string $wcgateway_module_url
string $wcgateway_module_url,
array $enabled_shipping_locations
) {
$this->module_url = $module_url;
$this->version = $version;
$this->session_handler = $session_handler;
$this->settings = $settings;
$this->environment = $environment;
$this->settings_status = $settings_status;
$this->currency = $currency;
$this->logger = $logger;
$this->wcgateway_module_url = $wcgateway_module_url;
$this->module_url = $module_url;
$this->version = $version;
$this->session_handler = $session_handler;
$this->settings = $settings;
$this->environment = $environment;
$this->settings_status = $settings_status;
$this->currency = $currency;
$this->logger = $logger;
$this->wcgateway_module_url = $wcgateway_module_url;
$this->enabled_shipping_locations = $enabled_shipping_locations;
}
/**
@ -163,13 +173,13 @@ class AxoManager {
*/
private function script_data() {
return array(
'environment' => array(
'environment' => array(
'is_sandbox' => $this->environment->current_environment() === 'sandbox',
),
'widgets' => array(
'widgets' => array(
'email' => 'render',
),
'insights' => array(
'insights' => array(
'enabled' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'client_id' => ( $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : null ),
'session_id' =>
@ -183,7 +193,8 @@ class AxoManager {
'value' => WC()->cart->get_total( 'numeric' ),
),
),
'style_options' => array(
'enabled_shipping_locations' => $this->enabled_shipping_locations,
'style_options' => array(
'root' => array(
'backgroundColor' => $this->settings->has( 'axo_style_root_bg_color' ) ? $this->settings->get( 'axo_style_root_bg_color' ) : '',
'errorColor' => $this->settings->has( 'axo_style_root_error_color' ) ? $this->settings->get( 'axo_style_root_error_color' ) : '',
@ -202,24 +213,24 @@ class AxoManager {
'focusBorderColor' => $this->settings->has( 'axo_style_input_focus_border_color' ) ? $this->settings->get( 'axo_style_input_focus_border_color' ) : '',
),
),
'name_on_card' => $this->settings->has( 'axo_name_on_card' ) ? $this->settings->get( 'axo_name_on_card' ) : '',
'woocommerce' => array(
'name_on_card' => $this->settings->has( 'axo_name_on_card' ) ? $this->settings->get( 'axo_name_on_card' ) : '',
'woocommerce' => array(
'states' => array(
'US' => WC()->countries->get_states( 'US' ),
'CA' => WC()->countries->get_states( 'CA' ),
),
),
'icons_directory' => esc_url( $this->wcgateway_module_url ) . 'assets/images/axo/',
'module_url' => untrailingslashit( $this->module_url ),
'ajax' => array(
'icons_directory' => esc_url( $this->wcgateway_module_url ) . 'assets/images/axo/',
'module_url' => untrailingslashit( $this->module_url ),
'ajax' => array(
'frontend_logger' => array(
'endpoint' => \WC_AJAX::get_endpoint( FrontendLoggerEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( FrontendLoggerEndpoint::nonce() ),
),
),
'logging_enabled' => $this->settings->has( 'logging_enabled' ) ? $this->settings->get( 'logging_enabled' ) : '',
'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'billing_email_button_text' => __( 'Continue', 'woocommerce-paypal-payments' ),
'logging_enabled' => $this->settings->has( 'logging_enabled' ) ? $this->settings->get( 'logging_enabled' ) : '',
'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'billing_email_button_text' => __( 'Continue', 'woocommerce-paypal-payments' ),
);
}

View file

@ -222,8 +222,10 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
// Render submit button.
add_action(
$manager->checkout_button_renderer_hook(),
static function () use ( $c, $manager ) {
$manager->render_checkout_button();
static function () use ( $c, $manager, $module ) {
if ( $module->should_render_fastlane( $c ) ) {
$manager->render_checkout_button();
}
}
);

View file

@ -688,7 +688,7 @@ const PayPalComponent = ( {
);
};
const BlockEditorPayPalComponent = () => {
const BlockEditorPayPalComponent = ({ fundingSource } ) => {
const urlParams = {
clientId: 'test',
...config.scriptData.url_params,
@ -701,6 +701,7 @@ const BlockEditorPayPalComponent = () => {
onClick={ ( data, actions ) => {
return false;
} }
fundingSource={ fundingSource }
/>
</PayPalScriptProvider>
);
@ -786,7 +787,7 @@ if ( block_enabled ) {
name: config.id,
label: <div dangerouslySetInnerHTML={ { __html: config.title } } />,
content: <PayPalComponent isEditing={ false } />,
edit: <BlockEditorPayPalComponent />,
edit: <BlockEditorPayPalComponent fundingSource={ 'paypal' }/>,
ariaLabel: config.title,
canMakePayment: () => {
return true;
@ -818,7 +819,9 @@ if ( block_enabled ) {
fundingSource={ fundingSource }
/>
),
edit: <BlockEditorPayPalComponent />,
edit: <BlockEditorPayPalComponent
fundingSource={ fundingSource }
/>,
ariaLabel: config.title,
canMakePayment: async () => {
if ( ! paypalScriptPromise ) {

View file

@ -38,6 +38,12 @@ class CardFieldsFreeTrialRenderer {
if ( hideDccGateway ) {
hideDccGateway.parentNode.removeChild( hideDccGateway );
}
const dccGatewayLi = document.querySelector(
'.wc_payment_method.payment_method_ppcp-credit-card-gateway'
);
if (dccGatewayLi.style.display === 'none' || dccGatewayLi.style.display === '') {
dccGatewayLi.style.display = 'block';
}
this.errorHandler.clear();

View file

@ -44,6 +44,12 @@ class CardFieldsRenderer {
if ( hideDccGateway ) {
hideDccGateway.parentNode.removeChild( hideDccGateway );
}
const dccGatewayLi = document.querySelector(
'.wc_payment_method.payment_method_ppcp-credit-card-gateway'
);
if (dccGatewayLi.style.display === 'none' || dccGatewayLi.style.display === '') {
dccGatewayLi.style.display = 'block';
}
const cardFields = paypal.CardFields( {
createOrder: contextConfig.createOrder,

View file

@ -52,6 +52,12 @@ class HostedFieldsRenderer {
if ( hideDccGateway ) {
hideDccGateway.parentNode.removeChild( hideDccGateway );
}
const dccGatewayLi = document.querySelector(
'.wc_payment_method.payment_method_ppcp-credit-card-gateway'
);
if (dccGatewayLi.style.display === 'none' || dccGatewayLi.style.display === '') {
dccGatewayLi.style.display = 'block';
}
const cardNumberField = document.querySelector(
'#ppcp-credit-card-gateway-card-number'

View file

@ -763,11 +763,16 @@ export default class PaymentButton {
const styleSelector = `style[data-hide-gateway="${ this.methodId }"]`;
const wrapperSelector = `#${ this.wrappers.Default }`;
const paymentMethodLi = document.querySelector(`.wc_payment_method.payment_method_${ this.methodId }`);
document
.querySelectorAll( styleSelector )
.forEach( ( el ) => el.remove() );
if (paymentMethodLi.style.display === 'none' || paymentMethodLi.style.display === '') {
paymentMethodLi.style.display = 'block';
}
document
.querySelectorAll( wrapperSelector )
.forEach( ( el ) => el.remove() );

0
modules/ppcp-settings/node_modules/.gitkeep generated vendored Normal file
View file

View file

@ -7,10 +7,12 @@
"build": "wp-scripts build --webpack-src-dir=resources/js --output-path=assets"
},
"devDependencies": {
"@woocommerce/navigation": "8.1.0",
"@wordpress/data": "^10.10.0",
"@wordpress/data-controls": "^4.10.0",
"@wordpress/scripts": "^30.3.0"
},
"dependencies": {
"@wordpress/data": "^10.10.0",
"@wordpress/data-controls": "^4.10.0"
"@woocommerce/settings": "^1.0.0"
}
}

View file

@ -15,3 +15,15 @@ $shadow-card: 0 3px 6px 0 rgba(0, 0, 0, 0.15);
$shadow-selection-box: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
$color-gradient-dark: #001435;
$gradient-header: linear-gradient(87.03deg, #003087 -0.49%, #001E51 29.22%, $color-gradient-dark 100%);
$max-width-onboarding: 1024px;
$max-width-onboarding-content: 662px;
$max-width-settings: 938px;
#ppcp-settings-container {
--max-width-settings: #{$max-width-settings};
--max-width-onboarding: #{$max-width-onboarding};
--max-width-onboarding-content: #{$max-width-onboarding-content};
--max-container-width: var(--max-width-settings);
}

View file

@ -82,6 +82,10 @@
&__checkbox-presentation {
@include fake-input-field(2px);
}
&__additional-content{
position: relative;
z-index: 1;
}
@media screen and (max-width: 480px) {
gap: 16px;

View file

@ -1,20 +1,23 @@
.ppcp-r {
&-container {
max-width: var(--max-container-width, none);
margin-left: auto;
margin-right: auto;
}
&-inner-container {
width: 652px;
max-width: 100%;
margin: 0 auto;
padding: 0 16px;
margin-left: auto;
margin-right: auto;
padding: 0 16px 72px;
box-sizing: border-box;
@media screen and (max-width: 480px) {
padding-bottom: 36px;
}
}
&-container {
max-width: 1024px;
margin: 0 auto;
&--settings {
max-width: 938px;
}
box-shadow: $shadow-card;
}
&-card {

View file

@ -0,0 +1,22 @@
.ppcp-r-tabs {
--wp-components-color-accent: #{$color-blueberry};
--wp-admin-border-width-focus: 3px;
max-width: var(--max-container-width);
margin: 0 auto;
transition: max-width 0.2s;
.components-tab-panel__tabs {
box-shadow: 0 -1px 0 0 $color-gray-400 inset;
margin-bottom: 48px;
gap: 12px;
.components-button {
padding: 16px 20px;
&.is-active {
background-color: #fff4;
}
}
}
}

View file

@ -0,0 +1,12 @@
@import './onboarding/step-welcome';
@import './onboarding/step-business';
@import './onboarding/step-products';
.ppcp-r-tabs.onboarding,
.ppcp-r-container--onboarding {
--max-container-width: var(--max-width-onboarding);
.ppcp-r-inner-container {
max-width: var(--max-width-onboarding-content);
}
}

View file

@ -1,12 +1,4 @@
.ppcp-r-page-business {
.ppcp-r-inner-container {
width: 622px;
padding-bottom: 84px;
@media screen and (max-width: 480px) {
padding-bottom: 42px;
}
}
.ppcp-r-payment-method-icons {
justify-content: flex-start;
}

View file

@ -19,14 +19,6 @@
}
.ppcp-r-page-products {
.ppcp-r-inner-container {
width: 622px;
padding-bottom: 48px;
@media screen and (max-width: 480px) {
padding-bottom: 24px;
}
}
.ppcp-r-payment-method-icons {
justify-content: flex-start;
}

View file

@ -1,11 +1,4 @@
.ppcp-r-page-welcome {
.ppcp-r-inner-container {
padding-bottom: 72px;
@media screen and (max-width: 480px) {
padding-bottom: 36px;
}
}
.ppcp-r-welcome-features {
margin: 0 0 32px 0;
}

View file

@ -11,14 +11,11 @@
@import './components/reusable-components/payment-method-icons';
@import './components/reusable-components/settings-wrapper';
@import './components/reusable-components/select-box';
@import './components/reusable-components/tab-navigation';
@import './components/reusable-components/navigation';
@import './components/reusable-components/tabs';
@import './components/reusable-components/fields';
@import './components/reusable-components/title-badge';
@import './components/reusable-components/payment-method-item';
@import './components/screens/onboarding/step-welcome';
@import './components/screens/onboarding/step-business';
@import './components/screens/onboarding/step-products';
@import './components/screens/onboarding';
@import './components/screens/dashboard/tab-dashboard';
@import './components/screens/dashboard/tab-payment-methods';
}

View file

@ -1,6 +1,3 @@
export const PAGE_ONBOARDING = 'onboarding';
export const PAGE_SETTINGS = 'settings';
const Container = ( { isCard = true, page, children } ) => {
let className = 'ppcp-r-container';

View file

@ -3,27 +3,35 @@ import { __ } from '@wordpress/i18n';
const Navigation = ( {
setStep,
setCompleted,
currentStep,
stepperOrder,
canProceeedCallback = () => true,
} ) => {
const setNextStep = ( nextStep ) => {
let newStep = currentStep + nextStep;
if ( newStep > stepperOrder.length - 1 ) {
newStep = currentStep;
const navigateBy = ( stepDirection ) => {
let newStep = currentStep + stepDirection;
if ( isNaN( newStep ) || newStep < 0 ) {
console.warn( 'Invalid next step:', newStep );
newStep = 0;
}
if ( newStep >= stepperOrder.length ) {
setCompleted( true );
} else {
setStep( newStep );
}
setStep( newStep );
};
return (
<div className="ppcp-r-navigation">
<Button variant="tertiary" onClick={ () => setNextStep( -1 ) }>
<Button variant="tertiary" onClick={ () => navigateBy( -1 ) }>
{ __( 'Back', 'woocommerce-paypal-payments' ) }
</Button>
<Button
variant="primary"
disabled={ ! canProceeedCallback() }
onClick={ () => setNextStep( 1 ) }
onClick={ () => navigateBy( 1 ) }
>
{ __( 'Next', 'woocommerce-paypal-payments' ) }
</Button>

View file

@ -1,56 +1,47 @@
import { useCallback, useEffect, useState } from '@wordpress/element';
import { TabPanel } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import TabDashboard from '../Screens/Dashboard/TabDashboard';
import TabPaymentMethods from '../Screens/Dashboard/TabPaymentMethods';
import TabSettings from '../Screens/Dashboard/TabSettings';
import TabStyling from '../Screens/Dashboard/TabStyling';
import { getQuery, updateQueryString } from '@woocommerce/navigation';
const TAB_DASHBOARD = 'TabDashboard';
const TAB_PAYMENT_METHODS = 'TabPaymentMethods';
const TAB_SETTINGS = 'TabSettings';
const TAB_STYLING = 'TabStyling';
const TabNavigation = ( { tabs } ) => {
const { panel } = getQuery();
const TabNavigation = () => {
const tabComponents = {
[ TAB_DASHBOARD ]: TabDashboard,
[ TAB_PAYMENT_METHODS ]: TabPaymentMethods,
[ TAB_SETTINGS ]: TabSettings,
[ TAB_STYLING ]: TabStyling,
const isValidTab = ( tabsList, checkTab ) => {
return tabsList.some( ( tab ) => tab.name === checkTab );
};
const getValidInitialPanel = () => {
if ( ! panel || ! isValidTab( tabs, panel ) ) {
return tabs[ 0 ].name;
}
return panel;
};
const [ activePanel, setActivePanel ] = useState( getValidInitialPanel );
const updateActivePanel = useCallback(
( tabName ) => {
if ( isValidTab( tabs, tabName ) ) {
setActivePanel( tabName );
} else {
console.warn( `Invalid tab name: ${ tabName }` );
}
},
[ tabs ]
);
useEffect( () => {
updateQueryString( { panel: activePanel }, '/', getQuery() );
}, [ activePanel ] );
return (
<TabPanel
className="my-tab-panel"
activeClass="active-tab"
tabs={ [
{
name: TAB_DASHBOARD,
title: __( 'Dashboard', 'woocommerce-paypal-payments' ),
className: 'ppcp-r-tab-dashboard',
},
{
name: TAB_PAYMENT_METHODS,
title: __(
'Payment Methods',
'woocommerce-paypal-payments'
),
className: 'ppcp-r-tab-payment-methods',
},
{
name: TAB_SETTINGS,
title: __( 'Settings', 'woocommerce-paypal-payments' ),
className: 'ppcp-r-tab-settings',
},
{
name: TAB_STYLING,
title: __( 'Styling', 'woocommerce-paypal-payments' ),
className: 'ppcp-r-tab-styling',
},
] }
className={ `ppcp-r-tabs ${ activePanel }` }
initialTabName={ activePanel }
onSelect={ updateActivePanel }
tabs={ tabs }
>
{ ( tab ) => {
const Component = tabComponents[ tab.name ];
return <Component />;
return tab.component || <>{ tab.title ?? tab.name }</>;
} }
</TabPanel>
);

View file

@ -1,13 +0,0 @@
import TabNavigation from '../../ReusableComponents/TabNavigation';
import Container from '../../ReusableComponents/Container';
import { PAGE_SETTINGS } from '../../ReusableComponents/Container';
const Dashboard = () => {
return (
<Container isCard={ false } page={ PAGE_SETTINGS }>
<TabNavigation />
</Container>
);
};
export default Dashboard;

View file

@ -1,44 +1,36 @@
import Container, {
PAGE_ONBOARDING,
} from '../../ReusableComponents/Container.js';
import StepWelcome from './StepWelcome.js';
import StepBusiness from './StepBusiness.js';
import StepProducts from './StepProducts.js';
import { useState } from '@wordpress/element';
import Container from '../../ReusableComponents/Container';
import { useOnboardingStep } from '../../../data';
import { getSteps } from './availableSteps';
const getCurrentStep = ( requestedStep, steps ) => {
const isValidStep = ( step ) =>
typeof step === 'number' &&
Number.isInteger( step ) &&
step >= 0 &&
step < steps.length;
const safeCurrentStep = isValidStep( requestedStep ) ? requestedStep : 0;
return steps[ safeCurrentStep ];
};
const Onboarding = () => {
const [ step, setStep ] = useState( 0 );
const { step, setStep, setCompleted, flags } = useOnboardingStep();
const steps = getSteps( flags );
const CurrentStepComponent = getCurrentStep( step, steps );
return (
<Container page={ PAGE_ONBOARDING }>
<Container page="onboarding">
<div className="ppcp-r-card">
<Stepper currentStep={ step } setStep={ setStep } />
<CurrentStepComponent
setStep={ setStep }
currentStep={ step }
setCompleted={ setCompleted }
stepperOrder={ steps }
/>
</div>
</Container>
);
};
const Stepper = ( { currentStep, setStep } ) => {
const stepperOrder = [ StepWelcome, StepBusiness, StepProducts ];
const renderSteps = () => {
return stepperOrder.map( ( Step, index ) => {
return (
<div
key={ index }
style={ index !== currentStep ? { display: 'none' } : {} }
>
<Step
setStep={ setStep }
currentStep={ currentStep }
stepperOrder={ stepperOrder }
/>
</div>
);
} );
};
return <>{ renderSteps() }</>;
};
export default Onboarding;

View file

@ -1,16 +1,35 @@
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader.js';
import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper.js';
import SelectBox from '../../ReusableComponents/SelectBox.js';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper';
import SelectBox from '../../ReusableComponents/SelectBox';
import { __ } from '@wordpress/i18n';
import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons';
import { useState } from '@wordpress/element';
import { useOnboardingStepBusiness } from '../../../data';
import Navigation from '../../ReusableComponents/Navigation';
import { BUSINESS_TYPES } from '../../../data/constants';
const StepBusiness = ( { setStep, currentStep, stepperOrder } ) => {
const [ businessCategory, setBusinessCategory ] = useState( null );
const BUSINESS_RADIO_GROUP_NAME = 'business';
const CASUAL_SELLER_CHECKBOX_VALUE = 'casual_seller';
const BUSINESS_CHECKBOX_VALUE = 'business';
const BUSINESS_RADIO_GROUP_NAME = 'business';
const StepBusiness = ( {
setStep,
currentStep,
stepperOrder,
setCompleted,
} ) => {
const { isCasualSeller, setIsCasualSeller } = useOnboardingStepBusiness();
const handleSellerTypeChange = ( value ) => {
setIsCasualSeller( BUSINESS_TYPES.CASUAL_SELLER === value );
};
const getCurrentValue = () => {
if ( isCasualSeller === null ) {
return '';
}
return isCasualSeller
? BUSINESS_TYPES.CASUAL_SELLER
: BUSINESS_TYPES.BUSINESS;
};
return (
<div className="ppcp-r-page-business">
@ -33,13 +52,10 @@ const StepBusiness = ( { setStep, currentStep, stepperOrder } ) => {
) }
icon="icon-business-casual-seller.svg"
name={ BUSINESS_RADIO_GROUP_NAME }
value={ CASUAL_SELLER_CHECKBOX_VALUE }
changeCallback={ setBusinessCategory }
currentValue={ businessCategory }
checked={
businessCategory ===
{ CASUAL_SELLER_CHECKBOX_VALUE }
}
value={ BUSINESS_TYPES.CASUAL_SELLER }
changeCallback={ handleSellerTypeChange }
currentValue={ getCurrentValue() }
checked={ isCasualSeller === true }
type="radio"
>
<PaymentMethodIcons
@ -64,12 +80,10 @@ const StepBusiness = ( { setStep, currentStep, stepperOrder } ) => {
) }
icon="icon-business-business.svg"
name={ BUSINESS_RADIO_GROUP_NAME }
value={ BUSINESS_CHECKBOX_VALUE }
currentValue={ businessCategory }
changeCallback={ setBusinessCategory }
checked={
businessCategory === { BUSINESS_CHECKBOX_VALUE }
}
value={ BUSINESS_TYPES.BUSINESS }
changeCallback={ handleSellerTypeChange }
currentValue={ getCurrentValue() }
checked={ isCasualSeller === false }
type="radio"
>
<PaymentMethodIcons
@ -91,7 +105,8 @@ const StepBusiness = ( { setStep, currentStep, stepperOrder } ) => {
setStep={ setStep }
currentStep={ currentStep }
stepperOrder={ stepperOrder }
canProceeedCallback={ () => businessCategory !== null }
setCompleted={ setCompleted }
canProceeedCallback={ () => isCasualSeller !== null }
/>
</div>
</div>

View file

@ -3,14 +3,18 @@ import Navigation from '../../ReusableComponents/Navigation';
import { __ } from '@wordpress/i18n';
import SelectBox from '../../ReusableComponents/SelectBox';
import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper';
import { useState } from '@wordpress/element';
import { useOnboardingStepProducts } from '../../../data';
import { PRODUCT_TYPES } from '../../../data/constants';
const StepProducts = ( { setStep, currentStep, stepperOrder } ) => {
const [ products, setProducts ] = useState( [] );
const PRODUCTS_CHECKBOX_GROUP_NAME = 'products';
const VIRTUAL_CHECKBOX_VALUE = 'virtual';
const PHYSICAL_CHECKBOX_VALUE = 'physical';
const SUBSCRIPTIONS_CHECKBOX_VALUE = 'subscriptions';
const PRODUCTS_CHECKBOX_GROUP_NAME = 'products';
const StepProducts = ( {
setStep,
currentStep,
stepperOrder,
setCompleted,
} ) => {
const { products, toggleProduct } = useOnboardingStepProducts();
return (
<div className="ppcp-r-page-products">
@ -30,8 +34,8 @@ const StepProducts = ( { setStep, currentStep, stepperOrder } ) => {
) }
icon="icon-product-virtual.svg"
name={ PRODUCTS_CHECKBOX_GROUP_NAME }
value={ VIRTUAL_CHECKBOX_VALUE }
changeCallback={ setProducts }
value={ PRODUCT_TYPES.VIRTUAL }
changeCallback={ toggleProduct }
currentValue={ products }
type="checkbox"
>
@ -73,8 +77,8 @@ const StepProducts = ( { setStep, currentStep, stepperOrder } ) => {
) }
icon="icon-product-physical.svg"
name={ PRODUCTS_CHECKBOX_GROUP_NAME }
value={ PHYSICAL_CHECKBOX_VALUE }
changeCallback={ setProducts }
value={ PRODUCT_TYPES.PHYSICAL }
changeCallback={ toggleProduct }
currentValue={ products }
type="checkbox"
>
@ -101,14 +105,17 @@ const StepProducts = ( { setStep, currentStep, stepperOrder } ) => {
) }
icon="icon-product-subscription.svg"
name={ PRODUCTS_CHECKBOX_GROUP_NAME }
value={ SUBSCRIPTIONS_CHECKBOX_VALUE }
changeCallback={ setProducts }
value={ PRODUCT_TYPES.SUBSCRIPTIONS }
changeCallback={ toggleProduct }
currentValue={ products }
type="checkbox"
>
<a href="#">
<a
target="__blank"
href="https://woocommerce.com/document/woocommerce-paypal-payments/#subscriptions-faq"
>
{ __(
'WooCommerce Subscriptions - TODO missing link',
'WooCommerce Subscriptions',
'woocommerce-paypal-payments'
) }
</a>
@ -118,6 +125,7 @@ const StepProducts = ( { setStep, currentStep, stepperOrder } ) => {
setStep={ setStep }
currentStep={ currentStep }
stepperOrder={ stepperOrder }
setCompleted={ setCompleted }
canProceeedCallback={ () => products.length > 0 }
/>
</div>

View file

@ -1,13 +1,14 @@
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader.js';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import { __, sprintf } from '@wordpress/i18n';
import { Button, TextControl } from '@wordpress/components';
import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons';
import SettingsToggleBlock from '../../ReusableComponents/SettingsToggleBlock';
import Separator from '../../ReusableComponents/Separator';
import { useOnboardingDetails } from '../../../data';
import { useOnboardingStepWelcome, useManualConnect } from '../../../data';
import DataStoreControl from '../../ReusableComponents/DataStoreControl';
const StepWelcome = ( { setStep, currentStep } ) => {
const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
return (
<div className="ppcp-r-page-welcome">
<OnboardingHeader
@ -37,7 +38,7 @@ const StepWelcome = ( { setStep, currentStep } ) => {
className="ppcp-r-page-welcome-or-separator"
text={ __( 'or', 'woocommerce-paypal-payments' ) }
/>
<WelcomeForm />
<WelcomeForm setCompleted={ setCompleted } />
</div>
</div>
);
@ -74,7 +75,7 @@ const WelcomeFeatures = () => {
);
};
const WelcomeForm = () => {
const WelcomeForm = ( { setCompleted } ) => {
const {
isSandboxMode,
setSandboxMode,
@ -84,15 +85,35 @@ const WelcomeForm = () => {
setClientId,
clientSecret,
setClientSecret,
} = useOnboardingDetails();
} = useOnboardingStepWelcome();
const { connectManual } = useManualConnect();
const handleConnect = async () => {
try {
const res = await connectManual(
clientId,
clientSecret,
isSandboxMode
);
if ( ! res.success ) {
throw new Error( 'Request failed.' );
}
setCompleted( true );
} catch ( exc ) {
console.error( exc );
alert( 'Connection failed.' );
}
};
const advancedUsersDescription = sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'For advanced users: Connect a custom PayPal REST app for full control over your integration. For more information on creating a PayPal REST application, <a href="%s">click here</a>.',
'For advanced users: Connect a custom PayPal REST app for full control over your integration. For more information on creating a PayPal REST application, <a target="_blank" href="%s">click here</a>.',
'woocommerce-paypal-payments'
),
'#'
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
);
return (
@ -116,7 +137,7 @@ const WelcomeForm = () => {
<Separator className="ppcp-r-page-welcome-mode-separator" />
<SettingsToggleBlock
label={ __(
'Manually Connect - TODO missing link',
'Manually Connect',
'woocommerce-paypal-payments'
) }
description={ advancedUsersDescription }
@ -125,24 +146,38 @@ const WelcomeForm = () => {
>
<DataStoreControl
control={ TextControl }
label={ __(
'Sandbox Client ID',
'woocommerce-paypal-payments'
) }
label={
isSandboxMode
? __(
'Sandbox Client ID',
'woocommerce-paypal-payments'
)
: __(
'Live Client ID',
'woocommerce-paypal-payments'
)
}
value={ clientId }
onChange={ setClientId }
/>
<DataStoreControl
control={ TextControl }
label={ __(
'Sandbox Secret Key',
'woocommerce-paypal-payments'
) }
label={
isSandboxMode
? __(
'Sandbox Secret Key',
'woocommerce-paypal-payments'
)
: __(
'Live Secret Key',
'woocommerce-paypal-payments'
)
}
value={ clientSecret }
onChange={ setClientSecret }
type="password"
/>
<Button variant="secondary">
<Button variant="secondary" onClick={ handleConnect }>
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
</Button>
</SettingsToggleBlock>

View file

@ -0,0 +1,13 @@
import StepWelcome from './StepWelcome';
import StepBusiness from './StepBusiness';
import StepProducts from './StepProducts';
export const getSteps = ( flags ) => {
const allSteps = [ StepWelcome, StepBusiness, StepProducts ];
if ( ! flags.canUseCasualSelling ) {
return allSteps.filter( ( step ) => step !== StepBusiness );
}
return allSteps;
};

View file

@ -1,10 +1,23 @@
import TabNavigation from '../ReusableComponents/TabNavigation';
import { getSettingsTabs } from './tabs';
import { useOnboardingStep } from '../../data';
import Onboarding from './Onboarding/Onboarding';
import { useState } from '@wordpress/element';
import Dashboard from './Dashboard/Dashboard';
const Settings = () => {
const [ onboarded, setOnboarded ] = useState( true );
return <>{ onboarded ? <Dashboard /> : <Onboarding /> }</>;
const onboardingProgress = useOnboardingStep();
if ( ! onboardingProgress.isReady ) {
// TODO: Use better loading state indicator.
return <div>Loading...</div>;
}
if ( ! onboardingProgress.completed ) {
return <Onboarding />;
}
const tabs = getSettingsTabs( onboardingProgress );
return <TabNavigation tabs={ tabs }></TabNavigation>;
};
export default Settings;

View file

@ -0,0 +1,35 @@
import { __ } from '@wordpress/i18n';
import TabDashboard from './Dashboard/TabDashboard';
import TabPaymentMethods from './Dashboard/TabPaymentMethods';
import TabSettings from './Dashboard/TabSettings';
import TabStyling from './Dashboard/TabStyling';
export const getSettingsTabs = () => {
const tabs = [];
tabs.push( {
name: 'dashboard',
title: __( 'Dashboard', 'woocommerce-paypal-payments' ),
component: <TabDashboard />,
} );
tabs.push( {
name: 'payment-methods',
title: __( 'Payment Methods', 'woocommerce-paypal-payments' ),
component: <TabPaymentMethods />,
} );
tabs.push( {
name: 'settings',
title: __( 'Settings', 'woocommerce-paypal-payments' ),
component: <TabSettings />,
} );
tabs.push( {
name: 'styling',
title: __( 'Styling', 'woocommerce-paypal-payments' ),
component: <TabStyling />,
} );
return tabs;
};

View file

@ -1,2 +1,13 @@
export const NAMESPACE = '/wc/v3/wc_paypal';
export const STORE_NAME = 'wc/paypal';
export const BUSINESS_TYPES = {
CASUAL_SELLER: 'casual_seller',
BUSINESS: 'business',
};
export const PRODUCT_TYPES = {
VIRTUAL: 'virtual',
PHYSICAL: 'physical',
SUBSCRIPTIONS: 'subscriptions',
};

View file

@ -1,12 +1,18 @@
export default {
RESET_ONBOARDING: 'RESET_ONBOARDING',
// Transient data.
SET_IS_SAVING_ONBOARDING_DETAILS: 'SET_IS_SAVING_ONBOARDING_DETAILS',
SET_ONBOARDING_IS_READY: 'SET_ONBOARDING_IS_READY',
SET_IS_SAVING_ONBOARDING: 'SET_IS_SAVING_ONBOARDING',
// Persistent data.
SET_ONBOARDING_COMPLETED: 'SET_ONBOARDING_COMPLETED',
SET_ONBOARDING_DETAILS: 'SET_ONBOARDING_DETAILS',
SET_ONBOARDING_STEP: 'SET_ONBOARDING_STEP',
SET_SANDBOX_MODE: 'SET_SANDBOX_MODE',
SET_MANUAL_CONNECTION_MODE: 'SET_MANUAL_CONNECTION_MODE',
SET_CLIENT_ID: 'SET_CLIENT_ID',
SET_CLIENT_SECRET: 'SET_CLIENT_SECRET',
SET_IS_CASUAL_SELLER: 'SET_IS_CASUAL_SELLER',
SET_PRODUCTS: 'SET_PRODUCTS',
};

View file

@ -3,6 +3,28 @@ import { apiFetch } from '@wordpress/data-controls';
import ACTION_TYPES from './action-types';
import { NAMESPACE, STORE_NAME } from '../constants';
/**
* Special. Resets all values in the onboarding store to initial defaults.
*
* @return {{type: string}} The action.
*/
export const resetOnboarding = () => {
return { type: ACTION_TYPES.RESET_ONBOARDING };
};
/**
* Non-persistent. Marks the onboarding details as "ready", i.e., fully initialized.
*
* @param {boolean} isReady
* @return {{type: string, isReady}} The action.
*/
export const setIsReady = ( isReady ) => {
return {
type: ACTION_TYPES.SET_ONBOARDING_IS_READY,
isReady,
};
};
/**
* Non-persistent. Changes the "saving" flag.
*
@ -11,7 +33,7 @@ import { NAMESPACE, STORE_NAME } from '../constants';
*/
export const setIsSaving = ( isSaving ) => {
return {
type: ACTION_TYPES.SET_IS_SAVING_ONBOARDING_DETAILS,
type: ACTION_TYPES.SET_IS_SAVING_ONBOARDING,
isSaving,
};
};
@ -19,7 +41,7 @@ export const setIsSaving = ( isSaving ) => {
/**
* Persistent. Set the full onboarding details, usually during app initialization.
*
* @param {Object} payload
* @param {{data: {}, flags?: {}}} payload
* @return {{type: string, payload}} The action.
*/
export const setOnboardingDetails = ( payload ) => {
@ -29,6 +51,19 @@ export const setOnboardingDetails = ( payload ) => {
};
};
/**
* Persistent.Set the "onboarding completed" flag which shows or hides the wizard.
*
* @param {boolean} completed
* @return {{type: string, payload}} The action.
*/
export const setCompleted = ( completed ) => {
return {
type: ACTION_TYPES.SET_ONBOARDING_COMPLETED,
completed,
};
};
/**
* Persistent. Sets the onboarding wizard to a new step.
*
@ -94,6 +129,32 @@ export const setClientSecret = ( clientSecret ) => {
};
};
/**
* Persistent. Sets the "isCasualSeller" value.
*
* @param {boolean} isCasualSeller
* @return {{type: string, isCasualSeller}} The action.
*/
export const setIsCasualSeller = ( isCasualSeller ) => {
return {
type: ACTION_TYPES.SET_IS_CASUAL_SELLER,
isCasualSeller,
};
};
/**
* Persistent. Sets the "products" array.
*
* @param {string[]} products
* @return {{type: string, products}} The action.
*/
export const setProducts = ( products ) => {
return {
type: ACTION_TYPES.SET_PRODUCTS,
products,
};
};
/**
* Saves the persistent details to the WP database.
*

View file

@ -1,14 +1,19 @@
import { useSelect, useDispatch } from '@wordpress/data';
import { STORE_NAME } from '../constants';
import apiFetch from '@wordpress/api-fetch';
import { NAMESPACE, PRODUCT_TYPES, STORE_NAME } from '../constants';
import { getFlags } from './selectors';
export const useOnboardingDetails = () => {
const useOnboardingDetails = () => {
const {
persist,
setOnboardingStep,
setCompleted,
setSandboxMode,
setManualConnectionMode,
persist,
setClientId,
setClientSecret,
setIsCasualSeller,
setProducts,
} = useDispatch( STORE_NAME );
// Transient accessors.
@ -16,7 +21,24 @@ export const useOnboardingDetails = () => {
return select( STORE_NAME ).getTransientData().isSaving;
}, [] );
const isReady = useSelect( ( select ) => {
return select( STORE_NAME ).getTransientData().isReady;
} );
// Read-only flags.
const flags = useSelect( ( select ) => {
return select( STORE_NAME ).getFlags();
} );
// Persistent accessors.
const step = useSelect( ( select ) => {
return select( STORE_NAME ).getPersistentData().step || 0;
} );
const completed = useSelect( ( select ) => {
return select( STORE_NAME ).getPersistentData().completed;
} );
const clientId = useSelect( ( select ) => {
return select( STORE_NAME ).getPersistentData().clientId;
}, [] );
@ -25,10 +47,6 @@ export const useOnboardingDetails = () => {
return select( STORE_NAME ).getPersistentData().clientSecret;
}, [] );
const onboardingStep = useSelect( ( select ) => {
return select( STORE_NAME ).getPersistentData().step || 0;
}, [] );
const isSandboxMode = useSelect( ( select ) => {
return select( STORE_NAME ).getPersistentData().useSandbox;
}, [] );
@ -37,26 +55,112 @@ export const useOnboardingDetails = () => {
return select( STORE_NAME ).getPersistentData().useManualConnection;
}, [] );
const isCasualSeller = useSelect( ( select ) => {
return select( STORE_NAME ).getPersistentData().isCasualSeller;
}, [] );
const products = useSelect( ( select ) => {
return select( STORE_NAME ).getPersistentData().products || [];
}, [] );
const toggleProduct = ( list ) => {
const validProducts = list.filter( ( item ) =>
Object.values( PRODUCT_TYPES ).includes( item )
);
return setDetailAndPersist( setProducts, validProducts );
};
const setDetailAndPersist = async ( setter, value ) => {
setter( value );
await persist();
};
return {
onboardingStep,
isSaving,
isReady,
step,
setStep: ( value ) => setDetailAndPersist( setOnboardingStep, value ),
completed,
setCompleted: ( state ) => setDetailAndPersist( setCompleted, state ),
isSandboxMode,
setSandboxMode: ( state ) =>
setDetailAndPersist( setSandboxMode, state ),
isManualConnectionMode,
setManualConnectionMode: ( state ) =>
setDetailAndPersist( setManualConnectionMode, state ),
clientId,
setClientId: ( value ) => setDetailAndPersist( setClientId, value ),
clientSecret,
setClientSecret: ( value ) =>
setDetailAndPersist( setClientSecret, value ),
setOnboardingStep: ( step ) =>
setDetailAndPersist( setOnboardingStep, step ),
setSandboxMode: ( state ) =>
setDetailAndPersist( setSandboxMode, state ),
setManualConnectionMode: ( state ) =>
setDetailAndPersist( setManualConnectionMode, state ),
isCasualSeller,
setIsCasualSeller: ( value ) =>
setDetailAndPersist( setIsCasualSeller, value ),
products,
toggleProduct,
flags,
};
};
export const useOnboardingStepWelcome = () => {
const {
isSaving,
isSandboxMode,
setSandboxMode,
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
} = useOnboardingDetails();
return {
isSaving,
isSandboxMode,
setSandboxMode,
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
};
};
export const useOnboardingStepBusiness = () => {
const { isCasualSeller, setIsCasualSeller } = useOnboardingDetails();
return { isCasualSeller, setIsCasualSeller };
};
export const useOnboardingStepProducts = () => {
const { products, toggleProduct } = useOnboardingDetails();
return { products, toggleProduct };
};
export const useOnboardingStep = () => {
const { isReady, step, setStep, completed, setCompleted, flags } =
useOnboardingDetails();
return { isReady, step, setStep, completed, setCompleted, flags };
};
export const useManualConnect = () => {
const connectManual = async ( clientId, clientSecret, isSandboxMode ) => {
return await apiFetch( {
path: `${ NAMESPACE }/connect_manual`,
method: 'POST',
data: {
clientId,
clientSecret,
useSandbox: isSandboxMode,
},
} );
};
return {
connectManual,
};
};

View file

@ -1,13 +1,26 @@
import ACTION_TYPES from './action-types';
const defaultState = {
isReady: false,
isSaving: false,
// Data persisted to the server.
data: {
completed: false,
step: 0,
useSandbox: false,
useManualConnection: false,
clientId: '',
clientSecret: '',
isCasualSeller: null, // null value will uncheck both options in the UI.
products: [],
},
// Read only values, provided by the server.
flags: {
canUseCasualSelling: false,
canUseVaulting: false,
canUseCardPayments: false,
},
};
@ -35,20 +48,36 @@ export const onboardingReducer = (
};
switch ( type ) {
// Reset store to initial state.
case ACTION_TYPES.RESET_ONBOARDING:
return setPersistent( defaultState.data );
// Transient data.
case ACTION_TYPES.SET_IS_SAVING_ONBOARDING_DETAILS:
case ACTION_TYPES.SET_ONBOARDING_IS_READY:
return setTransient( { isReady: action.isReady } );
case ACTION_TYPES.SET_IS_SAVING_ONBOARDING:
return setTransient( { isSaving: action.isSaving } );
// Persistent data.
case ACTION_TYPES.SET_ONBOARDING_DETAILS:
const newState = setPersistent( action.payload.data );
if ( action.payload.flags ) {
newState.flags = { ...newState.flags, ...action.payload.flags };
}
return newState;
case ACTION_TYPES.SET_ONBOARDING_COMPLETED:
return setPersistent( { completed: action.completed } );
case ACTION_TYPES.SET_CLIENT_ID:
return setPersistent( { clientId: action.clientId } );
case ACTION_TYPES.SET_CLIENT_SECRET:
return setPersistent( { clientSecret: action.clientSecret } );
case ACTION_TYPES.SET_ONBOARDING_DETAILS:
return setPersistent( action.payload );
case ACTION_TYPES.SET_ONBOARDING_STEP:
return setPersistent( { step: action.step } );
@ -60,6 +89,12 @@ export const onboardingReducer = (
useManualConnection: action.useManualConnection,
} );
case ACTION_TYPES.SET_IS_CASUAL_SELLER:
return setPersistent( { isCasualSeller: action.isCasualSeller } );
case ACTION_TYPES.SET_PRODUCTS:
return setPersistent( { products: action.products } );
default:
return state;
}

View file

@ -2,7 +2,7 @@ import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls';
import { NAMESPACE } from '../constants';
import { setOnboardingDetails } from './actions';
import { setIsReady, setOnboardingDetails } from './actions';
/**
* Retrieve settings from the site's REST API.
@ -13,6 +13,7 @@ export function* getPersistentData() {
try {
const result = yield apiFetch( { path } );
yield setOnboardingDetails( result );
yield setIsReady( true );
} catch ( e ) {
yield dispatch( 'core/notices' ).createErrorNotice(
__(

View file

@ -13,6 +13,10 @@ export const getPersistentData = ( state ) => {
};
export const getTransientData = ( state ) => {
const { data, ...transientState } = getOnboardingState( state );
const { data, flags, ...transientState } = getOnboardingState( state );
return transientState || EMPTY_OBJ;
};
export const getFlags = ( state ) => {
return getOnboardingState( state ).flags || EMPTY_OBJ;
};

View file

@ -27,4 +27,32 @@ export const initStore = () => {
} );
register( store );
/* eslint-disable no-console */
// Provide a debug tool to inspect the Redux store via the JS console.
if ( window.ppcpSettings?.debug && console?.groupCollapsed ) {
window.ppcpSettings.dumpStore = () => {
const storeSelector = `wp.data.select('${ STORE_NAME }')`;
console.group( `[STORE] ${ storeSelector }` );
const storeState = wp.data.select( STORE_NAME );
Object.keys( selectors ).forEach( ( selector ) => {
console.groupCollapsed( `[SELECTOR] .${ selector }()` );
console.table( storeState[ selector ]() );
console.groupEnd();
} );
console.groupEnd();
};
window.ppcpSettings.resetStore = () => {
wp.data.dispatch( STORE_NAME ).resetOnboarding();
wp.data.dispatch( STORE_NAME ).persist();
};
window.ppcpSettings.startOnboarding = () => {
wp.data.dispatch( STORE_NAME ).setCompleted( false );
wp.data.dispatch( STORE_NAME ).setOnboardingStep( 0 );
wp.data.dispatch( STORE_NAME ).persist();
};
}
/* eslint-enable no-console */
};

View file

@ -9,12 +9,13 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings;
use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile;
return array(
'settings.url' => static function ( ContainerInterface $container ) : string {
'settings.url' => static function ( ContainerInterface $container ) : string {
/**
* The path cannot be false.
*
@ -25,10 +26,46 @@ return array(
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'settings.data.onboarding' => static function ( ContainerInterface $container ) : OnboardingProfile {
return new OnboardingProfile();
'settings.data.onboarding' => static function ( ContainerInterface $container ) : OnboardingProfile {
$can_use_casual_selling = $container->get( 'settings.casual-selling.eligible' );
$can_use_vaulting = $container->has( 'save-payment-methods.eligible' ) && $container->get( 'save-payment-methods.eligible' );
$can_use_card_payments = $container->has( 'card-fields.eligible' ) && $container->get( 'card-fields.eligible' );
// Card payments are disabled for this plugin when WooPayments is active.
// TODO: Move this condition to the card-fields.eligible service?
if ( class_exists( '\WC_Payments' ) ) {
$can_use_card_payments = false;
}
return new OnboardingProfile(
$can_use_casual_selling,
$can_use_vaulting,
$can_use_card_payments
);
},
'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint {
'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint {
return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) );
},
'settings.rest.connect_manual' => static function ( ContainerInterface $container ) : ConnectManualRestEndpoint {
return new ConnectManualRestEndpoint();
},
'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array {
// TODO: This is a dummy list, while we wait for the official eligibility list.
return array(
'US',
'CA',
'DE',
'ES',
'AT',
'CH',
'NL',
);
},
'settings.casual-selling.eligible' => static function ( ContainerInterface $container ) : bool {
$country = $container->get( 'api.shop.country' );
$eligible_countries = $container->get( 'settings.casual-selling.supported-countries' );
return in_array( $country, $eligible_countries, true );
},
);

View file

@ -57,8 +57,9 @@ abstract class AbstractDataModel {
* Loads the model data from WordPress options.
*/
public function load() : void {
$saved_data = get_option( static::OPTION_KEY, array() );
$this->data = array_merge( $this->data, $saved_data );
$saved_data = get_option( static::OPTION_KEY, array() );
$filtered_data = array_intersect_key( $saved_data, $this->data );
$this->data = array_merge( $this->data, $filtered_data );
}
/**
@ -88,12 +89,38 @@ abstract class AbstractDataModel {
continue;
}
$setter = "set_$key";
if ( method_exists( $this, $setter ) ) {
$setter = $this->get_setter_name( $key );
if ( $setter && method_exists( $this, $setter ) ) {
$this->$setter( $value );
} else {
$this->data[ $key ] = $value;
}
}
}
/**
* Generates a setter method name for a given key, stripping the prefix from
* boolean fields (is_, use_, has_).
*
* @param int|string $field_key The key for which to generate a setter name.
*
* @return string The generated setter method name.
*/
private function get_setter_name( $field_key ) : string {
if ( ! is_string( $field_key ) ) {
return '';
}
$prefixes_to_strip = array( 'is_', 'use_', 'has_' );
$stripped_key = $field_key;
foreach ( $prefixes_to_strip as $prefix ) {
if ( str_starts_with( $field_key, $prefix ) ) {
$stripped_key = substr( $field_key, strlen( $prefix ) );
break;
}
}
return $stripped_key ? "set_$stripped_key" : '';
}
}

View file

@ -9,12 +9,16 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Data;
use RuntimeException;
/**
* Class OnboardingProfile
*
* This class serves as a container for managing the onboarding profile details
* within the WooCommerce PayPal Commerce plugin. It provides methods to retrieve
* and save the onboarding profile data using WordPress options.
* within the WooCommerce PayPal Commerce plugin.
*
* This profile impacts the onboarding wizard and help to apply default
* settings. The details here should not be used outside the onboarding process.
*/
class OnboardingProfile extends AbstractDataModel {
@ -25,6 +29,34 @@ class OnboardingProfile extends AbstractDataModel {
*/
protected const OPTION_KEY = 'woocommerce-ppcp-data-onboarding';
/**
* List of customization flags, provided by the server (read-only).
*
* @var array
*/
protected array $flags = array();
/**
* Constructor.
*
* @param bool $can_use_casual_selling Whether casual selling is enabled in the store's country.
* @param bool $can_use_vaulting Whether vaulting is enabled in the store's country.
* @param bool $can_use_card_payments Whether credit card payments are possible.
*
* @throws RuntimeException If the OPTION_KEY is not defined in the child class.
*/
public function __construct(
bool $can_use_casual_selling = false,
bool $can_use_vaulting = false,
bool $can_use_card_payments = false
) {
parent::__construct();
$this->flags['can_use_casual_selling'] = $can_use_casual_selling;
$this->flags['can_use_vaulting'] = $can_use_vaulting;
$this->flags['can_use_card_payments'] = $can_use_card_payments;
}
/**
* Get default values for the model.
*
@ -32,16 +64,37 @@ class OnboardingProfile extends AbstractDataModel {
*/
protected function get_defaults() : array {
return array(
'completed' => false,
'step' => 0,
'use_sandbox' => false,
'use_manual_connection' => false,
'client_id' => '',
'client_secret' => '',
'is_casual_seller' => null,
'products' => array(),
);
}
// -----
/**
* Gets the 'completed' flag.
*
* @return bool
*/
public function get_completed() : bool {
return (bool) $this->data['completed'];
}
/**
* Sets the 'completed' flag.
*
* @param bool $step Whether the onboarding process has been completed.
*/
public function set_completed( bool $step ) : void {
$this->data['completed'] = $step;
}
/**
* Gets the 'step' setting.
*
@ -54,7 +107,7 @@ class OnboardingProfile extends AbstractDataModel {
/**
* Sets the 'step' setting.
*
* @param int $step Whether to use sandbox mode.
* @param int $step The current onboarding step.
*/
public function set_step( int $step ) : void {
$this->data['step'] = $step;
@ -65,7 +118,7 @@ class OnboardingProfile extends AbstractDataModel {
*
* @return bool
*/
public function get_use_sandbox() : bool {
public function get_sandbox() : bool {
return (bool) $this->data['use_sandbox'];
}
@ -74,7 +127,7 @@ class OnboardingProfile extends AbstractDataModel {
*
* @param bool $use_sandbox Whether to use sandbox mode.
*/
public function set_use_sandbox( bool $use_sandbox ) : void {
public function set_sandbox( bool $use_sandbox ) : void {
$this->data['use_sandbox'] = $use_sandbox;
}
@ -83,7 +136,7 @@ class OnboardingProfile extends AbstractDataModel {
*
* @return bool
*/
public function get_use_manual_connection() : bool {
public function get_manual_connection() : bool {
return (bool) $this->data['use_manual_connection'];
}
@ -92,7 +145,7 @@ class OnboardingProfile extends AbstractDataModel {
*
* @param bool $use_manual_connection Whether to use manual connection.
*/
public function set_use_manual_connection( bool $use_manual_connection ) : void {
public function set_manual_connection( bool $use_manual_connection ) : void {
$this->data['use_manual_connection'] = $use_manual_connection;
}
@ -131,4 +184,49 @@ class OnboardingProfile extends AbstractDataModel {
public function set_client_secret( string $client_secret ) : void {
$this->data['client_secret'] = sanitize_text_field( $client_secret );
}
/**
* Gets the casual seller flag.
*
* @return bool|null
*/
public function get_casual_seller() : ?bool {
return $this->data['is_casual_seller'];
}
/**
* Sets the casual-seller flag.
*
* @param bool|null $casual_seller Whether the merchant uses a personal account for selling.
*/
public function set_casual_seller( ?bool $casual_seller ) : void {
$this->data['is_casual_seller'] = $casual_seller;
}
/**
* Gets the active product types for this store.
*
* @return string[]
*/
public function get_products() : array {
return $this->data['products'];
}
/**
* Sets the list of active product types.
*
* @param string[] $products Any of ['virtual'|'physical'|'subscriptions'].
*/
public function set_products( array $products ) : void {
$this->data['products'] = $products;
}
/**
* Returns the list of read-only customization flags
*
* @return array
*/
public function get_flags() : array {
return $this->flags;
}
}

View file

@ -0,0 +1,92 @@
<?php
/**
* REST controller for connection via manual credentials input.
*
* @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;
/**
* REST controller for connection via manual credentials input.
*/
class ConnectManualRestEndpoint extends RestEndpoint {
/**
* The base path for this REST controller.
*
* @var string
*/
protected $rest_base = 'connect_manual';
/**
* Field mapping for request.
*
* @var array
*/
private array $field_map = array(
'client_id' => array(
'js_name' => 'clientId',
'sanitize' => 'sanitize_text_field',
),
'client_secret' => array(
'js_name' => 'clientSecret',
'sanitize' => 'sanitize_text_field',
),
'use_sandbox' => array(
'js_name' => 'useSandbox',
'sanitize' => 'to_boolean',
),
);
/**
* 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, 'connect_manual' ),
'permission_callback' => array( $this, 'check_permission' ),
),
)
);
}
/**
* Retrieves merchantId and email.
*
* @param WP_REST_Request $request Full data about the request.
*/
public function connect_manual( WP_REST_Request $request ) : WP_REST_Response {
$data = $this->sanitize_for_wordpress(
$request->get_params(),
$this->field_map
);
if ( ! isset( $data['client_id'] ) || empty( $data['client_id'] )
|| ! isset( $data['client_secret'] ) || empty( $data['client_secret'] ) ) {
return rest_ensure_response(
array(
'success' => false,
)
);
}
$result = array(
'merchantId' => 'bt_us@woocommerce.com',
'email' => 'AT45V2DGMKLRY',
'success' => true,
);
return rest_ensure_response( $result );
}
}

View file

@ -41,6 +41,10 @@ class OnboardingRestEndpoint extends RestEndpoint {
* @var array
*/
private array $field_map = array(
'completed' => array(
'js_name' => 'completed',
'sanitize' => 'to_boolean',
),
'step' => array(
'js_name' => 'step',
'sanitize' => 'to_number',
@ -61,6 +65,30 @@ class OnboardingRestEndpoint extends RestEndpoint {
'js_name' => 'clientSecret',
'sanitize' => 'sanitize_text_field',
),
'is_casual_seller' => array(
'js_name' => 'isCasualSeller',
'sanitize' => 'to_boolean',
),
'products' => array(
'js_name' => 'products',
),
);
/**
* Map the internal flags to JS names.
*
* @var array
*/
private array $flag_map = array(
'can_use_casual_selling' => array(
'js_name' => 'canUseCasualSelling',
),
'can_use_vaulting' => array(
'js_name' => 'canUseVaulting',
),
'can_use_card_payments' => array(
'js_name' => 'canUseCardPayments',
),
);
/**
@ -70,6 +98,8 @@ class OnboardingRestEndpoint extends RestEndpoint {
*/
public function __construct( OnboardingProfile $profile ) {
$this->profile = $profile;
$this->field_map['products']['sanitize'] = fn( $list ) => array_map( 'sanitize_text_field', $list );
}
/**
@ -112,7 +142,17 @@ class OnboardingRestEndpoint extends RestEndpoint {
$this->field_map
);
return rest_ensure_response( $js_data );
$js_flags = $this->sanitize_for_javascript(
$this->profile->get_flags(),
$this->flag_map
);
return rest_ensure_response(
array(
'data' => $js_data,
'flags' => $js_flags,
)
);
}
/**

View file

@ -53,7 +53,11 @@ class RestEndpoint extends WC_REST_Controller {
$source_key = $details['js_name'] ?? '';
$sanitation_cb = $details['sanitize'] ?? null;
if ( ! $source_key || ! isset( $params[ $source_key ] ) ) {
if (
! $source_key
|| ! isset( $params[ $source_key ] )
|| 'read_only' === $sanitation_cb
) {
continue;
}
@ -61,7 +65,7 @@ class RestEndpoint extends WC_REST_Controller {
if ( null === $sanitation_cb ) {
$sanitized[ $key ] = $value;
} elseif ( method_exists( $this, $sanitation_cb ) ) {
} elseif ( is_string( $sanitation_cb ) && method_exists( $this, $sanitation_cb ) ) {
$sanitized[ $key ] = $this->{$sanitation_cb}( $value );
} elseif ( is_callable( $sanitation_cb ) ) {
$sanitized[ $key ] = $sanitation_cb( $value );
@ -121,5 +125,4 @@ class RestEndpoint extends WC_REST_Controller {
protected function to_number( $value ) {
return $value !== null ? ( is_numeric( $value ) ? $value + 0 : null ) : null;
}
}

View file

@ -9,6 +9,8 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings;
use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
@ -86,6 +88,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
'assets' => array(
'imagesUrl' => $module_url . '/images/',
),
'debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
)
);
}
@ -93,11 +96,12 @@ class SettingsModule implements ServiceModule, ExecutableModule {
add_action(
'woocommerce_paypal_payments_gateway_admin_options_wrapper',
static function () : void {
function () : void {
global $hide_save_button;
$hide_save_button = true;
echo '<div id="ppcp-settings-container"></div>';
$this->render_header();
$this->render_content();
}
);
@ -105,10 +109,35 @@ class SettingsModule implements ServiceModule, ExecutableModule {
'rest_api_init',
static function () use ( $container ) : void {
$onboarding_endpoint = $container->get( 'settings.rest.onboarding' );
assert( $onboarding_endpoint instanceof OnboardingRestEndpoint );
$onboarding_endpoint->register_routes();
$connect_manual_endpoint = $container->get( 'settings.rest.connect_manual' );
assert( $connect_manual_endpoint instanceof ConnectManualRestEndpoint );
$connect_manual_endpoint->register_routes();
}
);
return true;
}
/**
* Outputs the settings page header (title and back-link).
*
* @return void
*/
protected function render_header() : void {
echo '<h2>' . esc_html__( 'PayPal', 'woocommerce-paypal-payments' );
wc_back_link( __( 'Return to payments', 'woocommerce-paypal-payments' ), admin_url( 'admin.php?page=wc-settings&tab=checkout' ) );
echo '</h2>';
}
/**
* Renders the container for the React app.
*
* @return void
*/
protected function render_content() : void {
echo '<div id="ppcp-settings-container"></div>';
}
}

File diff suppressed because it is too large Load diff

View file

@ -94,20 +94,22 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
$this->register_wc_tasks( $c );
$this->register_void_button( $c );
add_action(
'woocommerce_sections_checkout',
function() use ( $c ) {
$header_renderer = $c->get( 'wcgateway.settings.header-renderer' );
assert( $header_renderer instanceof HeaderRenderer );
if ( ! $c->get( 'wcgateway.settings.admin-settings-enabled' ) ) {
add_action(
'woocommerce_sections_checkout',
function () use ( $c ) {
$header_renderer = $c->get( 'wcgateway.settings.header-renderer' );
assert( $header_renderer instanceof HeaderRenderer );
$section_renderer = $c->get( 'wcgateway.settings.sections-renderer' );
assert( $section_renderer instanceof SectionsRenderer );
$section_renderer = $c->get( 'wcgateway.settings.sections-renderer' );
assert( $section_renderer instanceof SectionsRenderer );
// phpcs:ignore WordPress.Security.EscapeOutput
echo $header_renderer->render() . $section_renderer->render();
},
20
);
// phpcs:ignore WordPress.Security.EscapeOutput
echo $header_renderer->render() . $section_renderer->render();
},
20
);
}
add_action(
'woocommerce_paypal_payments_order_captured',

0
node_modules/.gitkeep generated vendored Normal file
View file

View file

@ -28,6 +28,10 @@
<directory name="modules" />
<file name="bootstrap.php" />
<file name="woocommerce-paypal-payments.php" />
<ignoreFiles>
<directory name="node_modules"/>
<directory name="modules/*/node_modules"/>
</ignoreFiles>
</projectFiles>
<stubs>