Merge trunk

This commit is contained in:
Emili Castells Guasch 2025-03-27 09:53:35 +01:00
commit e4ee46e6ee
129 changed files with 2459 additions and 887 deletions

76
.github/workflows-config/typos.toml vendored Normal file
View file

@ -0,0 +1,76 @@
[default]
check-comments = true
check-docs = true
check-filenames = true
binary = false
ignore-non-words = false
type = ["de", "en"]
locale = "en"
# Ignore API keys and two character IDs like 'FO' or "FO"
extend-ignore-re = [
"\\b[0-9A-Za-z+/_-]{50,}(=|==)?\\b",
"'[A-Za-z0-9]{2}'",
"\"[A-Za-z0-9]{2}\"",
]
# Known good words
[default.extend-words]
paypal = "paypal"
PayPal = "PayPal"
woocommerce = "woocommerce"
Automattic = "Automattic"
automattic = "automattic"
Sie = "Sie"
sie = "sie"
oder = "oder"
als = "als"
# Define known typos to catch
[default.extend-corrections]
Fatslane = "Fastlane"
Fastalne = "Fastlane"
Faslane = "Fastlane"
Fastlain = "Fastlane"
Fasltane = "Fastlane"
Fastlan = "Fastlane"
Fastlanne = "Fastlane"
Fatstlane = "Fastlane"
Fstlane = "Fastlane"
# Explicit identifier corrections
[default.extend-identifiers]
"Fatslane" = "Fastlane"
"Fastalne" = "Fastlane"
"Faslane" = "Fastlane"
"Fastlain" = "Fastlane"
"Fasltane" = "Fastlane"
"Fastlan" = "Fastlane"
"Fastlanne" = "Fastlane"
"Fatstlane" = "Fastlane"
"Fstlane" = "Fastlane"
# Type-specific configurations
[type.php]
check-comments = true
check-docs = true
[type.js]
extend-glob = []
binary = false
check-filename = true
check-file = true
unicode = true
ignore-hex = true
identifier-leading-digits = false
[files]
extend-exclude = [
"vendor/*",
"node_modules/*",
"*.min.js",
"*.min.css",
"modules/ppcp-order-tracking/*",
"/tests/*"
]

20
.github/workflows/spell-check.yml vendored Normal file
View file

@ -0,0 +1,20 @@
name: Spell Check
on:
# Run on all pull requests
pull_request:
jobs:
typos:
runs-on: ubuntu-latest
name: Check for typos
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check spelling
id: spelling
uses: crate-ci/typos@v1.30.2
with:
# Path to config file
config: .github/workflows-config/typos.toml

1
.gitignore vendored
View file

@ -3,6 +3,7 @@
/.idea/
node_modules
.phpunit.result.cache
phpunit.xml.dist.bak
yarn-error.log
modules/*/vendor/*
modules/*/assets/*

View file

@ -2094,6 +2094,16 @@ function wcs_order_contains_product($order, $product)
*/
function wc_get_page_screen_id( $for ) {}
/**
* Checks if manual renewals are enabled.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v4.0.0
* @return bool Whether manual renewal is enabled.
*/
function wcs_is_manual_renewal_enabled()
{
}
/**
* Subscription Product Variation Class
*

View file

@ -1,5 +1,28 @@
*** Changelog ***
= 3.0.1 - 2025-03-26 =
* Enhancement - Include Fastlane meta on homepage #3151
* Enhancement - Include Branded-only plugin configuration for certain installation paths
* Enhancement - Include UI status in system report #3248
* Enhancement - Minor enhancements in new UI scrolling & highlighting behavior #3240
* Fix - "Warning: Class 'WooCommerce\PayPalCommerce\Vendor\Stringable' not found" after 3.0.0 update #3235
* Fix - ACDC does not work on the Classic Checkout when using the new UI #3219
* Fix - "Send only" country banner not displayed in the new UI #3236
* Fix - Typo in welcome screen #3258
* Fix - onboarding.js file from old UI enqueued in new UI #3263
* Fix - Onboarding in new UI with personal account does not hide all ineligible features #3254
* Fix - ACDC not defaulting on for eligible merchants after onboarding with Expanded Checkout selection #3250
* Fix - “Failed to fetch onboarding URL” error when onboarding with Subscriptions selected from non-Vault region #3242
* Fix - Fastlane SDK token requested when Fastlane is disabled #3009
* Fix - Subscription renewal payment via ACDC may fail in some cases due to 3D Secure #3098
* Fix - Error: _load_textdomain_just_in_time Called Incorrectly when running docker compose #3172
* Fix - Shipping callback not loading for guest users in some scenarios #3169
* Fix - Phone number not saved in WC order when using Pay Now experience #3160
* Fix - Phone number not pre-populated on Checkout block in continuation mode #3160
* Fix - "Unfortunately, your credit card details are not valid" shown with actually valid card during checkout with invalid postcode. #3067
* Fix - Incorrect Subscription Cancellation Handling with PayPal Subscriptions #3046
* Tweak - Added PayPal as contributor #3259
= 3.0.0 - 2025-03-17 =
* Enhancement - Redesigned settings UI for new users #2908
* Enhancement - Enable Fastlane by default on new store setups when eligible #3199
@ -159,7 +182,7 @@
* Fix - Shipping methods during callback not updated correctly #2421
* Fix - Preserve subscription renewal processing when switching Subscriptions Mode or disabling gateway #2394
* Fix - Remove shipping callback for Venmo express button #2374
* Fix - Google Pay: Fix issuse with data.paymentSource being undefined #2390
* Fix - Google Pay: Fix issue with data.paymentSource being undefined #2390
* Fix - Loading of non-Order as a WC_Order causes warnings and potential data corruption #2343
* Fix - Apple Pay and Google Pay buttons don't appear in PayPal Button stack on multi-step Checkout #2372
* Fix - Apple Pay: Fix when shipping is disabled #2391

View file

@ -18,6 +18,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts;
@ -31,6 +32,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundPayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerPayableBreakdownFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Settings\Service\BrandedExperience\ActivationDetector;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
@ -81,6 +83,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer;
use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Settings\Enum\InstallationPathEnum;
return array(
'api.host' => static function( ContainerInterface $container ) : string {
@ -963,4 +966,14 @@ return array(
return CONNECT_WOO_URL;
},
'api.helper.partner-attribution' => static function ( ContainerInterface $container ) : PartnerAttribution {
return new PartnerAttribution(
'ppcp_bn_code',
array(
InstallationPathEnum::CORE_PROFILER => 'WooPPCP_Ecom_PS_CoreProfiler',
InstallationPathEnum::PAYMENT_SETTINGS => 'WooPPCP_Ecom_PS_CoreProfiler',
),
PPCP_PAYPAL_BN_CODE
);
},
);

View file

@ -14,6 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
@ -133,6 +134,38 @@ class ApiModule implements ServiceModule, ExtendingModule, ExecutableModule {
}
);
/**
* Filters the request arguments to add the `'PayPal-Partner-Attribution-Id'` header.
*
* This ensures that all API requests include the appropriate BN code retrieved
* from the `PartnerAttribution` helper. Using this approach avoids the need
* for extensive refactoring of existing classes that use the `RequestTrait`.
*
* The filter is applied in {@see RequestTrait::request()} before making an API request.
*
* @see PartnerAttribution::get_bn_code() Retrieves the BN code dynamically.
* @see RequestTrait::request() Where the filter `ppcp_request_args` is applied.
*/
add_filter(
'ppcp_request_args',
static function ( array $args ) use ( $c ) {
if ( isset( $args['headers']['PayPal-Partner-Attribution-Id'] ) ) {
return $args;
}
$partner_attribution = $c->get( 'api.helper.partner-attribution' );
assert( $partner_attribution instanceof PartnerAttribution );
if ( ! isset( $args['headers'] ) || ! is_array( $args['headers'] ) ) {
$args['headers'] = array();
}
$args['headers']['PayPal-Partner-Attribution-Id'] = $partner_attribution->get_bn_code();
return $args;
}
);
return true;
}
}

View file

@ -106,7 +106,7 @@ class PartnersEndpoint {
* Returns the current seller status.
*
* @return SellerStatus
* @throws RuntimeException When request could not be fullfilled.
* @throws RuntimeException When request could not be fulfilled.
*/
public function seller_status() : SellerStatus {
$url = trailingslashit( $this->host ) . 'v1/customer/partners/' . $this->partner_id . '/merchant-integrations/' . $this->merchant_id;

View file

@ -41,9 +41,6 @@ trait RequestTrait {
* added here.
*/
$args = apply_filters( 'ppcp_request_args', $args, $url );
if ( ! isset( $args['headers']['PayPal-Partner-Attribution-Id'] ) ) {
$args['headers']['PayPal-Partner-Attribution-Id'] = PPCP_PAYPAL_BN_CODE;
}
$response = wp_remote_get( $url, $args );
if ( $this->is_request_logging_enabled ) {

View file

@ -14,7 +14,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
*/
class PhoneWithType {
const VALLID_TYPES = array(
const VALID_TYPES = array(
'FAX',
'HOME',
'MOBILE',
@ -43,7 +43,7 @@ class PhoneWithType {
* @param Phone $phone The phone.
*/
public function __construct( string $type, Phone $phone ) {
$this->type = in_array( $type, self::VALLID_TYPES, true ) ? $type : 'OTHER';
$this->type = in_array( $type, self::VALID_TYPES, true ) ? $type : 'OTHER';
$this->phone = $phone;
}

View file

@ -23,7 +23,7 @@ class PatchCollectionFactory {
/**
* Creates a Patch Collection by comparing two orders.
*
* @param Order $from The inital order.
* @param Order $from The initial order.
* @param Order $to The target order.
*
* @return PatchCollection

View file

@ -21,7 +21,7 @@ class ShippingPreferenceFactory {
/**
* Returns shipping_preference for the given state.
*
* @param PurchaseUnit $purchase_unit Thw PurchaseUnit.
* @param PurchaseUnit $purchase_unit The PurchaseUnit.
* @param string $context The operation context like 'checkout', 'cart'.
* @param WC_Cart|null $cart The current cart if relevant.
* @param string $funding_source The funding source (PayPal button) like 'paypal', 'venmo', 'card'.

View file

@ -0,0 +1,92 @@
<?php
/**
* PayPal Partner Attribution Helper.
*
* This class handles the retrieval and persistence of the BN (Build Notation) Code,
* which is used to track and attribute transactions for PayPal partner integrations.
*
* The BN Code is set once and remains persistent, even after disconnecting
* or uninstalling the plugin. It is determined based on the installation path
* and stored as a WordPress option.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
/**
* PayPal Partner Attribution Helper.
*
* @psalm-type installationPath = string
* @psalm-type bnCode = string
*/
class PartnerAttribution {
/**
* The BN code option name in DB.
*
* @var string
*/
protected string $bn_code_option_name;
/**
* BN Codes mapping for different installation paths.
*
* @var array<installationPath, bnCode>
*/
protected array $bn_codes;
/**
* The default BN code.
*
* @var string
*/
protected string $default_bn_code;
/**
* PartnerAttribution constructor.
*
* @param string $bn_code_option_name The BN code option name in DB.
* @param array<installationPath, bnCode> $bn_codes BN Codes mapping for different installation paths.
* @param string $default_bn_code The default BN code.
*/
public function __construct( string $bn_code_option_name, array $bn_codes, string $default_bn_code ) {
$this->bn_code_option_name = $bn_code_option_name;
$this->bn_codes = $bn_codes;
$this->default_bn_code = $default_bn_code;
}
/**
* Initializes the BN Code if not already set.
*
* This method ensures that the BN Code is only stored once during the initial setup.
*
* @param string $installation_path The installation path used to determine the BN Code.
*/
public function initialize_bn_code( string $installation_path ) : void {
$selected_bn_code = $this->bn_codes[ $installation_path ] ?? '';
if ( ! $selected_bn_code || get_option( $this->bn_code_option_name ) ) {
return;
}
// This option is permanent and should not change.
update_option( $this->bn_code_option_name, $selected_bn_code );
}
/**
* Retrieves the persisted BN Code.
*
* @return string The stored BN Code, or the default value if no path is detected.
*/
public function get_bn_code() : string {
$bn_code = (string) ( get_option( $this->bn_code_option_name, $this->default_bn_code ) ?? $this->default_bn_code );
if ( ! in_array( $bn_code, $this->bn_codes, true ) ) {
return $this->default_bn_code;
}
return $bn_code;
}
}

View file

@ -88,7 +88,10 @@ class PartnerReferralsData {
);
if ( true === $use_subscriptions ) {
$capabilities[] = 'PAYPAL_WALLET_VAULTING_ADVANCED';
if ( $this->dcc_applies->for_country_currency() ) {
$capabilities[] = 'PAYPAL_WALLET_VAULTING_ADVANCED';
}
$first_party_features[] = 'BILLING_AGREEMENT';
}

View file

@ -23,11 +23,19 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
// @deprecated - use `applepay.eligibility.check` instead.
'applepay.eligible' => static function ( ContainerInterface $container ): bool {
$eligibility_check = $container->get( 'applepay.eligibility.check' );
return $eligibility_check();
},
'applepay.eligibility.check' => static function ( ContainerInterface $container ): callable {
$apm_applies = $container->get( 'applepay.helpers.apm-applies' );
assert( $apm_applies instanceof ApmApplies );
return $apm_applies->for_country() && $apm_applies->for_currency();
return static function () use ( $apm_applies ) : bool {
return $apm_applies->for_country() && $apm_applies->for_currency() && $apm_applies->for_merchant();
};
},
'applepay.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
return new ApmApplies(

View file

@ -180,11 +180,22 @@ class ApplePayGateway extends WC_Payment_Gateway {
);
}
do_action( 'woocommerce_paypal_payments_before_process_order', $wc_order );
do_action_deprecated( 'woocommerce_paypal_payments_before_process_order', array( $wc_order ), '3.0.1', 'woocommerce_paypal_payments_before_order_process', __( 'Usage of this action is deprecated. Please use the filter woocommerce_paypal_payments_before_order_process instead.', 'woocommerce-paypal-payments' ) );
try {
try {
$this->order_processor->process( $wc_order );
/**
* This filter controls if the method 'process()' from OrderProcessor will be called.
* So you can implement your own for example on subscriptions
*
* - true bool controls execution of 'OrderProcessor::process()'
* - $this \WC_Payment_Gateway
* - $wc_order \WC_Order
*/
$process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order );
if ( $process ) {
$this->order_processor->process( $wc_order );
}
do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order );

View file

@ -370,8 +370,8 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
}
$env = $c->get( 'settings.environment' );
assert( $env instanceof Environment );
$is_sandobx = $env->current_environment_is( Environment::SANDBOX );
$this->load_domain_association_file( $is_sandobx );
$is_sandbox = $env->current_environment_is( Environment::SANDBOX );
$this->load_domain_association_file( $is_sandbox );
}
/**

View file

@ -316,7 +316,7 @@ class ApplePayDataObjectHttp {
/**
* Checks if the array contains all required fields and if those
* are not empty.
* If not it adds an unkown error to the object's error list, as this errors
* If not it adds an unknown error to the object's error list, as this errors
* are not supported by ApplePay.
*
* @param array $data The data.
@ -396,7 +396,7 @@ class ApplePayDataObjectHttp {
/**
* Checks if the address array contains all required fields and if those
* are not empty.
* If not it adds a contacField error to the object's error list.
* If not it adds a contactField error to the object's error list.
*
* @param array $post The address to check.
* @param array $required The required fields for the given address.

View file

@ -83,4 +83,16 @@ class ApmApplies {
return in_array( $this->currency->get(), $this->allowed_currencies, true );
}
/**
* Indicates, whether the current merchant is eligible for ApplePay. Always true,
* but the filter allows other modules to disable ApplePay site-wide.
*
* @return bool
*/
public function for_merchant() : bool {
return apply_filters(
'woocommerce_paypal_payments_is_eligible_for_applepay',
true
);
}
}

View file

@ -22,14 +22,20 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
return array(
// If AXO can be configured.
// @deprecated - use `axo.eligibility.check` instead.
'axo.eligible' => static function ( ContainerInterface $container ): bool {
$eligibility_check = $container->get( 'axo.eligibility.check' );
return $eligibility_check();
},
'axo.eligibility.check' => static function ( ContainerInterface $container ): callable {
$apm_applies = $container->get( 'axo.helpers.apm-applies' );
assert( $apm_applies instanceof ApmApplies );
return $apm_applies->for_country_currency();
return static function () use ( $apm_applies ) : bool {
return $apm_applies->for_country_currency() && $apm_applies->for_merchant();
};
},
'axo.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
return new ApmApplies(
$container->get( 'axo.supported-country-currency-matrix' ),

View file

@ -254,7 +254,18 @@ class AxoGateway extends WC_Payment_Gateway {
$order = $this->create_paypal_order( $wc_order, $token );
$this->order_processor->process_captured_and_authorized( $wc_order, $order );
/**
* This filter controls if the method 'process()' from OrderProcessor will be called.
* So you can implement your own for example on subscriptions
*
* - true bool controls execution of 'OrderProcessor::process()'
* - $this \WC_Payment_Gateway
* - $wc_order \WC_Order
*/
$process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order );
if ( $process ) {
$this->order_processor->process_captured_and_authorized( $wc_order, $order );
}
} catch ( Exception $exception ) {
return $this->handle_payment_failure( $wc_order, $exception );
}

View file

@ -66,4 +66,17 @@ class ApmApplies {
}
return in_array( $this->currency->get(), $this->allowed_country_currency_matrix[ $this->country ], true );
}
/**
* Indicates, whether the current merchant is eligible for Fastlane. Always true,
* but the filter allows other modules to disable Fastlane site-wide.
*
* @return bool
*/
public function for_merchant() : bool {
return apply_filters(
'woocommerce_paypal_payments_is_eligible_for_axo',
true
);
}
}

View file

@ -1,4 +1,4 @@
import MessagesBootstrap from '../../../../ppcp-button/resources/js/modules/ContextBootstrap/MessagesBootstap';
import MessagesBootstrap from '../../../../ppcp-button/resources/js/modules/ContextBootstrap/MessagesBootstrap';
import { debounce } from '../Helper/debounce';
class BlockCheckoutMessagesBootstrap {

View file

@ -59,7 +59,7 @@ if ( cartHasSubscriptionProducts( config.scriptData ) ) {
blockEnabled = false;
}
// Don't show buttons if cart contains free trial product and the stroe is not eligible for saving payment methods.
// Don't show buttons if cart contains free trial product and the store is not eligible for saving payment methods.
if (
! config.scriptData.vault_v3_enabled &&
config.scriptData.is_free_trial_cart

View file

@ -1,7 +1,7 @@
import MiniCartBootstap from './modules/ContextBootstrap/MiniCartBootstap';
import SingleProductBootstap from './modules/ContextBootstrap/SingleProductBootstap';
import CartBootstrap from './modules/ContextBootstrap/CartBootstap';
import CheckoutBootstap from './modules/ContextBootstrap/CheckoutBootstap';
import MiniCartBootstrap from './modules/ContextBootstrap/MiniCartBootstrap';
import SingleProductBootstrap from './modules/ContextBootstrap/SingleProductBootstrap';
import CartBootstrap from './modules/ContextBootstrap/CartBootstrap';
import CheckoutBootstrap from './modules/ContextBootstrap/CheckoutBootstrap';
import PayNowBootstrap from './modules/ContextBootstrap/PayNowBootstrap';
import Renderer from './modules/Renderer/Renderer';
import ErrorHandler from './modules/ErrorHandler';
@ -23,7 +23,7 @@ import FormSaver from './modules/Helper/FormSaver';
import FormValidator from './modules/Helper/FormValidator';
import { loadPaypalScript } from './modules/Helper/ScriptLoading';
import buttonModuleWatcher from './modules/ButtonModuleWatcher';
import MessagesBootstrap from './modules/ContextBootstrap/MessagesBootstap';
import MessagesBootstrap from './modules/ContextBootstrap/MessagesBootstrap';
import { apmButtonsInit } from './modules/Helper/ApmButtons';
// TODO: could be a good idea to have a separate spinner for each gateway,
@ -246,7 +246,7 @@ const bootstrap = () => {
);
if ( PayPalCommerceGateway.mini_cart_buttons_enabled === '1' ) {
const miniCartBootstrap = new MiniCartBootstap(
const miniCartBootstrap = new MiniCartBootstrap(
PayPalCommerceGateway,
renderer,
errorHandler
@ -264,7 +264,7 @@ const bootstrap = () => {
( PayPalCommerceGateway.single_product_buttons_enabled === '1' ||
hasMessages() )
) {
const singleProductBootstrap = new SingleProductBootstap(
const singleProductBootstrap = new SingleProductBootstrap(
PayPalCommerceGateway,
renderer,
errorHandler
@ -289,17 +289,17 @@ const bootstrap = () => {
}
if ( context === 'checkout' ) {
const checkoutBootstap = new CheckoutBootstap(
const checkoutBootstrap = new CheckoutBootstrap(
PayPalCommerceGateway,
renderer,
spinner,
errorHandler
);
checkoutBootstap.init();
checkoutBootstrap.init();
buttonModuleWatcher.registerContextBootstrap(
'checkout',
checkoutBootstap
checkoutBootstrap
);
}

View file

@ -15,7 +15,7 @@ import {
dispatchButtonEvent,
} from '../Helper/PaymentButtonHelpers';
class CheckoutBootstap {
class CheckoutBootstrap {
constructor( gateway, renderer, spinner, errorHandler ) {
this.gateway = gateway;
this.renderer = renderer;
@ -344,4 +344,4 @@ class CheckoutBootstap {
}
}
export default CheckoutBootstap;
export default CheckoutBootstrap;

View file

@ -1,7 +1,7 @@
import CartActionHandler from '../ActionHandler/CartActionHandler';
import BootstrapHelper from '../Helper/BootstrapHelper';
class MiniCartBootstap {
class MiniCartBootstrap {
constructor( gateway, renderer, errorHandler ) {
this.gateway = gateway;
this.renderer = renderer;
@ -71,4 +71,4 @@ class MiniCartBootstap {
}
}
export default MiniCartBootstap;
export default MiniCartBootstrap;

View file

@ -1,7 +1,7 @@
import CheckoutBootstap from './CheckoutBootstap';
import CheckoutBootstrap from './CheckoutBootstrap';
import { isChangePaymentPage } from '../Helper/Subscriptions';
class PayNowBootstrap extends CheckoutBootstap {
class PayNowBootstrap extends CheckoutBootstrap {
constructor( gateway, renderer, spinner, errorHandler ) {
super( gateway, renderer, spinner, errorHandler );
}

View file

@ -9,7 +9,7 @@ import { strRemoveWord, strAddWord, throttle } from '../Helper/Utils';
import merge from 'deepmerge';
import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce';
class SingleProductBootstap {
class SingleProductBootstrap {
constructor( gateway, renderer, errorHandler ) {
this.gateway = gateway;
this.renderer = renderer;
@ -162,6 +162,12 @@ class SingleProductBootstap {
},
]
.map( ( f ) => f() )
.sort((a, b) => {
if (parseInt(a.replace(/\D/g, '')) < parseInt(b.replace(/\D/g, '')) ) {
return 1;
}
return -1;
})
.find( ( val ) => val );
if ( typeof priceText === 'undefined' ) {
@ -368,4 +374,4 @@ class SingleProductBootstap {
}
}
export default SingleProductBootstap;
export default SingleProductBootstrap;

View file

@ -15,7 +15,7 @@ class PreviewButtonManager {
/**
* Resolves the promise.
* Used by `this.boostrap()` to process enqueued initialization logic.
* Used by `this.bootstrap()` to process enqueued initialization logic.
*/
#onInitResolver;

View file

@ -56,9 +56,9 @@ class CardFieldsRenderer {
onApprove( data ) {
return contextConfig.onApprove( data );
},
onError( error ) {
console.error( error );
this.spinner.unblock();
onError: ( error ) => {
console.error( error );
this.spinner.unblock();
},
} );
@ -105,11 +105,13 @@ class CardFieldsRenderer {
}
cardFields.submit().catch( ( error ) => {
this.spinner.unblock();
console.error( error );
this.errorHandler.message(
this.defaultConfig.hosted_fields.labels.fields_not_valid
);
this.spinner.unblock();
if (!error.type || error.type !== 'create-order-error') {
console.error( error );
this.errorHandler.message(
this.defaultConfig.hosted_fields.labels.fields_not_valid
);
}
} );
} );
}

View file

@ -168,7 +168,8 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'button.handle-shipping-in-paypal' ),
$container->get( 'button.helper.disabled-funding-sources' ),
$container->get( 'wcgateway.configuration.card-configuration' )
$container->get( 'wcgateway.configuration.card-configuration' ),
$container->get( 'api.helper.partner-attribution' )
);
},
'button.url' => static function ( ContainerInterface $container ): string {

View file

@ -21,6 +21,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
use WooCommerce\PayPalCommerce\Blocks\Endpoint\UpdateShippingEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
@ -245,6 +246,13 @@ class SmartButton implements SmartButtonInterface {
*/
private CardPaymentsConfiguration $dcc_configuration;
/**
* The PayPal Partner Attribution Helper.
*
* @var PartnerAttribution
*/
protected PartnerAttribution $partner_attribution;
/**
* SmartButton constructor.
*
@ -273,6 +281,7 @@ class SmartButton implements SmartButtonInterface {
* @param bool $should_handle_shipping_in_paypal Whether the shipping should be handled in PayPal.
* @param DisabledFundingSources $disabled_funding_sources List of funding sources to be disabled.
* @param CardPaymentsConfiguration $dcc_configuration The DCC Gateway Configuration.
* @param PartnerAttribution $partner_attribution The PayPal Partner Attribution Helper.
*/
public function __construct(
string $module_url,
@ -299,7 +308,8 @@ class SmartButton implements SmartButtonInterface {
LoggerInterface $logger,
bool $should_handle_shipping_in_paypal,
DisabledFundingSources $disabled_funding_sources,
CardPaymentsConfiguration $dcc_configuration
CardPaymentsConfiguration $dcc_configuration,
PartnerAttribution $partner_attribution
) {
$this->module_url = $module_url;
$this->version = $version;
@ -326,6 +336,7 @@ class SmartButton implements SmartButtonInterface {
$this->should_handle_shipping_in_paypal = $should_handle_shipping_in_paypal;
$this->disabled_funding_sources = $disabled_funding_sources;
$this->dcc_configuration = $dcc_configuration;
$this->partner_attribution = $partner_attribution;
}
/**
@ -841,9 +852,7 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
*/
do_action( "ppcp_before_{$location_hook}_message_wrapper" );
$bn_code = PPCP_PAYPAL_BN_CODE;
$messages_placeholder = '<div class="ppcp-messages" data-partner-attribution-id="' . esc_attr( $bn_code ) . '"></div>';
$messages_placeholder = '<div class="ppcp-messages" data-partner-attribution-id="' . esc_attr( $this->partner_attribution->get_bn_code() ) . '"></div>';
if ( is_array( $block_params ) && ( $block_params['blockName'] ?? false ) ) {
$this->render_after_block(
@ -1540,12 +1549,9 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
* @return string
*/
private function bn_code_for_context( string $context ): string {
$codes = $this->bn_codes();
$bn_code = PPCP_PAYPAL_BN_CODE;
return ( isset( $codes[ $context ] ) ) ? $codes[ $context ] : $bn_code;
return ( isset( $codes[ $context ] ) ) ? $codes[ $context ] : $this->partner_attribution->get_bn_code();
}
/**
@ -1555,7 +1561,7 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
*/
private function bn_codes() : array {
$bn_code = PPCP_PAYPAL_BN_CODE;
$bn_code = $this->partner_attribution->get_bn_code();
return array(
'checkout' => $bn_code,

View file

@ -188,7 +188,7 @@ class DisabledFundingSources {
/**
* Filters the final list of disabled funding sources.
*
* @param array $diabled_funding The filter value, funding sources to be disabled.
* @param array $disable_funding The filter value, funding sources to be disabled.
* @param array $flags Decision flags to provide more context to filters.
*/
$disable_funding = apply_filters(

View file

@ -1,6 +1,6 @@
<?php
/**
* Handles the Early Order logic, when we need to create the WC_Order by ourselfs.
* Handles the Early Order logic, when we need to create the WC_Order by ourselves.
*
* @package WooCommerce\PayPalCommerce\Button\Helper
*/

View file

@ -13,11 +13,19 @@ use WooCommerce\PayPalCommerce\CardFields\Helper\CardFieldsApplies;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
// @deprecated - use `card-fields.eligibility.check` instead.
'card-fields.eligible' => static function ( ContainerInterface $container ): bool {
$eligibility_check = $container->get( 'card-fields.eligibility.check' );
return $eligibility_check();
},
'card-fields.eligibility.check' => static function ( ContainerInterface $container ): callable {
$save_payment_methods_applies = $container->get( 'card-fields.helpers.save-payment-methods-applies' );
assert( $save_payment_methods_applies instanceof CardFieldsApplies );
return $save_payment_methods_applies->for_country();
return static function () use ( $save_payment_methods_applies ) : bool {
return $save_payment_methods_applies->for_country() && $save_payment_methods_applies->for_merchant();
};
},
'card-fields.helpers.save-payment-methods-applies' => static function ( ContainerInterface $container ) : CardFieldsApplies {
return new CardFieldsApplies(

View file

@ -50,4 +50,17 @@ class CardFieldsApplies {
public function for_country(): bool {
return in_array( $this->country, $this->allowed_country_matrix, true );
}
/**
* Indicates, whether the current merchant is eligible for Card Fields. Always true,
* but the filter allows other modules to disable Card Fields site-wide.
*
* @return bool
*/
public function for_merchant() : bool {
return apply_filters(
'woocommerce_paypal_payments_is_eligible_for_card_fields',
true
);
}
}

View file

@ -23,14 +23,20 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
// If GooglePay can be configured.
// @deprecated - use `googlepay.eligibility.check` instead.
'googlepay.eligible' => static function ( ContainerInterface $container ): bool {
$eligibility_check = $container->get( 'googlepay.eligibility.check' );
return $eligibility_check();
},
'googlepay.eligibility.check' => static function ( ContainerInterface $container ): callable {
$apm_applies = $container->get( 'googlepay.helpers.apm-applies' );
assert( $apm_applies instanceof ApmApplies );
return $apm_applies->for_country() && $apm_applies->for_currency();
return static function () use ( $apm_applies ) : bool {
return $apm_applies->for_country() && $apm_applies->for_currency() && $apm_applies->for_merchant();
};
},
'googlepay.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
return new ApmApplies(
$container->get( 'googlepay.supported-countries' ),

View file

@ -189,11 +189,22 @@ class GooglePayGateway extends WC_Payment_Gateway {
}
//phpcs:enable WordPress.Security.NonceVerification.Recommended
do_action( 'woocommerce_paypal_payments_before_process_order', $wc_order );
do_action_deprecated( 'woocommerce_paypal_payments_before_process_order', array( $wc_order ), '3.0.1', 'woocommerce_paypal_payments_before_order_process', __( 'Usage of this action is deprecated. Please use the filter woocommerce_paypal_payments_before_order_process instead.', 'woocommerce-paypal-payments' ) );
try {
try {
$this->order_processor->process( $wc_order );
/**
* This filter controls if the method 'process()' from OrderProcessor will be called.
* So you can implement your own for example on subscriptions
*
* - true bool controls execution of 'OrderProcessor::process()'
* - $this \WC_Payment_Gateway
* - $wc_order \WC_Order
*/
$process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order );
if ( $process ) {
$this->order_processor->process( $wc_order );
}
do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order );

View file

@ -83,4 +83,16 @@ class ApmApplies {
return in_array( $this->currency->get(), $this->allowed_currencies, true );
}
/**
* Indicates, whether the current merchant is eligible for GooglePay. Always true,
* but the filter allows other modules to disable GooglePay site-wide.
*
* @return bool
*/
public function for_merchant() : bool {
return apply_filters(
'woocommerce_paypal_payments_is_eligible_for_googlepay',
true
);
}
}

View file

@ -236,10 +236,10 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) {
( element.style.display = ! isExpress ? '' : 'none' )
);
const screemImg = document.querySelector(
const screenImg = document.querySelector(
'#ppcp-onboarding-cards-screen-img'
);
if ( screemImg ) {
if ( screenImg ) {
const currentRb =
Array.from(
document.querySelectorAll(
@ -248,7 +248,7 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) {
).filter( ( rb ) => rb.checked )[ 0 ] ?? null;
const imgUrl = currentRb.getAttribute( 'data-screen-url' );
screemImg.src = imgUrl;
screenImg.src = imgUrl;
}
};

View file

@ -359,7 +359,7 @@ document.addEventListener( 'DOMContentLoaded', () => {
payLaterMessagingInputSelectorByLocation( location )
)
: inputSelectros.concat(
butttonInputSelectorByLocation( location )
buttonInputSelectorByLocation( location )
);
} );
@ -386,7 +386,7 @@ document.addEventListener( 'DOMContentLoaded', () => {
return inputSelectors;
};
const butttonInputSelectorByLocation = ( location ) => {
const buttonInputSelectorByLocation = ( location ) => {
const locationPrefix = location === 'checkout' ? '' : '_' + location;
const inputSelectors = [
'#field-button' + locationPrefix + '_layout',

View file

@ -52,8 +52,9 @@ class OnboardingModule implements ServiceModule, ExtendingModule, ExecutableModu
apply_filters(
// phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled',
getenv( 'PCP_SETTINGS_ENABLED' ) === '1'
) && ! SettingsModule::should_use_the_old_ui()
'1' === get_option( 'woocommerce-ppcp-is-new-merchant' )
|| getenv( 'PCP_SETTINGS_ENABLED' ) === '1'
)
) {
return;
}

View file

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterBlock;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
@ -28,7 +29,10 @@ class PayLaterBlockRenderer {
public function render( array $attributes, ContainerInterface $c ): string {
if ( PayLaterBlockModule::is_block_enabled( $c->get( 'wcgateway.settings.status' ) ) ) {
$bn_code = PPCP_PAYPAL_BN_CODE;
$partner_attribution = $c->get( 'api.helper.partner-attribution' );
assert( $partner_attribution instanceof PartnerAttribution );
$bn_code = $partner_attribution->get_bn_code();
$html = '<div id="' . esc_attr( $attributes['id'] ?? '' ) . '" class="ppcp-messages" data-partner-attribution-id="' . esc_attr( $bn_code ) . '"></div>';

View file

@ -14,6 +14,7 @@ use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\GetConfig;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Factory\ConfigFactory;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
@ -54,9 +55,15 @@ return array(
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$dcc_product_status = $container->get( 'wcgateway.helper.dcc-product-status' );
assert( $dcc_product_status instanceof DCCProductStatus );
$card_fields_eligible = $container->get( 'card-fields.eligible' );
$vault_enabled = $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' );
return ! $vault_enabled && $messages_apply->for_country();
// Pay Later Messaging is available if vaulting is not enabled, the shop country is supported, and is eligible for ACDC.
return ! $vault_enabled && $messages_apply->for_country() && $dcc_product_status->is_active() && $card_fields_eligible;
},
'paylater-configurator.messaging-locations' => static function ( ContainerInterface $container ) : array {
// Get an array of locations that display the Pay-Later message.

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterConfigurator;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\GetConfig;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Factory\ConfigFactory;
@ -126,7 +127,8 @@ class PayLaterConfiguratorModule implements ServiceModule, ExtendingModule, Exec
$config_factory = $c->get( 'paylater-configurator.factory.config' );
assert( $config_factory instanceof ConfigFactory );
$bn_code = PPCP_PAYPAL_BN_CODE;
$partner_attribution = $c->get( 'api.helper.partner-attribution' );
assert( $partner_attribution instanceof PartnerAttribution );
wp_localize_script(
'ppcp-paylater-configurator',
@ -145,7 +147,7 @@ class PayLaterConfiguratorModule implements ServiceModule, ExtendingModule, Exec
'config' => $config_factory->from_settings( $settings ),
'merchantClientId' => $settings->get( 'client_id' ),
'partnerClientId' => $c->get( 'api.partner_merchant_id' ),
'bnCode' => $bn_code,
'bnCode' => $partner_attribution->get_bn_code(),
'publishButtonClassName' => 'ppcp-paylater-configurator-publishButton',
'headerClassName' => 'ppcp-paylater-configurator-header',
'subheaderClassName' => 'ppcp-paylater-configurator-subheader',

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterWCBlocks;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
@ -95,7 +96,10 @@ class PayLaterWCBlocksRenderer {
) {
if ( PayLaterWCBlocksModule::is_placement_enabled( $c->get( 'wcgateway.settings.status' ), $location ) ) {
$bn_code = PPCP_PAYPAL_BN_CODE;
$partner_attribution = $c->get( 'api.helper.partner-attribution' );
assert( $partner_attribution instanceof PartnerAttribution );
$bn_code = $partner_attribution->get_bn_code();
$html = '<div id="' . esc_attr( $attributes['ppcpId'] ?? '' ) . '" class="ppcp-messages" data-partner-attribution-id="' . esc_attr( $bn_code ) . '"></div>';

View file

@ -9,7 +9,7 @@ document.addEventListener( 'DOMContentLoaded', () => {
const variableId = children[ i ]
.querySelector( 'h3' )
.getElementsByClassName( 'variable_post_id' )[ 0 ].value;
if ( parseInt( variableId ) === productId ) {
if ( variableId === productId ) {
children[ i ]
.querySelector( '.woocommerce_variable_attributes' )
.getElementsByClassName(
@ -83,7 +83,7 @@ document.addEventListener( 'DOMContentLoaded', () => {
if (! price || parseInt( price ) <= 0 ) {
linkBtn.setAttribute('title', __( 'Prices must be above zero for PayPal Subscriptions!', 'woocommerce-paypal-subscriptions' ) );
} else {
linkBtn.setAttribute('title', __( 'Not allowed period intervall combination for PayPal Subscriptions!', 'woocommerce-paypal-subscriptions' ) );
linkBtn.setAttribute('title', __( 'Not allowed period interval combination for PayPal Subscriptions!', 'woocommerce-paypal-subscriptions' ) );
}
} else {
@ -122,21 +122,26 @@ document.addEventListener( 'DOMContentLoaded', () => {
jQuery( '.wc_input_subscription_price' ).trigger( 'change' );
PayPalCommerceGatewayPayPalSubscriptionProducts?.forEach(
( product ) => {
if ( product.product_connected === 'yes' ) {
disableFields( product.product_id );
}
let variationProductIds = [ PayPalCommerceGatewayPayPalSubscriptionProducts.product_id ];
const variationsInput = document.querySelectorAll( '.variable_post_id' );
for ( let i = 0; i < variationsInput.length; i++ ) {
variationProductIds.push( variationsInput[ i ].value );
}
variationProductIds?.forEach(
( productId ) => {
const linkBtn = document.getElementById(
`ppcp_enable_subscription_product-${ product.product_id }`
`ppcp_enable_subscription_product-${ productId }`
);
if ( linkBtn.checked && linkBtn.value === 'yes' ) {
disableFields( productId );
}
linkBtn?.addEventListener( 'click', ( event ) => {
const unlinkBtnP = document.getElementById(
`ppcp-enable-subscription-${ product.product_id }`
`ppcp-enable-subscription-${ productId }`
);
const titleP = document.getElementById(
`ppcp_subscription_plan_name_p-${ product.product_id }`
`ppcp_subscription_plan_name_p-${ productId }`
);
if (event.target.checked === true) {
if ( unlinkBtnP ) {
@ -156,26 +161,26 @@ document.addEventListener( 'DOMContentLoaded', () => {
});
const unlinkBtn = document.getElementById(
`ppcp-unlink-sub-plan-${ product.product_id }`
`ppcp-unlink-sub-plan-${ productId }`
);
unlinkBtn?.addEventListener( 'click', ( event ) => {
event.preventDefault();
unlinkBtn.disabled = true;
const spinner = document.getElementById(
'spinner-unlink-plan'
`spinner-unlink-plan-${ productId }`
);
spinner.style.display = 'inline-block';
fetch( product.ajax.deactivate_plan.endpoint, {
fetch( PayPalCommerceGatewayPayPalSubscriptionProducts.ajax.deactivate_plan.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify( {
nonce: product.ajax.deactivate_plan.nonce,
plan_id: product.plan_id,
product_id: product.product_id,
nonce: PayPalCommerceGatewayPayPalSubscriptionProducts.ajax.deactivate_plan.nonce,
plan_id: linkBtn.dataset.subsPlan,
product_id: productId,
} ),
} )
.then( function ( res ) {

View file

@ -12,10 +12,7 @@ namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use ActionScheduler_Store;
use WC_Order;
use WC_Product;
use WC_Product_Subscription;
use WC_Product_Subscription_Variation;
use WC_Product_Variable;
use WC_Product_Variable_Subscription;
use WC_Subscription;
use WC_Subscriptions_Product;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
@ -28,6 +25,7 @@ use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameI
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WP_Post;
@ -56,6 +54,146 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
if ( ! $subscriptions_helper->plugin_is_active() ) {
return true;
}
add_filter(
'woocommerce_available_payment_gateways',
function ( array $gateways ) use ( $c ) {
if ( is_account_page() || is_admin() || ! WC()->cart || WC()->cart->is_empty() || wcs_is_manual_renewal_enabled() ) {
return $gateways;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$subscriptions_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : '';
if ( $subscriptions_mode !== 'subscriptions_api' ) {
return $gateways;
}
$pp_subscriptions_product = false;
foreach ( WC()->cart->get_cart() as $cart_item ) {
$cart_product = wc_get_product( $cart_item['product_id'] );
if ( isset( $cart_item['subscription_renewal']['subscription_id'] ) ) {
$subscription_renewal = wcs_get_subscription( $cart_item['subscription_renewal']['subscription_id'] );
if ( $subscription_renewal && $subscription_renewal->get_meta( 'ppcp_subscription' ) ) {
$pp_subscriptions_product = true;
break;
}
} elseif ( $cart_product instanceof \WC_Product_Subscription || $cart_product instanceof \WC_Product_Variable_Subscription ) {
if ( $cart_product->get_meta( '_ppcp_enable_subscription_product' ) === 'yes' ) {
$pp_subscriptions_product = true;
break;
}
}
}
if ( $pp_subscriptions_product ) {
foreach ( $gateways as $id => $gateway ) {
if ( $gateway->id !== PayPalGateway::ID ) {
unset( $gateways[ $id ] );
}
}
return $gateways;
}
return $gateways;
}
);
add_filter(
'woocommerce_subscription_payment_gateway_supports',
function ( bool $payment_gateway_supports, string $payment_gateway_feature, \WC_Subscription $wc_order ): bool {
if ( ! in_array( $payment_gateway_feature, array( 'gateway_scheduled_payments', 'subscription_date_changes', 'subscription_amount_changes', 'subscription_payment_method_change', 'subscription_payment_method_change_customer', 'subscription_payment_method_change_admin' ), true ) ) {
return $payment_gateway_supports;
}
$subscription = wcs_get_subscription( $wc_order->get_id() );
if ( ! is_a( $subscription, WC_Subscription::class ) ) {
return $payment_gateway_supports;
}
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( ! $subscription_id ) {
return $payment_gateway_supports;
}
if ( $payment_gateway_feature === 'gateway_scheduled_payments' ) {
return true;
}
return false;
},
100,
3
);
add_filter(
'woocommerce_can_subscription_be_updated_to_active',
function ( bool $can_be_updated, \WC_Subscription $subscription ) use ( $c ) {
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $subscription_id && $subscription->get_status() === 'pending-cancel' ) {
return true;
}
return $can_be_updated;
},
10,
2
);
add_filter(
'woocommerce_can_subscription_be_updated_to_new-payment-method',
function ( bool $can_be_updated, \WC_Subscription $subscription ) use ( $c ) {
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $subscription_id ) {
return false;
}
return $can_be_updated;
},
10,
2
);
add_filter(
'woocommerce_paypal_payments_before_order_process',
function ( bool $process, \WC_Payment_Gateway $gateway, \WC_Order $wc_order ) use ( $c ) {
if ( ! $gateway instanceof PayPalGateway || $gateway::ID !== 'ppcp-gateway' ) {
return $process;
}
$paypal_subscription_id = \WC()->session->get( 'ppcp_subscription_id' );
if ( empty( $paypal_subscription_id ) || ! is_string( $paypal_subscription_id ) ) {
return $process;
}
$order = $c->get( 'session.handler' )->order();
$gateway->add_paypal_meta( $wc_order, $order, $c->get( 'settings.environment' ) );
$subscriptions = function_exists( 'wcs_get_subscriptions_for_order' ) ? wcs_get_subscriptions_for_order( $wc_order ) : array();
foreach ( $subscriptions as $subscription ) {
$subscription->update_meta_data( 'ppcp_subscription', $paypal_subscription_id );
$subscription->save();
// translators: %s PayPal Subscription id.
$subscription->add_order_note( sprintf( __( 'PayPal subscription %s added.', 'woocommerce-paypal-payments' ), $paypal_subscription_id ) );
}
$transaction_id = $gateway->get_paypal_order_transaction_id( $order );
if ( $transaction_id ) {
$gateway->update_transaction_id( $transaction_id, $wc_order, $c->get( 'woocommerce.logger.woocommerce' ) );
}
$wc_order->payment_complete();
return false;
},
10,
3
);
add_action(
'save_post',
/**
@ -64,12 +202,6 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
* @psalm-suppress MissingClosureParamType
*/
function( $product_id ) use ( $c ) {
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
if ( ! $subscriptions_helper->plugin_is_active() ) {
return;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
@ -82,6 +214,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
$nonce = wc_clean( wp_unslash( $_POST['_wcsnonce'] ?? '' ) );
if (
$subscriptions_mode !== 'subscriptions_api'
|| wcs_is_manual_renewal_enabled()
|| ! is_string( $nonce )
|| ! wp_verify_nonce( $nonce, 'wcs_subscription_meta' ) ) {
return;
@ -107,7 +240,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
* @psalm-suppress MissingClosureParamType
*/
static function ( $passed_validation, $product_id ) use ( $c ) {
if ( WC()->cart->is_empty() ) {
if ( WC()->cart->is_empty() || wcs_is_manual_renewal_enabled() ) {
return $passed_validation;
}
@ -163,12 +296,9 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
function( $variation_id ) use ( $c ) {
$wcsnonce_save_variations = wc_clean( wp_unslash( $_POST['_wcsnonce_save_variations'] ?? '' ) );
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
if (
! $subscriptions_helper->plugin_is_active()
|| ! WC_Subscriptions_Product::is_subscription( $variation_id )
! WC_Subscriptions_Product::is_subscription( $variation_id )
|| wcs_is_manual_renewal_enabled()
|| ! is_string( $wcsnonce_save_variations )
|| ! wp_verify_nonce( $wcsnonce_save_variations, 'wcs_subscription_variations' )
) {
@ -234,127 +364,6 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
}
);
add_filter(
'woocommerce_order_actions',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $actions, $subscription = null ): array {
if ( ! is_array( $actions ) || ! is_a( $subscription, WC_Subscription::class ) ) {
return $actions;
}
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $subscription_id && isset( $actions['wcs_process_renewal'] ) ) {
unset( $actions['wcs_process_renewal'] );
}
return $actions;
},
20,
2
);
add_filter(
'wcs_view_subscription_actions',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $actions, $subscription ): array {
if ( ! is_a( $subscription, WC_Subscription::class ) ) {
return $actions;
}
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $subscription_id && $subscription->get_status() === 'active' ) {
$url = wp_nonce_url(
add_query_arg(
array(
'change_subscription_to' => 'cancelled',
'ppcp_cancel_subscription' => $subscription->get_id(),
)
),
'ppcp_cancel_subscription_nonce'
);
array_unshift(
$actions,
array(
'url' => esc_url( $url ),
'name' => esc_html__( 'Cancel', 'woocommerce-paypal-payments' ),
)
);
$actions['cancel']['name'] = esc_html__( 'Suspend', 'woocommerce-paypal-payments' );
unset( $actions['subscription_renewal_early'] );
}
return $actions;
},
11,
2
);
add_action(
'wp_loaded',
function() use ( $c ) {
if ( ! function_exists( 'wcs_get_subscription' ) ) {
return;
}
$cancel_subscription_id = wc_clean( wp_unslash( $_GET['ppcp_cancel_subscription'] ?? '' ) );
$subscription = wcs_get_subscription( absint( $cancel_subscription_id ) );
if ( ! wcs_is_subscription( $subscription ) || $subscription === false ) {
return;
}
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
$nonce = wc_clean( wp_unslash( $_GET['_wpnonce'] ?? '' ) );
if ( ! is_string( $nonce ) ) {
return;
}
if (
$subscription_id
&& $cancel_subscription_id
&& $nonce
) {
if (
! wp_verify_nonce( $nonce, 'ppcp_cancel_subscription_nonce' )
|| ! user_can( get_current_user_id(), 'edit_shop_subscription_status', $subscription->get_id() )
) {
return;
}
$subscriptions_endpoint = $c->get( 'api.endpoint.billing-subscriptions' );
$subscription_id = $subscription->get_meta( 'ppcp_subscription' );
try {
$subscriptions_endpoint->cancel( $subscription_id );
$subscription->update_status( 'cancelled' );
$subscription->add_order_note( __( 'Subscription cancelled by the subscriber from their account page.', 'woocommerce-paypal-payments' ) );
wc_add_notice( __( 'Your subscription has been cancelled.', 'woocommerce-paypal-payments' ) );
wp_safe_redirect( $subscription->get_view_order_url() );
exit;
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$logger->error( 'Could not cancel subscription product on PayPal. ' . $error );
}
}
},
100
);
add_action(
'woocommerce_subscription_before_actions',
/**
@ -459,6 +468,9 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
add_action(
'woocommerce_product_options_general_product_data',
function() use ( $c ) {
if ( wcs_is_manual_renewal_enabled() ) {
return;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
@ -496,6 +508,9 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
* @psalm-suppress MissingClosureParamType
*/
function( $loop, $variation_data, $variation ) use ( $c ) {
if ( wcs_is_manual_renewal_enabled() ) {
return;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
@ -527,34 +542,12 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
* @psalm-suppress MissingClosureParamType
*/
function( $hook ) use ( $c ) {
if ( ! is_string( $hook ) ) {
if ( ! is_string( $hook ) || wcs_is_manual_renewal_enabled() ) {
return;
}
$settings = $c->get( 'wcgateway.settings' );
$subscription_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : '';
if ( $hook !== 'post.php' || $subscription_mode !== 'subscriptions_api' ) {
return;
}
//phpcs:disable WordPress.Security.NonceVerification.Recommended
$post_id = wc_clean( wp_unslash( $_GET['post'] ?? '' ) );
$product = wc_get_product( $post_id );
if ( ! ( is_a( $product, WC_Product::class ) ) ) {
return;
}
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
if (
! $subscriptions_helper->plugin_is_active()
|| ! (
is_a( $product, WC_Product_Subscription::class )
|| is_a( $product, WC_Product_Variable_Subscription::class )
|| is_a( $product, WC_Product_Subscription_Variation::class )
)
|| ! WC_Subscriptions_Product::is_subscription( $product )
) {
if ( $hook !== 'post.php' && $hook !== 'post-new.php' && $subscription_mode !== 'subscriptions_api' ) {
return;
}
@ -562,7 +555,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
wp_enqueue_script(
'ppcp-paypal-subscription',
untrailingslashit( $module_url ) . '/assets/js/paypal-subscription.js',
array( 'jquery', 'wc-admin-product-editor' ),
array( 'jquery' ),
$c->get( 'ppcp.asset-version' ),
true
);
@ -572,34 +565,23 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
'woocommerce-paypal-payments'
);
$products = array( $this->set_product_config( $product ) );
if ( $product->get_type() === 'variable-subscription' ) {
$products = array();
/**
* Suppress pslam.
*
* @psalm-suppress TypeDoesNotContainType
*
* WC_Product_Variable_Subscription extends WC_Product_Variable.
*/
assert( $product instanceof WC_Product_Variable );
$available_variations = $product->get_available_variations();
foreach ( $available_variations as $variation ) {
/**
* The method is defined in WooCommerce.
*
* @psalm-suppress UndefinedMethod
*/
$variation = wc_get_product_object( 'variation', $variation['variation_id'] );
$products[] = $this->set_product_config( $variation );
}
$product = wc_get_product();
if ( ! $product ) {
return;
}
wp_localize_script(
'ppcp-paypal-subscription',
'PayPalCommerceGatewayPayPalSubscriptionProducts',
$products
array(
'ajax' => array(
'deactivate_plan' => array(
'endpoint' => \WC_AJAX::get_endpoint( DeactivatePlanEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( DeactivatePlanEndpoint::ENDPOINT ),
),
),
'product_id' => $product->get_id(),
)
);
}
);
@ -663,35 +645,6 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
2
);
add_action(
'action_scheduler_before_execute',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $action_id ) {
/**
* Class exist in WooCommerce.
*
* @psalm-suppress UndefinedClass
*/
$store = ActionScheduler_Store::instance();
$action = $store->fetch_action( $action_id );
$subscription_id = $action->get_args()['subscription_id'] ?? null;
if ( $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
if ( is_a( $subscription, WC_Subscription::class ) ) {
$paypal_subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $paypal_subscription_id ) {
as_unschedule_action( $action->get_hook(), $action->get_args() );
}
}
}
}
);
return true;
}
@ -747,29 +700,6 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
}
}
/**
* Returns subscription product configuration.
*
* @param WC_Product $product The product.
* @return array
*/
private function set_product_config( WC_Product $product ): array {
$plan = $product->get_meta( 'ppcp_subscription_plan' ) ?? array();
$plan_id = $plan['id'] ?? '';
return array(
'product_connected' => $product->get_meta( '_ppcp_enable_subscription_product' ) ?? '',
'plan_id' => $plan_id,
'product_id' => $product->get_id(),
'ajax' => array(
'deactivate_plan' => array(
'endpoint' => \WC_AJAX::get_endpoint( DeactivatePlanEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( DeactivatePlanEndpoint::ENDPOINT ),
),
),
);
}
/**
* Render PayPal Subscriptions fields.
*
@ -780,15 +710,19 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
private function render_paypal_subscription_fields( WC_Product $product, Environment $environment ): void {
$enable_subscription_product = $product->get_meta( '_ppcp_enable_subscription_product' );
$style = $product->get_type() === 'subscription_variation' ? 'float:left; width:150px;' : '';
$subscription_product = $product->get_meta( 'ppcp_subscription_product' );
$subscription_plan = $product->get_meta( 'ppcp_subscription_plan' );
$subscription_plan_name = $product->get_meta( '_ppcp_subscription_plan_name' );
echo '<p class="form-field">';
echo sprintf(
// translators: %1$s and %2$s are label open and close tags.
esc_html__( '%1$sConnect to PayPal%2$s', 'woocommerce-paypal-payments' ),
'<label for="_ppcp_enable_subscription_product-' . esc_attr( (string) $product->get_id() ) . '" style="' . esc_attr( $style ) . '">',
'<label for="ppcp_enable_subscription_product-' . esc_attr( (string) $product->get_id() ) . '" style="' . esc_attr( $style ) . '">',
'</label>'
);
echo '<input type="checkbox" id="ppcp_enable_subscription_product-' . esc_attr( (string) $product->get_id() ) . '" name="_ppcp_enable_subscription_product" value="yes" ' . checked( $enable_subscription_product, 'yes', false ) . '/>';
$plan_id = isset( $subscription_plan['id'] ) ?? '';
echo '<input type="checkbox" id="ppcp_enable_subscription_product-' . esc_attr( (string) $product->get_id() ) . '" data-subs-plan="' . esc_attr( (string) $plan_id ) . '" name="_ppcp_enable_subscription_product" value="yes" ' . checked( $enable_subscription_product, 'yes', false ) . '/>';
echo sprintf(
// translators: %1$s and %2$s are label open and close tags.
esc_html__( '%1$sConnect Product to PayPal Subscriptions Plan%2$s', 'woocommerce-paypal-payments' ),
@ -799,9 +733,6 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
echo wc_help_tip( esc_html__( 'Create a subscription product and plan to bill customers at regular intervals. Be aware that certain subscription settings cannot be modified once the PayPal Subscription is linked to this product. Unlink the product to edit disabled fields.', 'woocommerce-paypal-payments' ) );
echo '</p>';
$subscription_product = $product->get_meta( 'ppcp_subscription_product' );
$subscription_plan = $product->get_meta( 'ppcp_subscription_plan' );
$subscription_plan_name = $product->get_meta( '_ppcp_subscription_plan_name' );
if ( $subscription_product || $subscription_plan ) {
$display_unlink_p = 'display:none;';
if ( $enable_subscription_product !== 'yes' ) {
@ -811,7 +742,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
// translators: %1$s and %2$s are button and wrapper html tags.
esc_html__( '%1$sUnlink PayPal Subscription Plan%2$s', 'woocommerce-paypal-payments' ),
'<p class="form-field ppcp-enable-subscription" id="ppcp-enable-subscription-' . esc_attr( (string) $product->get_id() ) . '" style="' . esc_attr( $display_unlink_p ) . '"><label></label><button class="button ppcp-unlink-sub-plan" id="ppcp-unlink-sub-plan-' . esc_attr( (string) $product->get_id() ) . '">',
'</button><span class="spinner is-active" id="spinner-unlink-plan" style="float: none; display:none;"></span></p>'
'</button><span class="spinner is-active" id="spinner-unlink-plan-' . esc_attr( (string) $product->get_id() ) . '" style="float: none; display:none;"></span></p>'
);
echo sprintf(
// translators: %1$s and %2$s is open and closing paragraph tag.
@ -839,7 +770,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
}
} else {
$display_plan_name_p = '';
if ( $enable_subscription_product !== 'yes' && $product->get_name() !== 'AUTO-DRAFT' ) {
if ( $enable_subscription_product !== 'yes' ) {
$display_plan_name_p = 'display:none;';
}
echo sprintf(

View file

@ -55,7 +55,7 @@ class SubscriptionStatus {
* @return void
*/
public function update_status( string $subscription_status, string $subscription_id ): void {
if ( $subscription_status === 'pending-cancel' || $subscription_status === 'cancelled' ) {
if ( $subscription_status === 'cancelled' ) {
try {
$current_subscription = $this->subscriptions_endpoint->subscription( $subscription_id );
if ( $current_subscription->status === 'CANCELLED' ) {
@ -81,7 +81,7 @@ class SubscriptionStatus {
}
}
if ( $subscription_status === 'on-hold' ) {
if ( $subscription_status === 'on-hold' || $subscription_status === 'pending-cancel' ) {
try {
$this->logger->info(
sprintf(

View file

@ -16,11 +16,19 @@ use WooCommerce\PayPalCommerce\SavePaymentMethods\Helper\SavePaymentMethodsAppli
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
// @deprecated - use `save-payment-methods.eligibility.check` instead.
'save-payment-methods.eligible' => static function ( ContainerInterface $container ): bool {
$eligibility_check = $container->get( 'save-payment-methods.eligibility.check' );
return $eligibility_check();
},
'save-payment-methods.eligibility.check' => static function ( ContainerInterface $container ): callable {
$save_payment_methods_applies = $container->get( 'save-payment-methods.helpers.save-payment-methods-applies' );
assert( $save_payment_methods_applies instanceof SavePaymentMethodsApplies );
return $save_payment_methods_applies->for_country();
return static function () use ( $save_payment_methods_applies ) : bool {
return $save_payment_methods_applies->for_country() && $save_payment_methods_applies->for_merchant();
};
},
'save-payment-methods.helpers.save-payment-methods-applies' => static function ( ContainerInterface $container ) : SavePaymentMethodsApplies {
return new SavePaymentMethodsApplies(

View file

@ -48,7 +48,19 @@ class SavePaymentMethodsApplies {
* @return bool
*/
public function for_country(): bool {
return in_array( $this->country, $this->allowed_countries, true );
}
/**
* Indicates, whether the current merchant is eligible for Save Payment Methods. Always true,
* but the filter allows other modules to disable Save Payment Methods site-wide.
*
* @return bool
*/
public function for_merchant() : bool {
return apply_filters(
'woocommerce_paypal_payments_is_eligible_for_save_payment_methods',
true
);
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Controlls the cancel mechanism to step out of the PayPal order session.
* Controls the cancel mechanism to step out of the PayPal order session.
*
* @package WooCommerce\PayPalCommerce\Session\Cancellation
*/

View file

@ -17,4 +17,9 @@
.ppcp-r-payment-method--separator {
margin: 8px 0 24px 0;
}
.components-button.ppcp-r-button-activate-paypal,
.ppcp-r-connection-button.ppcp--mode-live {
--button-background: #000;
}
}

View file

@ -5,10 +5,23 @@ import PaymentMethodsGroup from './PaymentMethodsGroup';
import { PayPalCheckout } from './PaymentOptions';
import { usePaymentConfig } from '../hooks/usePaymentConfig';
/**
* Displays the payment method details, tailored to the defined merchant.
*
* @param {Object} props
* @param {string} props.storeCountry The merchant's store country. 2-character ISO code.
* @param {boolean} props.useAcdc Whether to include advanced card payments. When false, only BCDC items are included.
* @param {boolean} props.isFastlane Whether Fastlane should be included.
* @param {boolean} props.ownBrandOnly Whether to show only PayPal's own payment methods.
* @param {boolean} props.onlyOptional Whether to only return the "right column", which includes the optional opt-in payment methods. When true, the "core" payment methods are not included.
* @return {JSX.Element} The payment options component.
* @class
*/
const PaymentFlow = ( {
useAcdc,
isFastlane,
storeCountry,
ownBrandOnly,
onlyOptional = false,
} ) => {
const {
@ -18,8 +31,9 @@ const PaymentFlow = ( {
optionalDescription,
learnMoreConfig,
paypalCheckoutDescription,
} = usePaymentConfig( storeCountry, useAcdc, isFastlane );
} = usePaymentConfig( storeCountry, useAcdc, isFastlane, ownBrandOnly );
// When only opt-in methods are requested, without core-payment details, return early.
if ( onlyOptional ) {
return (
<OptionalMethodsSection

View file

@ -11,7 +11,7 @@ const Fastlane = ( { learnMore = '' } ) => {
<PricingTitleBadge item="fast country currency=storeCurrency=storeCountrylane" />
}
description={ __(
"Speed up guest checkout with Fatslane. Link a customer's email address to their payment details.",
"Speed up guest checkout with Fastlane. Link a customer's email address to their payment details.",
'woocommerce-paypal-payments'
) }
learnMoreLink={ learnMore }

View file

@ -3,7 +3,7 @@ import { __ } from '@wordpress/i18n';
import PricingDescription from './PricingDescription';
import PaymentFlow from './PaymentFlow';
const WelcomeDocs = ( { useAcdc, isFastlane, storeCountry } ) => {
const WelcomeDocs = ( { useAcdc, isFastlane, storeCountry, ownBrandOnly } ) => {
return (
<div className="ppcp-r-welcome-docs">
<h2 className="ppcp-r-welcome-docs__title">
@ -16,6 +16,7 @@ const WelcomeDocs = ( { useAcdc, isFastlane, storeCountry } ) => {
useAcdc={ useAcdc }
isFastlane={ isFastlane }
storeCountry={ storeCountry }
ownBrandOnly={ ownBrandOnly }
/>
<PricingDescription />
</div>

View file

@ -64,7 +64,8 @@ const PaymentStepTitle = () => {
const OptionalMethodDescription = () => {
const { isCasualSeller } = OnboardingHooks.useBusiness();
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
const { storeCountry, storeCurrency, ownBrandOnly } =
CommonHooks.useWooSettings();
const { canUseCardPayments } = OnboardingHooks.useFlags();
return (
@ -73,6 +74,7 @@ const OptionalMethodDescription = () => {
useAcdc={ ! isCasualSeller && canUseCardPayments }
isFastlane={ true }
isPayLater={ true }
ownBrandOnly={ ownBrandOnly }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>

View file

@ -12,13 +12,14 @@ import AdvancedOptionsForm from '../Components/AdvancedOptionsForm';
import { usePaymentConfig } from '../hooks/usePaymentConfig';
const StepWelcome = ( { setStep, currentStep } ) => {
const { storeCountry } = CommonHooks.useWooSettings();
const { storeCountry, ownBrandOnly } = CommonHooks.useWooSettings();
const { canUseCardPayments, canUseFastlane } = OnboardingHooks.useFlags();
const { acdcIcons, bcdcIcons } = usePaymentConfig(
const { icons } = usePaymentConfig(
storeCountry,
canUseCardPayments,
canUseFastlane
canUseFastlane,
ownBrandOnly
);
const onboardingHeaderDescription = canUseCardPayments
@ -42,9 +43,7 @@ const StepWelcome = ( { setStep, currentStep } ) => {
/>
<div className="ppcp-r-inner-container">
<WelcomeFeatures />
<PaymentMethodIcons
icons={ canUseCardPayments ? acdcIcons : bcdcIcons }
/>
<PaymentMethodIcons icons={ icons } />
<p className="ppcp-r-button__description">
{ __(
'Click the button below to be guided through connecting your existing PayPal account or creating a new one. You will be able to choose the payment options that are right for your store.',
@ -69,6 +68,7 @@ const StepWelcome = ( { setStep, currentStep } ) => {
useAcdc={ canUseCardPayments }
isFastlane={ canUseFastlane }
storeCountry={ storeCountry }
ownBrandOnly={ ownBrandOnly }
/>
<Separator text={ __( 'or', 'woocommerce-paypal-payments' ) } />
<Accordion

View file

@ -15,56 +15,57 @@ import {
CreditDebitCards,
} from '../Components/PaymentOptions';
// List of all payment icons and which requirements they have.
const PAYMENT_ICONS = [
{ name: 'paypal', always: true },
{ name: 'venmo', isOwnBrand: true, onlyAcdc: false, countries: [ 'US' ] },
{ name: 'visa', isOwnBrand: false, onlyAcdc: false },
{ name: 'mastercard', isOwnBrand: false, onlyAcdc: false },
{ name: 'amex', isOwnBrand: false, onlyAcdc: false },
{ name: 'discover', isOwnBrand: false, onlyAcdc: false },
{ name: 'apple-pay', isOwnBrand: false, onlyAcdc: true },
{ name: 'google-pay', isOwnBrand: false, onlyAcdc: true },
{ name: 'blik', isOwnBrand: true, onlyAcdc: true },
{ name: 'ideal', isOwnBrand: true, onlyAcdc: true },
{ name: 'bancontact', isOwnBrand: true, onlyAcdc: true },
];
// Default configuration, used for all countries, unless they override individual attributes below.
const defaultConfig = {
// Included: Items in the left column.
const DEFAULT_CONFIG = {
includedMethods: [
{ name: 'PayWithPayPal', Component: PayWithPayPal },
{ name: 'PayLater', Component: PayLater },
],
// Basic: Items on right side for BCDC-flow.
basicMethods: [ { name: 'CreditDebitCards', Component: CreditDebitCards } ],
// Extended: Items on right side for ACDC-flow.
extendedMethods: [
{ name: 'CardFields', Component: CardFields },
{ name: 'DigitalWallets', Component: DigitalWallets },
{ name: 'APMs', Component: AlternativePaymentMethods },
],
// Title, Description: Header above the right column items.
optionalTitle: __(
'Optional payment methods',
'woocommerce-paypal-payments'
),
optionalDescription: __(
'with additional application',
'woocommerce-paypal-payments'
),
// PayPal Checkout description.
paypalCheckoutDescription: __(
'Our all-in-one checkout solution lets you offer PayPal, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
),
// Icon groups.
bcdcIcons: [ 'paypal', 'visa', 'mastercard', 'amex', 'discover' ],
acdcIcons: [
'paypal',
'visa',
'mastercard',
'amex',
'discover',
'apple-pay',
'google-pay',
'ideal',
'bancontact',
{
name: 'CreditDebitCards',
Component: CreditDebitCards,
isOwnBrand: false,
isAcdc: false,
},
{
name: 'CardFields',
Component: CardFields,
isOwnBrand: false,
isAcdc: true,
},
{
name: 'DigitalWallets',
Component: DigitalWallets,
isOwnBrand: false,
isAcdc: true,
},
{
name: 'APMs',
Component: AlternativePaymentMethods,
isOwnBrand: true,
isAcdc: true,
},
],
};
const countrySpecificConfigs = {
// Country-specific configurations.
const COUNTRY_CONFIGS = {
US: {
includedMethods: [
{ name: 'PayWithPayPal', Component: PayWithPayPal },
@ -72,43 +73,38 @@ const countrySpecificConfigs = {
{ name: 'Venmo', Component: Venmo },
{ name: 'Crypto', Component: Crypto },
],
basicMethods: [
{ name: 'CreditDebitCards', Component: CreditDebitCards },
],
extendedMethods: [
{ name: 'CardFields', Component: CardFields },
{ name: 'DigitalWallets', Component: DigitalWallets },
{ name: 'APMs', Component: AlternativePaymentMethods },
{ name: 'Fastlane', Component: Fastlane },
],
paypalCheckoutDescription: __(
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
),
optionalTitle: __( 'Expanded Checkout', 'woocommerce-paypal-payments' ),
optionalDescription: __(
'Accept debit/credit cards, PayPal, Apple Pay, Google Pay, and more. Note: Additional application required for more methods',
'woocommerce-paypal-payments'
),
bcdcIcons: [
'paypal',
'venmo',
'visa',
'mastercard',
'amex',
'discover',
],
acdcIcons: [
'paypal',
'venmo',
'visa',
'mastercard',
'amex',
'discover',
'apple-pay',
'google-pay',
'ideal',
'bancontact',
{
name: 'CreditDebitCards',
Component: CreditDebitCards,
isOwnBrand: false,
isAcdc: false,
},
{
name: 'CardFields',
Component: CardFields,
isOwnBrand: false,
isAcdc: true,
},
{
name: 'DigitalWallets',
Component: DigitalWallets,
isOwnBrand: false,
isAcdc: true,
},
{
name: 'APMs',
Component: AlternativePaymentMethods,
isOwnBrand: true,
isAcdc: true,
},
{
name: 'Fastlane',
Component: Fastlane,
isOwnBrand: false,
isAcdc: true,
isFastlane: true,
},
],
},
GB: {
@ -119,40 +115,202 @@ const countrySpecificConfigs = {
},
MX: {
extendedMethods: [
{ name: 'CardFields', Component: CardFields },
{ name: 'APMs', Component: AlternativePaymentMethods },
{
name: 'CardFields',
Component: CardFields,
isOwnBrand: false,
isAcdc: true,
},
{
name: 'APMs',
Component: AlternativePaymentMethods,
isOwnBrand: true,
isAcdc: true,
},
],
},
};
/**
* Gets all UI text elements based on country and branding options.
*
* @param {string} country - The country code
* @param {boolean} onlyBranded - Whether to show only branded payment methods
* @return {Object} All UI text elements
*/
const getUIText = ( country, onlyBranded ) => {
const TITLES = {
EXPANDED: __( 'Expanded Checkout', 'woocommerce-paypal-payments' ),
OPTIONAL: __(
'Optional payment methods',
'woocommerce-paypal-payments'
),
};
const OPTIONAL_DESCRIPTIONS = {
LOCAL_METHODS: __(
'Accept local payment methods. Note: Additional application required for some methods',
'woocommerce-paypal-payments'
),
WITH_APPLICATION: __(
'with additional application',
'woocommerce-paypal-payments'
),
US_EXPANDED: __(
'Accept debit/credit cards, PayPal, Apple Pay, Google Pay, and more. Note: Additional application required for some methods',
'woocommerce-paypal-payments'
),
};
const CORE_DESCRIPTIONS = {
DEFAULT_CHECKOUT: __(
'Our all-in-one checkout solution lets you offer PayPal, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
),
US_CHECKOUT: __(
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
),
};
// Base text configuration for all countries.
const texts = {
paypalCheckoutDescription: CORE_DESCRIPTIONS.DEFAULT_CHECKOUT,
optionalTitle: TITLES.OPTIONAL,
optionalDescription: OPTIONAL_DESCRIPTIONS.WITH_APPLICATION,
};
// Country-specific overrides.
if ( country === 'US' ) {
texts.paypalCheckoutDescription = CORE_DESCRIPTIONS.US_CHECKOUT;
texts.optionalTitle = TITLES.EXPANDED;
texts.optionalDescription = OPTIONAL_DESCRIPTIONS.US_EXPANDED;
}
// Branded-only mode overrides.
if ( onlyBranded ) {
texts.optionalTitle = TITLES.EXPANDED;
texts.optionalDescription = OPTIONAL_DESCRIPTIONS.LOCAL_METHODS;
}
return texts;
};
/**
* Filters payment icons based on country and configuration.
*
* @param {string} country - The country code
* @param {boolean} includeAcdc - Whether to include advanced card payment methods
* @param {boolean} onlyBranded - Whether to show only branded payment methods
* @return {string[]} List of icon names
*/
const getRelevantIcons = ( country, includeAcdc, onlyBranded ) =>
PAYMENT_ICONS.filter(
( { always, isOwnBrand, onlyAcdc, countries = [] } ) => {
if ( always ) {
return true;
}
if ( onlyBranded && ! isOwnBrand ) {
return false;
}
if ( ! includeAcdc && onlyAcdc ) {
return false;
}
return ! countries.length || countries.includes( country );
}
).map( ( icon ) => icon.name );
/**
* Filters payment methods based on provided conditions.
*
* @param {Array} methods - The methods to filter
* @param {Array<Function>} conditions - List of filter conditions
* @return {Array} Filtered methods
*/
const filterMethods = ( methods, conditions ) => {
return methods.filter( ( method ) =>
conditions.every( ( condition ) => condition( method ) )
);
};
export const usePaymentConfig = ( country, useAcdc, isFastlane ) => {
/**
* Custom hook that generates payment configuration based on merchant settings.
*
* @param {string} country - Merchant country code
* @param {boolean} canUseCardPayments - Whether merchant can use card payments
* @param {boolean} hasFastlane - Whether merchant has Fastlane enabled
* @param {boolean} ownBrandOnly - Whether to show only branded payment methods
* @return {Object} Complete payment configuration
*/
export const usePaymentConfig = (
country,
canUseCardPayments,
hasFastlane,
ownBrandOnly
) => {
return useMemo( () => {
const countryConfig = countrySpecificConfigs[ country ] || {};
const config = { ...defaultConfig, ...countryConfig };
const learnMoreConfig = learnMoreLinks[ country ] || {};
// eslint-disable-next-line no-console
console.log( '[Payment Config]', {
country,
canUseCardPayments,
hasFastlane,
ownBrandOnly,
} );
// Determine the "right side" items: Either BCDC or ACDC items.
const optionalMethods = useAcdc
? config.extendedMethods
: config.basicMethods;
// Merge country-specific config with default.
const countryConfig = COUNTRY_CONFIGS[ country ] || {};
const config = { ...DEFAULT_CONFIG, ...countryConfig };
// Remove conditional items from the right side list.
const availableOptionalMethods = filterMethods( optionalMethods, [
( method ) => method.name !== 'Fastlane' || isFastlane,
] );
// Get "learn more" links for the country
let learnMoreConfig = learnMoreLinks[ country ] || {};
// If ownBrandOnly is true, move the "OptionalMethods" link to the "APMs" component.
if ( ownBrandOnly && learnMoreConfig.OptionalMethods ) {
const { OptionalMethods, ...rest } = learnMoreConfig;
learnMoreConfig = { ...rest, APMs: OptionalMethods };
}
// Filter out conditional methods.
const availableOptionalMethods = filterMethods(
config.extendedMethods,
[
// Either include Acdc or non-Acdc methods.
( method ) => method.isAcdc === canUseCardPayments,
// Only include own-brand methods when ownBrandOnly is true.
( method ) => ! ownBrandOnly || method.isOwnBrand === true,
// Only include Fastlane when hasFastlane is true.
( method ) => method.name !== 'Fastlane' || hasFastlane,
]
);
// Get all UI text elements.
const uiText = getUIText( country, ownBrandOnly );
// Get icons appropriate for this configuration.
const icons = getRelevantIcons(
country,
canUseCardPayments,
ownBrandOnly
);
// Return the complete configuration.
return {
...config,
// Payment methods configuration.
includedMethods: config.includedMethods,
basicMethods: config.basicMethods,
optionalMethods: availableOptionalMethods,
// UI text configuration.
paypalCheckoutDescription: uiText.paypalCheckoutDescription,
optionalTitle: uiText.optionalTitle,
optionalDescription: uiText.optionalDescription,
// Additional configuration.
learnMoreConfig,
acdcIcons: config.acdcIcons,
bcdcIcons: config.bcdcIcons,
icons,
};
}, [ country, useAcdc, isFastlane ] );
}, [ country, canUseCardPayments, hasFastlane, ownBrandOnly ] );
};

View file

@ -16,9 +16,9 @@ const NOTIFICATION_ANIMATION_DURATION = 300;
const SettingsNavigation = ( {
canSave = true,
tabs,
activePanel,
setActivePanel,
tabs = [],
activePanel = '',
setActivePanel = () => {},
} ) => {
const { persistAll } = useStoreManager();

View file

@ -32,7 +32,10 @@ const FeatureItem = ( {
);
const handleClick = async ( feature ) => {
if ( feature.action?.type === 'tab' ) {
const highlight = Boolean( feature.action?.highlight );
const highlight =
feature.action?.highlight === undefined
? true
: Boolean( feature.action.highlight );
const tabId = TAB_IDS[ feature.action.tab.toUpperCase() ];
await selectTab( tabId, feature.action.section, highlight );
}

View file

@ -6,8 +6,13 @@ import useSettingDependencyState from '../../../../../hooks/useSettingDependency
import PaymentDependencyMessage from './PaymentDependencyMessage';
import SettingDependencyMessage from './SettingDependencyMessage';
import SpinnerOverlay from '../../../../ReusableComponents/SpinnerOverlay';
import { PaymentHooks, SettingsHooks } from '../../../../../data';
import {
PaymentHooks,
SettingsHooks,
OnboardingHooks,
} from '../../../../../data';
import { useNavigation } from '../../../../../hooks/useNavigation';
import usePaymentGatewayRefresh from '../../../../../hooks/usePaymentGatewayRefresh';
/**
* Renders a payment method card with dependency handling
@ -36,6 +41,10 @@ const PaymentMethodCard = ( {
const { isReady: isPaymentStoreReady } = PaymentHooks.useStore();
const { isReady: isSettingsStoreReady } = SettingsHooks.useStore();
const { handleHighlightFromUrl } = useNavigation();
const { gatewaysRefreshed } = OnboardingHooks.useGatewayRefresh();
// Re-fetch payment gateway data to hide methods based on exclusion conditions.
usePaymentGatewayRefresh();
const paymentDependencies = usePaymentDependencyState(
methods,
@ -50,7 +59,11 @@ const PaymentMethodCard = ( {
}
}, [ handleHighlightFromUrl, isPaymentStoreReady, isSettingsStoreReady ] );
if ( ! isPaymentStoreReady || ! isSettingsStoreReady ) {
if (
! isPaymentStoreReady ||
! isSettingsStoreReady ||
! gatewaysRefreshed
) {
return <SpinnerOverlay asModal={ true } />;
}

View file

@ -5,7 +5,7 @@ import { ControlToggleButton } from '../../../../../ReusableComponents/Controls'
import { SettingsHooks } from '../../../../../../data';
import { useMerchantInfo } from '../../../../../../data/common/hooks';
const SavePaymentMethods = () => {
const SavePaymentMethods = ( { ownBradOnly } ) => {
const {
savePaypalAndVenmo,
setSavePaypalAndVenmo,
@ -50,18 +50,21 @@ const SavePaymentMethods = () => {
disabled={ ! features.save_paypal_and_venmo.enabled }
/>
<ControlToggleButton
label={ __(
'Save Credit and Debit Cards',
'woocommerce-paypal-payments'
) }
description={ __(
"Securely store your customer's credit card.",
'woocommerce-paypal-payments'
) }
onChange={ setSaveCardDetails }
value={ saveCardDetails }
/>
{ ownBradOnly || (
// Credit card settings are only available in "white label" mode.
<ControlToggleButton
label={ __(
'Save Credit and Debit Cards',
'woocommerce-paypal-payments'
) }
description={ __(
"Securely store your customer's credit card.",
'woocommerce-paypal-payments'
) }
onChange={ setSaveCardDetails }
value={ saveCardDetails }
/>
) }
</SettingsBlock>
);
};

View file

@ -6,7 +6,7 @@ import SavePaymentMethods from './Blocks/SavePaymentMethods';
import InvoicePrefix from './Blocks/InvoicePrefix';
import PayNowExperience from './Blocks/PayNowExperience';
const CommonSettings = () => (
const CommonSettings = ( { ownBradOnly } ) => (
<SettingsCard
icon="icon-settings-common.svg"
title={ __( 'Common settings', 'woocommerce-paypal-payments' ) }
@ -18,7 +18,7 @@ const CommonSettings = () => (
>
<InvoicePrefix />
<OrderIntent />
<SavePaymentMethods />
<SavePaymentMethods ownBradOnly={ ownBradOnly } />
<PayNowExperience />
</SettingsCard>
);

View file

@ -8,10 +8,7 @@ import Troubleshooting from './Blocks/Troubleshooting';
import PaypalSettings from './Blocks/PaypalSettings';
import OtherSettings from './Blocks/OtherSettings';
const ExpertSettings = () => {
const settings = {}; // dummy object
const updateFormValue = () => {}; // dummy function
const ExpertSettings = ( { ownBradOnly } ) => {
return (
<SettingsCard
icon="icon-settings-expert.svg"
@ -22,39 +19,29 @@ const ExpertSettings = () => {
'woocommerce-paypal-payments'
) }
actionProps={ {
callback: updateFormValue,
key: 'payNowExperience',
} }
contentContainer={ false }
>
<ContentWrapper>
{ /*<Content>
<ConnectionDetails
updateFormValue={ updateFormValue }
settings={ settings }
/>
<ConnectionDetails />
</Content>*/ }
<Content>
<Troubleshooting
updateFormValue={ updateFormValue }
settings={ settings }
/>
<Troubleshooting />
</Content>
<Content>
<PaypalSettings
updateFormValue={ updateFormValue }
settings={ settings }
/>
<PaypalSettings />
</Content>
<Content>
<OtherSettings
updateFormValue={ updateFormValue }
settings={ settings }
/>
</Content>
{ ownBradOnly || (
// The "other settings" accordion is only relevant in white-label mode.
<Content>
<OtherSettings />
</Content>
) }
</ContentWrapper>
</SettingsCard>
);

View file

@ -3,12 +3,16 @@ import Features from '../Components/Overview/Features/Features';
import Help from '../Components/Overview/Help/Help';
import { TodosHooks, CommonHooks, FeaturesHooks } from '../../../../data';
import SpinnerOverlay from '../../../ReusableComponents/SpinnerOverlay';
import usePaymentGatewaySync from '../../../../hooks/usePaymentGatewaySync';
const TabOverview = () => {
const { isReady: areTodosReady } = TodosHooks.useTodos();
const { isReady: merchantIsReady } = CommonHooks.useMerchantInfo();
const { isReady: featuresIsReady } = FeaturesHooks.useFeatures();
// Enable payment gateways after onboarding based on relevant flags.
usePaymentGatewaySync();
if ( ! areTodosReady || ! merchantIsReady || ! featuresIsReady ) {
return <SpinnerOverlay asModal={ true } />;
}

View file

@ -1,4 +1,4 @@
import { PayLaterMessagingHooks } from '../../../data';
import { PayLaterMessagingHooks } from '../../../../data';
import { useEffect } from '@wordpress/element';
const TabPayLaterMessaging = () => {

View file

@ -54,6 +54,16 @@ const TabPaymentMethods = () => {
const merchant = CommonHooks.useMerchant();
const { canUseCardPayments } = OnboardingHooks.useFlags();
const showCardPayments =
methods.cardPayment.length > 0 &&
merchant.isBusinessSeller &&
canUseCardPayments;
const showApms =
methods.apm.length > 0 &&
merchant.isBusinessSeller &&
canUseCardPayments;
return (
<div className="ppcp-r-payment-methods">
<PaymentMethodCard
@ -68,7 +78,8 @@ const TabPaymentMethods = () => {
onTriggerModal={ setActiveModal }
methodsMap={ methodsMap }
/>
{ merchant.isBusinessSeller && canUseCardPayments && (
{ showCardPayments && (
<PaymentMethodCard
id="ppcp-card-payments-card"
title={ __(
@ -85,21 +96,24 @@ const TabPaymentMethods = () => {
methodsMap={ methodsMap }
/>
) }
<PaymentMethodCard
id="ppcp-alternative-payments-card"
title={ __(
'Alternative Payment Methods',
'woocommerce-paypal-payments'
) }
description={ __(
'With alternative payment methods, customers across the globe can pay with their bank accounts and other local payment methods.',
'woocommerce-paypal-payments'
) }
icon="icon-checkout-alternative-methods.svg"
methods={ methods.apm }
onTriggerModal={ setActiveModal }
methodsMap={ methodsMap }
/>
{ showApms && (
<PaymentMethodCard
id="ppcp-alternative-payments-card"
title={ __(
'Alternative Payment Methods',
'woocommerce-paypal-payments'
) }
description={ __(
'With alternative payment methods, customers across the globe can pay with their bank accounts and other local payment methods.',
'woocommerce-paypal-payments'
) }
icon="icon-checkout-alternative-methods.svg"
methods={ methods.apm }
onTriggerModal={ setActiveModal }
methodsMap={ methodsMap }
/>
) }
{ activeModal && (
<Modal

View file

@ -2,9 +2,10 @@ import ConnectionStatus from '../Components/Settings/ConnectionStatus';
import CommonSettings from '../Components/Settings/CommonSettings';
import ExpertSettings from '../Components/Settings/ExpertSettings';
import SpinnerOverlay from '../../../ReusableComponents/SpinnerOverlay';
import { SettingsHooks } from '../../../../data';
import { CommonHooks, SettingsHooks } from '../../../../data';
const TabSettings = () => {
const { ownBrandOnly } = CommonHooks.useWooSettings();
const { isReady } = SettingsHooks.useStore();
if ( ! isReady ) {
@ -14,8 +15,8 @@ const TabSettings = () => {
return (
<div className="ppcp-r-settings">
<ConnectionStatus />
<CommonSettings />
<ExpertSettings />
<CommonSettings ownBradOnly={ ownBrandOnly } />
<ExpertSettings ownBradOnly={ ownBrandOnly } />
</div>
);
};

View file

@ -4,7 +4,7 @@ import TabOverview from './TabOverview';
import TabPaymentMethods from './TabPaymentMethods';
import TabSettings from './TabSettings';
import TabStyling from './TabStyling';
import TabPayLaterMessaging from '../../Overview/TabPayLaterMessaging';
import TabPayLaterMessaging from './TabPayLaterMessaging';
/**
* List of all default settings tabs.

View file

@ -212,6 +212,7 @@ export const useMerchant = () => {
clientSecret: merchant.clientSecret ?? '',
isBusinessSeller: 'business' === merchant.sellerType,
isCasualSeller: 'personal' === merchant.sellerType,
isSendOnlyCountry: merchant.isSendOnlyCountry ?? false,
} ),
// the merchant object is stable, so a new memo is only generated when a merchant prop changes.
[ merchant ]

View file

@ -32,6 +32,16 @@ const defaultTransient = Object.freeze( {
wooSettings: Object.freeze( {
storeCountry: '',
storeCurrency: '',
/**
* The "branded-only" experience is determined on server-side, based on the installation path.
*
* When true, the plugin must only display "PayPal's own brand" payment options
* i.e. no card payments or Apple Pay/Google Pay.
*
* @type {boolean}
*/
ownBrandOnly: false,
} ),
features: Object.freeze( {

View file

@ -41,7 +41,26 @@ export const features = ( state ) => {
};
export const wooSettings = ( state ) => {
return getState( state ).wooSettings || EMPTY_OBJ;
const settings = getState( state ).wooSettings || EMPTY_OBJ;
// For development and testing. Remove this eventually!
const simulateBrandedOnly = document.cookie
.split( '; ' )
.find( ( row ) => row.startsWith( 'simulate-branded-only=' ) )
?.split( '=' )[ 1 ];
/**
* The "own-brand-only" experience is determined on server-side, based on the installation path.
*
* When true, the plugin must only display "PayPal's own brand" payment options
* i.e. no card payments or Apple Pay/Google Pay.
*
* @type {boolean}
*/
const ownBrandOnly =
'true' === simulateBrandedOnly || settings.ownBrandOnly;
return { ...settings, ownBrandOnly };
};
export const webhooks = ( state ) => {

View file

@ -168,6 +168,16 @@ export const addDebugTools = ( context, modules ) => {
onboarding.persist();
};
/**
* Sets a cookie to simulate the branded-only experience.
* @param {boolean} value - Whether to simulate branded-only experience.
*/
debugApi.simulateBrandedOnly = ( value ) => {
const expirationDate = new Date( Date.now() + 3600000 ).toUTCString();
document.cookie = `simulate-branded-only=${ value }; expires=${ expirationDate }; path=/`;
window.location.reload();
};
// Expose original debug API.
Object.assign( context, debugApi );
};

View file

@ -8,7 +8,7 @@ export default {
// Transient data
SET_TRANSIENT: 'ppcp/features/SET_TRANSIENT',
// Persistant data
// Persistent data
SET_FEATURES: 'ppcp/features/SET_FEATURES',
HYDRATE: 'ppcp/features/HYDRATE',
};

View file

@ -12,4 +12,8 @@ export default {
SET_PERSISTENT: 'ppcp/onboarding/SET_PERSISTENT',
RESET: 'ppcp/onboarding/RESET',
HYDRATE: 'ppcp/onboarding/HYDRATE',
// Gateway sync flag
SYNC_GATEWAYS: 'ppcp/onboarding/SYNC_GATEWAYS',
REFRESH_GATEWAYS: 'ppcp/onboarding/REFRESH_GATEWAYS',
};

View file

@ -100,3 +100,43 @@ export function refresh() {
select.persistentData();
};
}
/**
* Persistent. Updates the gateway synced status.
*
* @param {boolean} synced The sync status to set
* @return {Action} The action.
*/
export const updateGatewaysSynced = ( synced = true ) =>
setPersistent( 'gatewaysSynced', synced );
/**
* Persistent. Updates the gateway refreshed status.
*
* @param {boolean} refreshed The refreshed status to set
* @return {Action} The action.
*/
export const updateGatewaysRefreshed = ( refreshed = true ) =>
setPersistent( 'gatewaysRefreshed', refreshed );
/**
* Action creator to sync payment gateways.
* This will both update the state and persist it.
*
* @return {Function} The thunk function.
*/
export function syncGateways() {
return async ( { dispatch } ) => {
dispatch( setPersistent( 'gatewaysSynced', true ) );
await dispatch.persist();
return { success: true };
};
}
export function refreshGateways() {
return async ( { dispatch } ) => {
dispatch( setPersistent( 'gatewaysRefreshed', true ) );
await dispatch.persist();
return { success: true };
};
}

View file

@ -12,10 +12,11 @@ import { useSelect, useDispatch } from '@wordpress/data';
import { createHooksForStore } from '../utils';
import { PRODUCT_TYPES } from './configuration';
import { STORE_NAME } from './constants';
import ACTION_TYPES from './action-types';
const useHooks = () => {
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
const { persist } = useDispatch( STORE_NAME );
const { persist, dispatch } = useDispatch( STORE_NAME );
// Read-only flags and derived state.
const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] );
@ -36,6 +37,12 @@ const useHooks = () => {
'areOptionalPaymentMethodsEnabled'
);
const [ products, setProducts ] = usePersistent( 'products' );
// Add the setter for gatewaysSynced
const [ gatewaysSynced, setGatewaysSynced ] =
usePersistent( 'gatewaysSynced' );
const [ gatewaysRefreshed, setGatewaysRefreshed ] =
usePersistent( 'gatewaysRefreshed' );
const savePersistent = async ( setter, value ) => {
setter( value );
@ -76,6 +83,26 @@ const useHooks = () => {
);
return savePersistent( setProducts, validProducts );
},
gatewaysSynced,
setGatewaysSynced: ( value ) => {
return savePersistent( setGatewaysSynced, value );
},
syncGateways: async () => {
await savePersistent( setGatewaysSynced, true );
dispatch( {
type: ACTION_TYPES.SYNC_GATEWAYS,
} );
},
gatewaysRefreshed,
setGatewaysRefreshed: ( value ) => {
return savePersistent( setGatewaysRefreshed, value );
},
refreshGateways: async () => {
await savePersistent( setGatewaysRefreshed, true );
dispatch( {
type: ACTION_TYPES.REFRESH_GATEWAYS,
} );
},
};
};
@ -145,3 +172,13 @@ export const useFlags = () => {
const { flags } = useHooks();
return flags;
};
export const useGatewaySync = () => {
const { gatewaysSynced, syncGateways } = useHooks();
return { gatewaysSynced, syncGateways };
};
export const useGatewayRefresh = () => {
const { gatewaysRefreshed, refreshGateways } = useHooks();
return { gatewaysRefreshed, refreshGateways };
};

View file

@ -35,6 +35,8 @@ const defaultPersistent = Object.freeze( {
isCasualSeller: null, // null value will uncheck both options in the UI.
areOptionalPaymentMethodsEnabled: null,
products: [],
gatewaysSynced: false,
gatewaysRefreshed: false,
} );
// Reducer logic.
@ -77,6 +79,14 @@ const onboardingReducer = createReducer( defaultTransient, defaultPersistent, {
return newState;
},
[ ACTION_TYPES.SYNC_GATEWAYS ]: ( state ) => {
return changePersistent( state, { gatewaysSynced: true } );
},
[ ACTION_TYPES.REFRESH_GATEWAYS ]: ( state ) => {
return changePersistent( state, { gatewaysRefreshed: true } );
},
} );
export default onboardingReducer;

View file

@ -83,34 +83,22 @@ export const usePaymentMethods = () => {
const [ pui ] = usePersistent( 'ppcp-pay-upon-invoice-gateway' );
const [ oxxo ] = usePersistent( 'ppcp-oxxo-gateway' );
const payPalCheckout = [ paypal, venmo, payLater, creditCard ];
const onlineCardPayments = [
advancedCreditCard,
fastlane,
applePay,
googlePay,
];
const alternative = [
bancontact,
blik,
eps,
ideal,
mybank,
p24,
trustly,
multibanco,
pui,
oxxo,
];
const paymentMethods = [
const removeEmpty = ( list ) =>
list.filter( ( item ) => item && item.id?.length );
const payPalCheckout = removeEmpty( [
paypal,
venmo,
payLater,
creditCard,
] );
const onlineCardPayments = removeEmpty( [
advancedCreditCard,
fastlane,
applePay,
googlePay,
] );
const alternative = removeEmpty( [
bancontact,
blik,
eps,
@ -121,6 +109,12 @@ export const usePaymentMethods = () => {
multibanco,
pui,
oxxo,
] );
const paymentMethods = [
...payPalCheckout,
...onlineCardPayments,
...alternative,
];
return {

View file

@ -0,0 +1,121 @@
import { useState, useEffect, useCallback } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
import { STORE_NAME as PAYMENT_STORE_NAME } from '../data/payment';
import { OnboardingHooks } from '../data';
import { STORE_NAME as ONBOARDING_STORE_NAME } from '../data/onboarding';
/**
* Custom hook for refreshing payment gateway data.
*
* @return {Object} Refresh API including the refresh function and state
*/
export const usePaymentGatewayRefresh = () => {
const paymentDispatch = useDispatch( PAYMENT_STORE_NAME );
const onboardingDispatch = useDispatch( ONBOARDING_STORE_NAME );
const { gatewaysRefreshed } = OnboardingHooks.useGatewayRefresh();
const { gatewaysSynced } = OnboardingHooks.useGatewaySync();
const { refreshGateways } = onboardingDispatch;
const { hydrate, refresh, reset } = paymentDispatch;
const [ refreshCompleted, setRefreshCompleted ] = useState( false );
const [ isRefreshing, setIsRefreshing ] = useState( false );
const [ refreshError, setRefreshError ] = useState( null );
// Only refresh if gateways are synced.
const isReadyToRefresh = gatewaysSynced;
/**
* Refreshes payment gateway data
*
* @return {Promise<boolean|Object>} True when completed successfully, or object with error details
*/
const refreshPaymentGateways = useCallback( async () => {
if ( isRefreshing ) {
return {
success: false,
skipped: true,
reason: 'already-refreshing',
};
}
if ( ! isReadyToRefresh ) {
return {
success: false,
skipped: true,
reason: 'not-ready',
};
}
setIsRefreshing( true );
setRefreshError( null );
try {
// Reset payment store if available.
if ( typeof reset === 'function' ) {
await reset();
}
// Fetch payment data.
const response = await apiFetch( {
path: `/wc/v3/wc_paypal/payment`,
method: 'GET',
} );
// Update store with data.
hydrate( response );
// Refresh payment store if available.
if ( typeof refresh === 'function' ) {
await refresh();
}
// Update Redux state to mark gateways as refreshed.
const result = await refreshGateways();
setRefreshCompleted( true );
return { success: true };
} catch ( error ) {
setRefreshError( error );
return { success: false, error };
} finally {
setIsRefreshing( false );
}
}, [
isRefreshing,
isReadyToRefresh,
reset,
hydrate,
refresh,
refreshGateways,
] );
// Auto-trigger refresh when conditions are met.
useEffect( () => {
if (
isReadyToRefresh &&
! gatewaysRefreshed &&
! isRefreshing &&
! refreshCompleted
) {
refreshPaymentGateways().catch( () => {
// Silent catch to prevent unhandled promise rejections.
} );
}
}, [
isReadyToRefresh,
gatewaysRefreshed,
isRefreshing,
refreshCompleted,
refreshPaymentGateways,
] );
return {
refreshPaymentGateways,
refreshCompleted,
isRefreshing,
refreshError,
gatewaysRefreshed: gatewaysRefreshed || refreshCompleted,
};
};
export default usePaymentGatewayRefresh;

View file

@ -0,0 +1,89 @@
import { useState, useEffect, useRef, useCallback } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { OnboardingHooks, CommonHooks } from '../data';
import { STORE_NAME as ONBOARDING_STORE_NAME } from '../data/onboarding';
/**
* Custom hook for handling gateway synchronization
*
* @return {boolean} Whether gateway sync is completed
*/
export const usePaymentGatewaySync = () => {
const { gatewaysSynced } = OnboardingHooks.useGatewaySync();
const onboardingDispatch = useDispatch( ONBOARDING_STORE_NAME );
const { syncGateways } = onboardingDispatch;
const { isReady: onboardingIsReady, completed: onboardingCompleted } =
OnboardingHooks.useSteps();
const { isReady: merchantIsReady } = CommonHooks.useStore();
const [ isSyncing, setIsSyncing ] = useState( false );
const [ syncCompleted, setSyncCompleted ] = useState( false );
const [ syncError, setSyncError ] = useState( null );
// Use a ref to track if we've initiated a sync during this session.
const syncAttemptedRef = useRef( false );
/**
* Handles the gateway synchronization
*
* @return {Promise<Object>} Result of the sync operation
*/
const handleSync = useCallback( async () => {
if ( isSyncing ) {
return { success: false, skipped: true };
}
setIsSyncing( true );
setSyncError( null );
try {
const result = await syncGateways();
if ( result.success ) {
// Add a small delay to ensure UI updates properly.
await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) );
setSyncCompleted( true );
return { success: true };
}
throw new Error( result.message || 'Failed to sync gateways' );
} catch ( error ) {
setSyncError( error );
// After an error, allow retry after 5 seconds.
setTimeout( () => {
syncAttemptedRef.current = false;
}, 5000 );
return { success: false, error };
} finally {
setIsSyncing( false );
}
}, [ isSyncing, syncGateways ] );
// Automatically sync when conditions are met.
useEffect( () => {
// Skip if required conditions aren't met.
if ( ! onboardingIsReady || ! merchantIsReady || gatewaysSynced ) {
return;
}
// Only attempt sync if not already syncing and no previous attempt.
if ( ! isSyncing && ! syncAttemptedRef.current ) {
syncAttemptedRef.current = true;
handleSync();
}
}, [
onboardingIsReady,
merchantIsReady,
onboardingCompleted,
gatewaysSynced,
isSyncing,
handleSync,
] );
return gatewaysSynced;
};
export default usePaymentGatewaySync;

View file

@ -46,6 +46,8 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\StylingRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\TodosRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager;
use WooCommerce\PayPalCommerce\Settings\Service\BrandedExperience\ActivationDetector;
use WooCommerce\PayPalCommerce\Settings\Service\BrandedExperience\PathRepository;
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
use WooCommerce\PayPalCommerce\Settings\Service\FeaturesEligibilityService;
use WooCommerce\PayPalCommerce\Settings\Service\GatewayRedirectService;
@ -69,7 +71,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState;
use WooCommerce\PayPalCommerce\Settings\Service\InternalRestService;
return array(
'settings.url' => static function ( ContainerInterface $container ) : string {
'settings.url' => static function ( ContainerInterface $container ) : string {
/**
* The path cannot be false.
*
@ -80,7 +82,7 @@ return array(
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'settings.data.onboarding' => static function ( ContainerInterface $container ) : 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' );
@ -100,27 +102,27 @@ return array(
$can_use_pay_later->for_country()
);
},
'settings.data.general' => static function ( ContainerInterface $container ) : GeneralSettings {
'settings.data.general' => static function ( ContainerInterface $container ) : GeneralSettings {
return new GeneralSettings(
$container->get( 'api.shop.country' ),
$container->get( 'api.shop.currency.getter' )->get(),
$container->get( 'wcgateway.is-send-only-country' )
);
},
'settings.data.styling' => static function ( ContainerInterface $container ) : StylingSettings {
'settings.data.styling' => static function ( ContainerInterface $container ) : StylingSettings {
return new StylingSettings(
$container->get( 'settings.service.sanitizer' )
);
},
'settings.data.payment' => static function ( ContainerInterface $container ) : PaymentSettings {
'settings.data.payment' => static function ( ContainerInterface $container ) : PaymentSettings {
return new PaymentSettings();
},
'settings.data.settings' => static function ( ContainerInterface $container ) : SettingsModel {
'settings.data.settings' => static function ( ContainerInterface $container ) : SettingsModel {
return new SettingsModel(
$container->get( 'settings.service.sanitizer' )
);
},
'settings.data.paylater-messaging' => static function ( ContainerInterface $container ) : array {
'settings.data.paylater-messaging' => static function ( ContainerInterface $container ) : array {
// TODO: Create an AbstractDataModel wrapper for this configuration!
$config_factors = $container->get( 'paylater-configurator.factory.config' );
@ -144,7 +146,7 @@ return array(
* (onboarding/connected) and connection-aware environment checks.
* This is the preferred solution to check environment and connection state.
*/
'settings.connection-state' => static function ( ContainerInterface $container ) : ConnectionState {
'settings.connection-state' => static function ( ContainerInterface $container ) : ConnectionState {
$data = $container->get( 'settings.data.general' );
assert( $data instanceof GeneralSettings );
@ -158,7 +160,7 @@ return array(
*
* @deprecated Directly use 'settings.connection-state' instead of this.
*/
'settings.environment' => static function ( ContainerInterface $container ) : Environment {
'settings.environment' => static function ( ContainerInterface $container ) : Environment {
$state = $container->get( 'settings.connection-state' );
assert( $state instanceof ConnectionState );
@ -170,7 +172,7 @@ return array(
*
* @deprecated Use 'settings.connection-state' instead.
*/
'settings.flag.is-connected' => static function ( ContainerInterface $container ) : bool {
'settings.flag.is-connected' => static function ( ContainerInterface $container ) : bool {
$state = $container->get( 'settings.connection-state' );
assert( $state instanceof ConnectionState );
@ -182,71 +184,71 @@ return array(
*
* @deprecated Use 'settings.connection-state' instead.
*/
'settings.flag.is-sandbox' => static function ( ContainerInterface $container ) : bool {
'settings.flag.is-sandbox' => static function ( ContainerInterface $container ) : bool {
$state = $container->get( 'settings.connection-state' );
assert( $state instanceof ConnectionState );
return $state->is_sandbox();
},
'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.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint {
'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint {
return new CommonRestEndpoint(
$container->get( 'settings.data.general' ),
$container->get( 'api.endpoint.partners' )
);
},
'settings.rest.payment' => static function ( ContainerInterface $container ) : PaymentRestEndpoint {
'settings.rest.payment' => static function ( ContainerInterface $container ) : PaymentRestEndpoint {
return new PaymentRestEndpoint(
$container->get( 'settings.data.payment' ),
$container->get( 'settings.data.definition.methods' ),
$container->get( 'settings.data.definition.method_dependencies' )
);
},
'settings.rest.styling' => static function ( ContainerInterface $container ) : StylingRestEndpoint {
'settings.rest.styling' => static function ( ContainerInterface $container ) : StylingRestEndpoint {
return new StylingRestEndpoint(
$container->get( 'settings.data.styling' ),
$container->get( 'settings.service.sanitizer' )
);
},
'settings.rest.refresh_feature_status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint {
'settings.rest.refresh_feature_status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint {
return new RefreshFeatureStatusEndpoint(
$container->get( 'wcgateway.settings' ),
new Cache( 'ppcp-timeout' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'settings.rest.authentication' => static function ( ContainerInterface $container ) : AuthenticationRestEndpoint {
'settings.rest.authentication' => static function ( ContainerInterface $container ) : AuthenticationRestEndpoint {
return new AuthenticationRestEndpoint(
$container->get( 'settings.service.authentication_manager' ),
$container->get( 'settings.service.data-manager' )
);
},
'settings.rest.login_link' => static function ( ContainerInterface $container ) : LoginLinkRestEndpoint {
'settings.rest.login_link' => static function ( ContainerInterface $container ) : LoginLinkRestEndpoint {
return new LoginLinkRestEndpoint(
$container->get( 'settings.service.connection-url-generator' ),
);
},
'settings.rest.webhooks' => static function ( ContainerInterface $container ) : WebhookSettingsEndpoint {
'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.rest.pay_later_messaging' => static function ( ContainerInterface $container ) : PayLaterMessagingEndpoint {
'settings.rest.pay_later_messaging' => static function ( ContainerInterface $container ) : PayLaterMessagingEndpoint {
return new PayLaterMessagingEndpoint(
$container->get( 'wcgateway.settings' ),
$container->get( 'paylater-configurator.endpoint.save-config' )
);
},
'settings.rest.settings' => static function ( ContainerInterface $container ) : SettingsRestEndpoint {
'settings.rest.settings' => static function ( ContainerInterface $container ) : SettingsRestEndpoint {
return new SettingsRestEndpoint(
$container->get( 'settings.data.settings' )
);
},
'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array {
'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array {
return array(
'AR',
'AU',
@ -296,13 +298,13 @@ return array(
'VN',
);
},
'settings.casual-selling.eligible' => static function ( ContainerInterface $container ) : bool {
'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 );
},
'settings.handler.connection-listener' => static function ( ContainerInterface $container ) : ConnectionListener {
'settings.handler.connection-listener' => static function ( ContainerInterface $container ) : ConnectionListener {
$page_id = $container->has( 'wcgateway.current-ppcp-settings-page-id' ) ? $container->get( 'wcgateway.current-ppcp-settings-page-id' ) : '';
return new ConnectionListener(
@ -313,16 +315,16 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'settings.service.signup-link-cache' => static function ( ContainerInterface $container ) : Cache {
'settings.service.signup-link-cache' => static function ( ContainerInterface $container ) : Cache {
return new Cache( 'ppcp-paypal-signup-link' );
},
'settings.service.onboarding-url-manager' => static function ( ContainerInterface $container ) : OnboardingUrlManager {
'settings.service.onboarding-url-manager' => static function ( ContainerInterface $container ) : OnboardingUrlManager {
return new OnboardingUrlManager(
$container->get( 'settings.service.signup-link-cache' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'settings.service.connection-url-generator' => static function ( ContainerInterface $container ) : ConnectionUrlGenerator {
'settings.service.connection-url-generator' => static function ( ContainerInterface $container ) : ConnectionUrlGenerator {
return new ConnectionUrlGenerator(
$container->get( 'api.env.endpoint.partner-referrals' ),
$container->get( 'api.repository.partner-referrals-data' ),
@ -330,7 +332,7 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'settings.service.authentication_manager' => static function ( ContainerInterface $container ) : AuthenticationManager {
'settings.service.authentication_manager' => static function ( ContainerInterface $container ) : AuthenticationManager {
return new AuthenticationManager(
$container->get( 'settings.data.general' ),
$container->get( 'api.env.paypal-host' ),
@ -341,15 +343,15 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'settings.service.rest-service' => static function ( ContainerInterface $container ) : InternalRestService {
'settings.service.rest-service' => static function ( ContainerInterface $container ) : InternalRestService {
return new InternalRestService(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'settings.service.sanitizer' => static function ( ContainerInterface $container ) : DataSanitizer {
'settings.service.sanitizer' => static function ( ContainerInterface $container ) : DataSanitizer {
return new DataSanitizer();
},
'settings.service.data-manager' => static function ( ContainerInterface $container ) : SettingsDataManager {
'settings.service.data-manager' => static function ( ContainerInterface $container ) : SettingsDataManager {
return new SettingsDataManager(
$container->get( 'settings.data.definition.methods' ),
$container->get( 'settings.data.onboarding' ),
@ -361,7 +363,7 @@ return array(
$container->get( 'settings.data.todos' ),
);
},
'settings.ajax.switch_ui' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint {
'settings.ajax.switch_ui' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint {
return new SwitchSettingsUiEndpoint(
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'button.request-data' ),
@ -369,7 +371,7 @@ return array(
$container->get( 'api.merchant_id' ) !== ''
);
},
'settings.rest.todos' => static function ( ContainerInterface $container ) : TodosRestEndpoint {
'settings.rest.todos' => static function ( ContainerInterface $container ) : TodosRestEndpoint {
return new TodosRestEndpoint(
$container->get( 'settings.data.todos' ),
$container->get( 'settings.data.definition.todos' ),
@ -377,16 +379,16 @@ return array(
$container->get( 'settings.service.todos_sorting' )
);
},
'settings.data.todos' => static function ( ContainerInterface $container ) : TodosModel {
'settings.data.todos' => static function ( ContainerInterface $container ) : TodosModel {
return new TodosModel();
},
'settings.data.definition.todos' => static function ( ContainerInterface $container ) : TodosDefinition {
'settings.data.definition.todos' => static function ( ContainerInterface $container ) : TodosDefinition {
return new TodosDefinition(
$container->get( 'settings.service.todos_eligibilities' ),
$container->get( 'settings.data.general' )
);
},
'settings.data.definition.methods' => static function ( ContainerInterface $container ) : PaymentMethodsDefinition {
'settings.data.definition.methods' => static function ( ContainerInterface $container ) : PaymentMethodsDefinition {
$axo_checkout_config_notice = $container->get( 'axo.checkout-config-notice.raw' );
$axo_incompatible_plugins_notice = $container->get( 'axo.incompatible-plugins-notice.raw' );
@ -400,13 +402,14 @@ return array(
return new PaymentMethodsDefinition(
$container->get( 'settings.data.payment' ),
$container->get( 'settings.data.general' ),
$axo_notices
);
},
'settings.data.definition.method_dependencies' => static function ( ContainerInterface $container ) : PaymentMethodsDependenciesDefinition {
'settings.data.definition.method_dependencies' => static function ( ContainerInterface $container ) : PaymentMethodsDependenciesDefinition {
return new PaymentMethodsDependenciesDefinition( $container->get( 'wcgateway.settings' ) );
},
'settings.service.pay_later_status' => static function ( ContainerInterface $container ) : array {
'settings.service.pay_later_status' => static function ( ContainerInterface $container ) : array {
$pay_later_endpoint = $container->get( 'settings.rest.pay_later_messaging' );
$pay_later_settings = $pay_later_endpoint->get_details()->get_data();
@ -427,7 +430,7 @@ return array(
'is_enabled_for_any_location' => $is_pay_later_messaging_enabled_for_any_location,
);
},
'settings.service.button_locations' => static function ( ContainerInterface $container ) : array {
'settings.service.button_locations' => static function ( ContainerInterface $container ) : array {
$styling_endpoint = $container->get( 'settings.rest.styling' );
$styling_data = $styling_endpoint->get_details()->get_data()['data'];
@ -437,7 +440,7 @@ return array(
'product_enabled' => $styling_data['product']->enabled ?? false,
);
},
'settings.service.gateways_status' => static function ( ContainerInterface $container ) : array {
'settings.service.gateways_status' => static function ( ContainerInterface $container ) : array {
$payment_endpoint = $container->get( 'settings.rest.payment' );
$settings = $payment_endpoint->get_details()->get_data();
@ -448,7 +451,7 @@ return array(
'card-button' => $settings['data']['ppcp-card-button-gateway']['enabled'] ?? false,
);
},
'settings.service.merchant_capabilities' => static function ( ContainerInterface $container ) : array {
'settings.service.merchant_capabilities' => static function ( ContainerInterface $container ) : array {
$features = apply_filters(
'woocommerce_paypal_payments_rest_common_merchant_features',
array()
@ -464,7 +467,7 @@ return array(
);
},
'settings.service.todos_eligibilities' => static function ( ContainerInterface $container ) : TodosEligibilityService {
'settings.service.todos_eligibilities' => static function ( ContainerInterface $container ) : TodosEligibilityService {
$pay_later_service = $container->get( 'settings.service.pay_later_status' );
$pay_later_statuses = $pay_later_service['statuses'];
$is_pay_later_messaging_enabled_for_any_location = $pay_later_service['is_enabled_for_any_location'];
@ -519,13 +522,13 @@ return array(
$container->get( 'googlepay.eligible' ) && $capabilities['google_pay'] && ! $gateways['google_pay'],
);
},
'settings.rest.features' => static function ( ContainerInterface $container ) : FeaturesRestEndpoint {
'settings.rest.features' => static function ( ContainerInterface $container ) : FeaturesRestEndpoint {
return new FeaturesRestEndpoint(
$container->get( 'settings.data.definition.features' ),
$container->get( 'settings.rest.settings' )
);
},
'settings.data.definition.features' => static function ( ContainerInterface $container ) : FeaturesDefinition {
'settings.data.definition.features' => static function ( ContainerInterface $container ) : FeaturesDefinition {
$features = apply_filters(
'woocommerce_paypal_payments_rest_common_merchant_features',
array()
@ -544,16 +547,15 @@ return array(
'google_pay' => $features['google_pay']['enabled'] ?? false,
'acdc' => $features['advanced_credit_and_debit_cards']['enabled'] ?? false,
'save_paypal' => $features['save_paypal_and_venmo']['enabled'] ?? false,
'apm' => $features['alternative_payment_methods']['enabled'] ?? false,
'paylater' => $features['pay_later_messaging']['enabled'] ?? false,
);
$merchant_capabilities = array(
'save_paypal' => $capabilities['save_paypal'], // Save PayPal and Venmo eligibility.
'acdc' => $capabilities['acdc'] && ! $gateways['card-button'], // Advanced credit and debit cards eligibility.
'apm' => $capabilities['apm'], // Alternative payment methods eligibility.
'apm' => $capabilities['acdc'] && ! $gateways['card-button'], // Alternative payment methods eligibility.
'google_pay' => $capabilities['acdc'] && $capabilities['google_pay'], // Google Pay eligibility.
'apple_pay' => $capabilities['acdc'] && $capabilities['apple_pay'], // Apple Pay eligibility.
'pay_later' => $capabilities['paylater'],
'pay_later' => $capabilities['acdc'] && ! $gateways['card-button'], // Pay Later eligibility.
);
return new FeaturesDefinition(
$container->get( 'settings.service.features_eligibilities' ),
@ -562,7 +564,7 @@ return array(
$container->get( 'settings.data.settings' )
);
},
'settings.service.features_eligibilities' => static function( ContainerInterface $container ): FeaturesEligibilityService {
'settings.service.features_eligibilities' => static function( ContainerInterface $container ): FeaturesEligibilityService {
$messages_apply = $container->get( 'button.helper.messages-apply' );
assert( $messages_apply instanceof MessagesApply );
@ -574,19 +576,19 @@ return array(
return new FeaturesEligibilityService(
$container->get( 'save-payment-methods.eligible' ), // Save PayPal and Venmo eligibility.
$container->get( 'card-fields.eligible' ), // Advanced credit and debit cards eligibility.
$container->get( 'card-fields.eligibility.check' ), // Advanced credit and debit cards eligibility.
$apm_eligible, // Alternative payment methods eligibility.
$container->get( 'googlepay.eligible' ), // Google Pay eligibility.
$container->get( 'applepay.eligible' ), // Apple Pay eligibility.
$container->get( 'googlepay.eligibility.check' ), // Google Pay eligibility.
$container->get( 'applepay.eligibility.check' ), // Apple Pay eligibility.
$pay_later_eligible, // Pay Later eligibility.
);
},
'settings.service.todos_sorting' => static function ( ContainerInterface $container ) : TodosSortingAndFilteringService {
'settings.service.todos_sorting' => static function ( ContainerInterface $container ) : TodosSortingAndFilteringService {
return new TodosSortingAndFilteringService(
$container->get( 'settings.data.todos' )
);
},
'settings.service.gateway-redirect' => static function (): GatewayRedirectService {
'settings.service.gateway-redirect' => static function (): GatewayRedirectService {
return new GatewayRedirectService();
},
/**
@ -594,7 +596,7 @@ return array(
*
* @returns string[] The list of all gateway IDs.
*/
'settings.config.all-gateway-ids' => static function (): array {
'settings.config.all-gateway-ids' => static function (): array {
return array(
PayPalGateway::ID,
CardButtonGateway::ID,
@ -614,4 +616,13 @@ return array(
OXXO::ID,
);
},
'settings.service.branded-experience.activation-detector' => static function (): ActivationDetector {
return new ActivationDetector();
},
'settings.service.branded-experience.path-repository' => static function ( ContainerInterface $container ): PathRepository {
return new PathRepository(
$container->get( 'settings.service.branded-experience.activation-detector' ),
$container->get( 'settings.data.general' )
);
},
);

View file

@ -150,11 +150,10 @@ class FeaturesDefinition {
'type' => 'secondary',
'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
'action' => array(
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-credit-card-gateway',
'highlight' => 'ppcp-credit-card-gateway',
'modal' => 'ppcp-credit-card-gateway',
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-credit-card-gateway',
'modal' => 'ppcp-credit-card-gateway',
),
'showWhen' => 'enabled',
'class' => 'small-button',
@ -189,7 +188,7 @@ class FeaturesDefinition {
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-alternative-payments-card',
'highlight' => 'ppcp-alternative-payments-card',
'highlight' => false,
),
'showWhen' => 'enabled',
'class' => 'small-button',
@ -218,11 +217,10 @@ class FeaturesDefinition {
'type' => 'secondary',
'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
'action' => array(
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-googlepay',
'highlight' => 'ppcp-googlepay',
'modal' => 'ppcp-googlepay',
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-googlepay',
'modal' => 'ppcp-googlepay',
),
'showWhen' => 'enabled',
'class' => 'small-button',
@ -257,11 +255,10 @@ class FeaturesDefinition {
'type' => 'secondary',
'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
'action' => array(
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-card-payments-card',
'highlight' => 'ppcp-applepay',
'modal' => 'ppcp-applepay',
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-applepay',
'modal' => 'ppcp-applepay',
),
'showWhen' => 'enabled',
'class' => 'small-button',

View file

@ -21,6 +21,7 @@ use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\MyBankGateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\P24Gateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\TrustlyGateway;
use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings;
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXO;
@ -40,6 +41,14 @@ class PaymentMethodsDefinition {
*/
private PaymentSettings $settings;
/**
* Data model for the general plugin settings, used to access flags like
* "own brand only" to modify the payment method details.
*
* @var GeneralSettings
*/
private GeneralSettings $general_settings;
/**
* Conflict notices for Axo gateway.
*
@ -57,14 +66,17 @@ class PaymentMethodsDefinition {
/**
* Constructor.
*
* @param PaymentSettings $settings Payment methods data model.
* @param PaymentSettings $settings Payment methods data model.
* @param GeneralSettings $general_settings General plugin settings model.
* @param array $axo_conflicts_notices Conflicts notices for Axo.
*/
public function __construct(
PaymentSettings $settings,
GeneralSettings $general_settings,
array $axo_conflicts_notices = array()
) {
$this->settings = $settings;
$this->general_settings = $general_settings;
$this->axo_conflicts_notices = $axo_conflicts_notices;
}
@ -94,6 +106,7 @@ class PaymentMethodsDefinition {
$method['warningMessages'] ?? array(),
);
}
return $result;
}
@ -105,9 +118,11 @@ class PaymentMethodsDefinition {
* @param string $title Admin-side payment method title.
* @param string $description Admin-side info about the payment method.
* @param string $icon Admin-side icon of the payment method.
* @param array|false $fields Optional. Additional fields to display in the edit modal.
* Setting this to false omits all fields.
* @param array $warning_messages Optional. Warning messages to display in the UI.
* @param array|false $fields Optional. Additional fields to display in the
* edit modal. Setting this to false omits all
* fields.
* @param array $warning_messages Optional. Warning messages to display in the
* UI.
* @return array Payment method definition.
*/
private function build_method_definition(
@ -115,7 +130,7 @@ class PaymentMethodsDefinition {
string $title,
string $description,
string $icon,
$fields = array(),
$fields = array(),
array $warning_messages = array()
) : array {
$gateway = $this->wc_gateways[ $gateway_id ] ?? null;
@ -197,13 +212,16 @@ class PaymentMethodsDefinition {
'icon' => 'payment-method-paypal',
'fields' => false,
),
array(
);
if ( ! $this->general_settings->own_brand_only() ) {
$group[] = array(
'id' => CardButtonGateway::ID,
'title' => __( 'Credit and debit card payments', 'woocommerce-paypal-payments' ),
'description' => __( "Accept all major credit and debit cards - even if your customer doesn't have a PayPal account . ", 'woocommerce-paypal-payments' ),
'icon' => 'payment-method-cards',
),
);
);
}
return apply_filters( 'woocommerce_paypal_payments_gateway_group_paypal', $group );
}
@ -214,8 +232,10 @@ class PaymentMethodsDefinition {
* @return array
*/
public function group_card_methods() : array {
$group = array(
array(
$group = array();
if ( ! $this->general_settings->own_brand_only() ) {
$group[] = array(
'id' => CreditCardGateway::ID,
'title' => __( 'Advanced Credit and Debit Card Payments', 'woocommerce-paypal-payments' ),
'description' => __( "Present custom credit and debit card fields to your payers so they can pay with credit and debit cards using your site's branding.", 'woocommerce-paypal-payments' ),
@ -254,8 +274,8 @@ class PaymentMethodsDefinition {
),
),
),
),
array(
);
$group[] = array(
'id' => AxoGateway::ID,
'title' => __( 'Fastlane by PayPal', 'woocommerce-paypal-payments' ),
'description' => __( "Tap into the scale and trust of PayPal's customer network to recognize shoppers and make guest checkout more seamless than ever.", 'woocommerce-paypal-payments' ),
@ -279,20 +299,20 @@ class PaymentMethodsDefinition {
),
),
'warningMessages' => $this->axo_conflicts_notices,
),
array(
);
$group[] = array(
'id' => ApplePayGateway::ID,
'title' => __( 'Apple Pay', 'woocommerce-paypal-payments' ),
'description' => __( 'Allow customers to pay via their Apple Pay digital wallet.', 'woocommerce-paypal-payments' ),
'icon' => 'payment-method-apple-pay',
),
array(
);
$group[] = array(
'id' => GooglePayGateway::ID,
'title' => __( 'Google Pay', 'woocommerce-paypal-payments' ),
'description' => __( 'Allow customers to pay via their Google Pay digital wallet.', 'woocommerce-paypal-payments' ),
'icon' => 'payment-method-google-pay',
),
);
);
}
return apply_filters( 'woocommerce_paypal_payments_gateway_group_cards', $group );
}

View file

@ -9,9 +9,11 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Data;
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\DefaultPaymentGateways;
use RuntimeException;
use WooCommerce\PayPalCommerce\Settings\DTO\MerchantConnectionDTO;
use WooCommerce\PayPalCommerce\Settings\Enum\SellerTypeEnum;
use WooCommerce\PayPalCommerce\Settings\Enum\InstallationPathEnum;
/**
* Class GeneralSettings
@ -78,6 +80,9 @@ class GeneralSettings extends AbstractDataModel {
'client_id' => '',
'client_secret' => '',
'seller_type' => 'unknown',
// Branded experience installation path.
'installation_path' => '',
);
}
@ -125,7 +130,11 @@ class GeneralSettings extends AbstractDataModel {
* @return array
*/
public function get_woo_settings() : array {
return $this->woo_settings;
$settings = $this->woo_settings;
$settings['own_brand_only'] = $this->own_brand_only();
return $settings;
}
/**
@ -257,4 +266,68 @@ class GeneralSettings extends AbstractDataModel {
return $this->data['merchant_country'];
}
/**
* Sets the installation path. This function will only set the installation
* path a single time and ignore subsequent calls.
*
* Short: The installation path cannot be updated once it's defined.
*
* @param string $installation_path The installation path.
*
* @return void
*/
public function set_installation_path( string $installation_path ) : void {
// The installation path can be set only once.
if ( InstallationPathEnum::is_valid( $this->data['installation_path'] ?? '' ) ) {
return;
}
// Ignore invalid installation paths.
if ( ! $installation_path || ! InstallationPathEnum::is_valid( $installation_path ) ) {
return;
}
$this->data['installation_path'] = $installation_path;
}
/**
* Retrieves the installation path. Used for the branded experience.
*
* @return string
*/
public function get_installation_path() : string {
return $this->data['installation_path'] ?? InstallationPathEnum::DIRECT;
}
/**
* Whether the plugin is in the branded-experience mode and shows/enables only
* payment methods that are PayPal's own brand.
*
* @return bool
*/
public function own_brand_only() : bool {
/**
* If the current store is not eligible for WooPayments, we have to also show the other payment methods.
*/
if ( ! in_array( $this->woo_settings['country'], DefaultPaymentGateways::get_wcpay_countries(), true ) ) {
return false;
}
// Temporary dev/test mode.
$simulate_cookie = sanitize_key( wp_unslash( $_COOKIE['simulate-branded-only'] ?? '' ) );
if ( $simulate_cookie === 'true' ) {
return true;
} elseif ( $simulate_cookie === 'false' ) {
return false;
}
$brand_only_paths = array(
InstallationPathEnum::CORE_PROFILER,
InstallationPathEnum::PAYMENT_SETTINGS,
);
return in_array( $this->get_installation_path(), $brand_only_paths, true );
}
}

View file

@ -74,7 +74,7 @@ class OnboardingProfile extends AbstractDataModel {
*
* @return array
*/
protected function get_defaults() : array {
protected function get_defaults(): array {
return array(
'completed' => false,
'step' => 0,
@ -82,6 +82,8 @@ class OnboardingProfile extends AbstractDataModel {
'accept_card_payments' => null,
'products' => array(),
'setup_done' => false,
'gateways_synced' => false,
'gateways_refreshed' => false,
);
}
@ -203,4 +205,45 @@ class OnboardingProfile extends AbstractDataModel {
public function set_setup_done( bool $done ) : void {
$this->data['setup_done'] = $done;
}
/**
* Get whether gateways have been synced.
*
* @return bool
*/
public function is_gateways_synced(): bool {
return $this->data['gateways_synced'] ?? false;
}
/**
* Set whether gateways have been synced.
*
* @param bool $synced Whether gateways have been synced.
*/
public function set_gateways_synced( bool $synced ): void {
$this->data['gateways_synced'] = $synced;
// If enabling the flag, trigger the action.
if ( $synced ) {
do_action( 'woocommerce_paypal_payments_sync_gateways' );
}
}
/**
* Get whether gateways have been refreshed.
*
* @return bool
*/
public function is_gateways_refreshed(): bool {
return $this->data['gateways_refreshed'] ?? false;
}
/**
* Set whether gateways have been refreshed.
*
* @param bool $refreshed Whether gateways have been refreshed.
*/
public function set_gateways_refreshed( bool $refreshed ): void {
$this->data['gateways_refreshed'] = $refreshed;
}
}

View file

@ -64,6 +64,7 @@ class CommonRestEndpoint extends RestEndpoint {
'js_name' => 'useManualConnection',
'sanitize' => 'to_boolean',
),
// TODO: Is this really a "read-and-write" field? If no, it should not be listed in this map!
'webhooks' => array(
'js_name' => 'webhooks',
),
@ -107,12 +108,15 @@ class CommonRestEndpoint extends RestEndpoint {
* @var array
*/
private array $woo_settings_map = array(
'country' => array(
'country' => array(
'js_name' => 'storeCountry',
),
'currency' => array(
'currency' => array(
'js_name' => 'storeCurrency',
),
'own_brand_only' => array(
'js_name' => 'ownBrandOnly',
),
);
/**

View file

@ -60,6 +60,14 @@ class OnboardingRestEndpoint extends RestEndpoint {
'products' => array(
'js_name' => 'products',
),
'gateways_synced' => array(
'js_name' => 'gatewaysSynced',
'sanitize' => 'to_boolean',
),
'gateways_refreshed' => array(
'js_name' => 'gatewaysRefreshed',
'sanitize' => 'to_boolean',
),
);
/**

View file

@ -0,0 +1,61 @@
<?php
/**
* Defines valid "installation path" values that document _how_ the plugin was
* installed. This path is used for the branded-only experience.
*
* @package WooCommerce\PayPalCommerce\Settings\Enum
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Enum;
/**
* Enum for the "installation path" values.
*/
class InstallationPathEnum {
/**
* The plugin was installed via the WooCommerce onboarding wizard, by
* selecting PayPal in the "Get a boost with our free features" screen.
*/
public const CORE_PROFILER = 'core-profiler';
/**
* The plugin was installed from the WooCommerce "Payment" settings page:
* `/wp-admin/admin.php?page=wc-settings&tab=checkout` - only available in
* the reactified version, i.e. the "new layout".
*/
public const PAYMENT_SETTINGS = 'payment-settings';
/**
* The plugin was installed in a different way, most likely the "Plugins"
* admin page, or FTP upload, WP CLI or similar.
*
* Also applies to merchants that installed the plugin before we added the
* installation path detection.
*/
public const DIRECT = 'direct';
/**
* Get all valid seller types.
*
* @return array List of all valid options.
*/
public static function get_valid_values() : array {
return array(
self::CORE_PROFILER,
self::PAYMENT_SETTINGS,
self::DIRECT,
);
}
/**
* Check if a given type is valid.
*
* @param string $type The value to validate.
* @return bool True, if the value is a valid installation path.
*/
public static function is_valid( string $type ) : bool {
return in_array( $type, self::get_valid_values(), true );
}
}

View file

@ -34,7 +34,7 @@ class ProductChoicesEnum {
/**
* Get all valid seller types.
*
* @return array List of all valid seller_types.
* @return array List of all valid products.
*/
public static function get_valid_values() : array {
return array(
@ -48,7 +48,7 @@ class ProductChoicesEnum {
* Check if a given type is valid.
*
* @param string $type The value to validate.
* @return bool True, if the value is a valid seller_type.
* @return bool True, if the value is a valid product.
*/
public static function is_valid( string $type ) : bool {
return in_array( $type, self::get_valid_values(), true );

View file

@ -0,0 +1,42 @@
<?php
/**
* Branded Experience activation detector service.
*
* @package WooCommerce\PayPalCommerce\Settings\Service\BrandedExperience
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Service\BrandedExperience;
use WooCommerce\PayPalCommerce\Settings\Enum\InstallationPathEnum;
/**
* Class that includes detection logic for Branded Experience.
*/
class ActivationDetector {
/**
* The expected slug that identifies the "core-profiler" installation path.
*/
private const ATTACHMENT_CORE_PROFILER = 'payments_settings';
/**
* Detects from which path the plugin was installed.
*
* @return string The installation path.
*/
public function detect_activation_path() : string {
/**
* Get the custom attachment detail which was added by WooCommerce
*
* @see PaymentProviders::attach_extension_suggestion()
*/
$branded_option = get_option( 'woocommerce_paypal_branded' );
if ( self::ATTACHMENT_CORE_PROFILER === $branded_option ) {
return InstallationPathEnum::CORE_PROFILER;
}
return InstallationPathEnum::DIRECT;
}
}

View file

@ -0,0 +1,57 @@
<?php
/**
* Persist the Branded Experience path.
*
* @package WooCommerce\PayPalCommerce\Settings\Service\BrandedExperience
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Service\BrandedExperience;
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
/**
* Class that includes logic for persisting the Branded Experience path.
*/
class PathRepository {
/**
* The Branded Experience activation path detector.
*
* @var ActivationDetector
*/
private ActivationDetector $activation_detector;
/**
* The general settings.
*
* @var GeneralSettings
*/
private GeneralSettings $general_settings;
/**
* PathRepository constructor.
*
* @param ActivationDetector $activation_detector The Branded Experience activation path detector.
* @param GeneralSettings $general_settings The general settings.
*/
public function __construct( ActivationDetector $activation_detector, GeneralSettings $general_settings ) {
$this->activation_detector = $activation_detector;
$this->general_settings = $general_settings;
}
/**
* Persists Branded Experience activation path only once.
*
* @return void
*/
public function persist(): void {
$persisted = $this->general_settings->get_installation_path();
if ( $persisted ) {
return;
}
$this->general_settings->set_installation_path( $this->activation_detector->detect_activation_path() );
$this->general_settings->save();
}
}

View file

@ -3,13 +3,13 @@
* PayPal Commerce eligibility service for WooCommerce.
*
* This file contains the FeaturesEligibilityService class which manages eligibility checks
* for various PayPal Commerce features including saving PayPal and Venmo, advanced credit and debit cards,
* alternative payment methods, Google Pay, Apple Pay, and Pay Later.
* for various PayPal Commerce features including saving PayPal and Venmo, advanced credit and
* debit cards, alternative payment methods, Google Pay, Apple Pay, and Pay Later.
*
* @package WooCommerce\PayPalCommerce\Settings\Service
*/
declare(strict_types=1);
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Service;
@ -22,35 +22,35 @@ class FeaturesEligibilityService {
*
* @var bool
*/
private bool $is_save_paypal_and_venmo_eligible;
private bool $is_save_paypal_eligible;
/**
* Whether advanced credit and debit cards are eligible.
*
* @var bool
* @var callable
*/
private bool $is_advanced_credit_and_debit_cards_eligible;
private $check_acdc_eligible;
/**
* Whether alternative payment methods are eligible.
*
* @var bool
*/
private bool $is_alternative_payment_methods_eligible;
private bool $is_apm_eligible;
/**
* Whether Google Pay is eligible.
*
* @var bool
* @var callable
*/
private bool $is_google_pay_eligible;
private $check_google_pay_eligible;
/**
* Whether Apple Pay is eligible.
*
* @var bool
* @var callable
*/
private bool $is_apple_pay_eligible;
private $check_apple_pay_eligible;
/**
* Whether Pay Later is eligible.
@ -62,27 +62,27 @@ class FeaturesEligibilityService {
/**
* Constructor.
*
* @param bool $is_save_paypal_and_venmo_eligible Whether saving PayPal and Venmo is eligible.
* @param bool $is_advanced_credit_and_debit_cards_eligible Whether advanced credit and debit cards are eligible.
* @param bool $is_alternative_payment_methods_eligible Whether alternative payment methods are eligible.
* @param bool $is_google_pay_eligible Whether Google Pay is eligible.
* @param bool $is_apple_pay_eligible Whether Apple Pay is eligible.
* @param bool $is_pay_later_eligible Whether Pay Later is eligible.
* @param bool $is_save_paypal_eligible If saving PayPal and Venmo is eligible.
* @param callable $check_acdc_eligible If advanced credit and debit cards are eligible.
* @param bool $is_apm_eligible If alternative payment methods are eligible.
* @param callable $check_google_pay_eligible If Google Pay is eligible.
* @param callable $check_apple_pay_eligible If Apple Pay is eligible.
* @param bool $is_pay_later_eligible If Pay Later is eligible.
*/
public function __construct(
bool $is_save_paypal_and_venmo_eligible,
bool $is_advanced_credit_and_debit_cards_eligible,
bool $is_alternative_payment_methods_eligible,
bool $is_google_pay_eligible,
bool $is_apple_pay_eligible,
bool $is_save_paypal_eligible,
callable $check_acdc_eligible,
bool $is_apm_eligible,
callable $check_google_pay_eligible,
callable $check_apple_pay_eligible,
bool $is_pay_later_eligible
) {
$this->is_save_paypal_and_venmo_eligible = $is_save_paypal_and_venmo_eligible;
$this->is_advanced_credit_and_debit_cards_eligible = $is_advanced_credit_and_debit_cards_eligible;
$this->is_alternative_payment_methods_eligible = $is_alternative_payment_methods_eligible;
$this->is_google_pay_eligible = $is_google_pay_eligible;
$this->is_apple_pay_eligible = $is_apple_pay_eligible;
$this->is_pay_later_eligible = $is_pay_later_eligible;
$this->is_save_paypal_eligible = $is_save_paypal_eligible;
$this->check_acdc_eligible = $check_acdc_eligible;
$this->is_apm_eligible = $is_apm_eligible;
$this->check_google_pay_eligible = $check_google_pay_eligible;
$this->check_apple_pay_eligible = $check_apple_pay_eligible;
$this->is_pay_later_eligible = $is_pay_later_eligible;
}
/**
@ -90,13 +90,13 @@ class FeaturesEligibilityService {
*
* @return array<string, callable>
*/
public function get_eligibility_checks(): array {
public function get_eligibility_checks() : array {
return array(
'save_paypal_and_venmo' => fn() => $this->is_save_paypal_and_venmo_eligible,
'advanced_credit_and_debit_cards' => fn() => $this->is_advanced_credit_and_debit_cards_eligible,
'alternative_payment_methods' => fn() => $this->is_alternative_payment_methods_eligible,
'google_pay' => fn() => $this->is_google_pay_eligible,
'apple_pay' => fn() => $this->is_apple_pay_eligible,
'save_paypal_and_venmo' => fn() => $this->is_save_paypal_eligible,
'advanced_credit_and_debit_cards' => $this->check_acdc_eligible,
'alternative_payment_methods' => fn() => $this->is_apm_eligible,
'google_pay' => $this->check_google_pay_eligible,
'apple_pay' => $this->check_apple_pay_eligible,
'pay_later' => fn() => $this->is_pay_later_eligible,
);
}

View file

@ -193,9 +193,6 @@ class SettingsDataManager {
* @return void
*/
protected function apply_configuration( ConfigurationFlagsDTO $flags ) : void {
// Apply defaults for the "Payment Methods" tab.
$this->toggle_payment_gateways( $flags );
// Apply defaults for the "Settings" tab.
$this->apply_payment_settings( $flags );
@ -206,6 +203,23 @@ class SettingsDataManager {
$this->apply_pay_later_messaging( $flags );
}
/**
* Synchronize gateway settings with merchant onboarding choices.
*
* @return void
*/
public function sync_gateway_settings() : void {
$flags = new ConfigurationFlagsDTO();
$profile_data = $this->onboarding_profile->to_array();
$flags->is_business_seller = ! ( $profile_data['is_casual_seller'] ?? false );
$flags->use_card_payments = $profile_data['accept_card_payments'] ?? false;
$flags->use_subscriptions = in_array( 'SUBSCRIPTIONS', $profile_data['products'] ?? array(), true );
$this->toggle_payment_gateways( $flags );
}
/**
* Enables or disables payment gateways depending on the provided
* configuration flags.
@ -227,9 +241,8 @@ class SettingsDataManager {
// Always enable PayPal, Venmo and Pay Later.
$this->payment_methods->toggle_method_state( PayPalGateway::ID, true );
$this->payment_methods->toggle_method_state( 'venmo', true );
$this->payment_methods->toggle_method_state( 'pay-later', true );
if ( $flags->is_business_seller && $flags->use_card_payments ) {
if ( ! $flags->is_business_seller && $flags->use_card_payments ) {
// Use BCDC for casual sellers.
$this->payment_methods->toggle_method_state( CardButtonGateway::ID, true );
}
@ -242,6 +255,9 @@ class SettingsDataManager {
// Apple Pay and Google Pay depend on the ACDC gateway.
$this->payment_methods->toggle_method_state( ApplePayGateway::ID, true );
$this->payment_methods->toggle_method_state( GooglePayGateway::ID, true );
// Enable Pay Later for business sellers.
$this->payment_methods->toggle_method_state( 'pay-later', true );
}
// Enable all APM methods.
@ -250,6 +266,14 @@ class SettingsDataManager {
}
}
/**
* Allow plugins to modify payment gateway states before saving.
*
* @param PaymentSettings $payment_methods The payment methods object.
* @param ConfigurationFlagsDTO $flags Configuration flags that determine which gateways to enable.
*/
do_action( 'woocommerce_paypal_payments_toggle_payment_gateways', $this->payment_methods, $flags );
$this->payment_methods->save();
}

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Settings;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
use WooCommerce\PayPalCommerce\Applepay\Assets\AppleProductStatus;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus;
@ -23,11 +24,11 @@ use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\MyBankGateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\P24Gateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\TrustlyGateway;
use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint;
use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDefinition;
use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile;
use WooCommerce\PayPalCommerce\Settings\Data\TodosModel;
use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
use WooCommerce\PayPalCommerce\Settings\Service\BrandedExperience\PathRepository;
use WooCommerce\PayPalCommerce\Settings\Service\GatewayRedirectService;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
@ -144,12 +145,37 @@ class SettingsModule implements ServiceModule, ExecutableModule {
return true;
}
/**
* This hook is fired when the plugin is updated.
*/
add_action(
'woocommerce_paypal_payments_gateway_migrate_on_update',
static fn() => ! get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI )
&& update_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI, 'yes' )
);
/**
* This hook is fired when the plugin is installed or updated.
*/
add_action(
'woocommerce_paypal_payments_gateway_migrate',
function () use ( $container ) {
$path_repository = $container->get( 'settings.service.branded-experience.path-repository' );
assert( $path_repository instanceof PathRepository );
$partner_attribution = $container->get( 'api.helper.partner-attribution' );
assert( $partner_attribution instanceof PartnerAttribution );
$general_settings = $container->get( 'settings.data.general' );
assert( $general_settings instanceof GeneralSettings );
$path_repository->persist();
$partner_attribution->initialize_bn_code( $general_settings->get_installation_path() );
}
);
$this->apply_branded_only_limitations( $container );
add_action(
'admin_enqueue_scripts',
/**
@ -220,6 +246,9 @@ class SettingsModule implements ServiceModule, ExecutableModule {
);
if ( $is_pay_later_configurator_available ) {
$partner_attribution = $container->get( 'api.helper.partner-attribution' );
assert( $partner_attribution instanceof PartnerAttribution );
wp_enqueue_script(
'ppcp-paylater-configurator-lib',
'https://www.paypalobjects.com/merchant-library/merchant-configurator.js',
@ -235,7 +264,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
'config' => array(),
'merchantClientId' => $settings->get( 'client_id' ),
'partnerClientId' => $container->get( 'api.partner_merchant_id' ),
'bnCode' => PPCP_PAYPAL_BN_CODE,
'bnCode' => $partner_attribution->get_bn_code(),
);
}
@ -244,6 +273,9 @@ class SettingsModule implements ServiceModule, ExecutableModule {
'ppcpSettings',
$script_data
);
// Dequeue the PayPal Subscription script.
wp_dequeue_script( 'ppcp-paypal-subscription' );
}
);
@ -303,6 +335,8 @@ class SettingsModule implements ServiceModule, ExecutableModule {
$onboarding_profile->set_completed( false );
$onboarding_profile->set_step( 0 );
$onboarding_profile->set_gateways_synced( false );
$onboarding_profile->set_gateways_refreshed( false );
$onboarding_profile->save();
// Reset dismissed and completed on click todos.
@ -367,6 +401,20 @@ class SettingsModule implements ServiceModule, ExecutableModule {
$card_fields_eligible = $container->get( 'card-fields.eligible' );
if ( $dcc_product_status->is_active() && $card_fields_eligible ) {
unset( $payment_methods[ CardButtonGateway::ID ] );
} else {
// For non-ACDC regions unset ACDC, local APMs and set BCDC.
unset( $payment_methods[ CreditCardGateway::ID ] );
unset( $payment_methods['pay-later'] );
unset( $payment_methods[ BancontactGateway::ID ] );
unset( $payment_methods[ BlikGateway::ID ] );
unset( $payment_methods[ EPSGateway::ID ] );
unset( $payment_methods[ IDealGateway::ID ] );
unset( $payment_methods[ MyBankGateway::ID ] );
unset( $payment_methods[ P24Gateway::ID ] );
unset( $payment_methods[ TrustlyGateway::ID ] );
unset( $payment_methods[ MultibancoGateway::ID ] );
unset( $payment_methods[ PayUponInvoiceGateway::ID ] );
unset( $payment_methods[ OXXO::ID ] );
}
// Unset Venmo when store location is not United States.
@ -399,23 +447,6 @@ class SettingsModule implements ServiceModule, ExecutableModule {
unset( $payment_methods[ PayUponInvoiceGateway::ID ] );
}
// For non-ACDC regions unset ACDC, local APMs and set BCDC.
if ( ! $dcc_applies ) {
unset( $payment_methods[ CreditCardGateway::ID ] );
unset( $payment_methods[ BancontactGateway::ID ] );
unset( $payment_methods[ BlikGateway::ID ] );
unset( $payment_methods[ EPSGateway::ID ] );
unset( $payment_methods[ IDealGateway::ID ] );
unset( $payment_methods[ MyBankGateway::ID ] );
unset( $payment_methods[ P24Gateway::ID ] );
unset( $payment_methods[ TrustlyGateway::ID ] );
unset( $payment_methods[ MultibancoGateway::ID ] );
unset( $payment_methods[ PayUponInvoiceGateway::ID ] );
unset( $payment_methods[ OXXO::ID ] );
$payment_methods[ CardButtonGateway::ID ] = $all_payment_methods[ CardButtonGateway::ID ];
}
return $payment_methods;
}
);
@ -579,19 +610,28 @@ class SettingsModule implements ServiceModule, ExecutableModule {
// Enable Fastlane after onboarding if the store is compatible.
add_action(
'woocommerce_paypal_payments_apply_default_configuration',
static function () use ( $container ) {
$compatibility_checker = $container->get( 'axo.helpers.compatibility-checker' );
assert( $compatibility_checker instanceof CompatibilityChecker );
'woocommerce_paypal_payments_toggle_payment_gateways',
function( PaymentSettings $payment_methods, ConfigurationFlagsDTO $flags ) use ( $container ) {
if ( $flags->is_business_seller && $flags->use_card_payments ) {
$compatibility_checker = $container->get( 'axo.helpers.compatibility-checker' );
assert( $compatibility_checker instanceof CompatibilityChecker );
$payment_settings = $container->get( 'settings.data.payment' );
assert( $payment_settings instanceof PaymentSettings );
if ( $compatibility_checker->is_fastlane_compatible() ) {
$payment_settings->toggle_method_state( AxoGateway::ID, true );
if ( $compatibility_checker->is_fastlane_compatible() ) {
$payment_methods->toggle_method_state( AxoGateway::ID, true );
}
}
},
10,
2
);
$payment_settings->save();
// Toggle payment gateways after onboarding based on flags.
add_action(
'woocommerce_paypal_payments_sync_gateways',
static function() use ( $container ) {
$settings_data_manager = $container->get( 'settings.service.data-manager' );
assert( $settings_data_manager instanceof SettingsDataManager );
$settings_data_manager->sync_gateway_settings();
}
);
@ -603,6 +643,30 @@ class SettingsModule implements ServiceModule, ExecutableModule {
return true;
}
/**
* Checks the branded-only state and applies relevant site-wide feature limitations, if needed.
*
* @param ContainerInterface $container The DI container provider.
* @return void
*/
protected function apply_branded_only_limitations( ContainerInterface $container ) : void {
$settings = $container->get( 'settings.data.general' );
assert( $settings instanceof GeneralSettings );
if ( ! $settings->own_brand_only() ) {
return;
}
/**
* In branded-only mode, we completely disable all white label features.
*/
add_filter( 'woocommerce_paypal_payments_is_eligible_for_applepay', '__return_false' );
add_filter( 'woocommerce_paypal_payments_is_eligible_for_googlepay', '__return_false' );
add_filter( 'woocommerce_paypal_payments_is_eligible_for_axo', '__return_false' );
add_filter( 'woocommerce_paypal_payments_is_eligible_for_save_payment_methods', '__return_false' );
add_filter( 'woocommerce_paypal_payments_is_eligible_for_card_fields', '__return_false' );
}
/**
* Outputs the settings page header (title and back-link).
*

View file

@ -190,7 +190,12 @@ class CaptureCardPayment {
throw new RuntimeException( $response->get_error_message() );
}
return json_decode( $response['body'] );
$decoded_response = json_decode( $response['body'] );
if ( ! isset( $decoded_response->invoice_id ) ) {
$decoded_response->invoice_id = $invoice_id;
}
return $decoded_response;
}
}

Some files were not shown because too many files have changed in this diff Show more