🔀 Merge branch 'trunk'

This commit is contained in:
Philipp Stracker 2025-02-03 16:46:23 +01:00
commit 7cdeab40fe
No known key found for this signature in database
120 changed files with 2741 additions and 1126 deletions

View file

@ -80,10 +80,19 @@ use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer;
use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
return array(
'api.host' => function( ContainerInterface $container ) : string {
return PAYPAL_API_URL;
'api.host' => static function( ContainerInterface $container ) : string {
$environment = $container->get( 'onboarding.environment' );
assert( $environment instanceof Environment );
if ( $environment->is_sandbox() ) {
return (string) $container->get( 'api.sandbox-host' );
}
return (string) $container->get( 'api.production-host' );
},
'api.paypal-host' => function( ContainerInterface $container ) : string {
return PAYPAL_API_URL;
@ -116,6 +125,12 @@ return array(
return 'WC-';
},
'api.bearer' => static function ( ContainerInterface $container ): Bearer {
$is_connected = $container->get( 'settings.flag.is-connected' );
if ( ! $is_connected ) {
return new ConnectBearer();
}
return new PayPalBearer(
$container->get( 'api.paypal-bearer-cache' ),
$container->get( 'api.host' ),
@ -811,7 +826,7 @@ return array(
return new OrderHelper();
},
'api.helper.order-transient' => static function( ContainerInterface $container ): OrderTransient {
$cache = new Cache( 'ppcp-paypal-bearer' );
$cache = $container->get( 'api.paypal-bearer-cache' );
$purchase_unit_sanitizer = $container->get( 'api.helper.purchase-unit-sanitizer' );
return new OrderTransient( $cache, $purchase_unit_sanitizer );
},
@ -927,4 +942,22 @@ return array(
$container->get( 'api.endpoint.partner-referrals-sandbox' )
);
},
'api.sandbox-host' => static function ( ContainerInterface $container ): string {
$is_connected = $container->get( 'settings.flag.is-connected' );
if ( $is_connected ) {
return PAYPAL_SANDBOX_API_URL;
}
return CONNECT_WOO_SANDBOX_URL;
},
'api.production-host' => static function ( ContainerInterface $container ): string {
$is_connected = $container->get( 'settings.flag.is-connected' );
if ( $is_connected ) {
return PAYPAL_API_URL;
}
return CONNECT_WOO_URL;
},
);

View file

@ -0,0 +1,208 @@
<?php
/**
* Eligibility status.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
use RuntimeException;
use Exception;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatus;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class ProductStatus
*
* Base class to check the eligibility of a product for the current merchant.
*/
abstract class ProductStatus {
/**
* Caches the SellerStatus API response to avoid duplicate API calls
* during the same request.
*
* @var ?SellerStatus
*/
private static ?SellerStatus $seller_status = null;
/**
* The current status stored in memory.
*
* @var bool|null
*/
private ?bool $is_eligible = null;
/**
* If there was a request failure.
*
* @var bool
*/
private bool $has_request_failure = false;
/**
* Whether the merchant onboarding process was completed and the
* merchant API is available.
*
* @var bool
*/
private bool $is_connected;
/**
* The partners endpoint.
*
* @var PartnersEndpoint
*/
private PartnersEndpoint $partners_endpoint;
/**
* The API failure registry
*
* @var FailureRegistry
*/
private FailureRegistry $api_failure_registry;
/**
* AppleProductStatus constructor.
*
* @param bool $is_connected Whether the merchant is connected.
* @param PartnersEndpoint $partners_endpoint The Partner Endpoint.
* @param FailureRegistry $api_failure_registry The API failure registry.
*/
public function __construct(
bool $is_connected,
PartnersEndpoint $partners_endpoint,
FailureRegistry $api_failure_registry
) {
$this->is_connected = $is_connected;
$this->partners_endpoint = $partners_endpoint;
$this->api_failure_registry = $api_failure_registry;
}
/**
* Uses local data (DB values, hooks) to determine if the feature is eligible.
*
* Returns true when the feature is available, and false if ineligible.
* On failure, an RuntimeException is thrown.
*
* @return null|bool Boolean to indicate the status; null if the status not locally defined.
* @throws RuntimeException When the check failed.
* @throws NotFoundException When a relevant service or setting was not found.
*/
abstract protected function check_local_state() : ?bool;
/**
* Inspects the API response of the SellerStatus to determine feature eligibility.
*
* Returns true when the feature is available, and false if ineligible.
* On failure, an RuntimeException is thrown.
*
* @param SellerStatus $seller_status The seller status, returned from the API.
* @return bool
* @throws RuntimeException When the check failed.
*/
abstract protected function check_active_state( SellerStatus $seller_status ) : bool;
/**
* Clears the eligibility status from the local cache/DB to enforce a new
* API call on the next eligibility check.
*
* @param Settings|null $settings See description in {@see self::clear()}.
* @return void
*/
abstract protected function clear_state( Settings $settings = null ) : void;
/**
* Whether the merchant has access to the feature.
*
* @return bool
*/
public function is_active() : bool {
if ( null !== $this->is_eligible ) {
return $this->is_eligible;
}
$this->is_eligible = false;
$this->has_request_failure = false;
if ( ! $this->is_onboarded() ) {
return $this->is_eligible;
}
try {
// Try to use filters and DB values to determine the state.
$local_state = $this->check_local_state();
if ( null !== $local_state ) {
$this->is_eligible = $local_state;
return $this->is_eligible;
}
// Check using the merchant-API.
$seller_status = $this->get_seller_status_object();
$this->is_eligible = $this->check_active_state( $seller_status );
} catch ( Exception $exception ) {
$this->has_request_failure = true;
}
return $this->is_eligible;
}
/**
* Fetches the seller-status object from the PayPal merchant API.
*
* @return SellerStatus
* @throws RuntimeException When the check failed.
*/
protected function get_seller_status_object() : SellerStatus {
if ( null === self::$seller_status ) {
// Check API failure registry to prevent multiple failed API requests.
if ( $this->api_failure_registry->has_failure_in_timeframe( FailureRegistry::SELLER_STATUS_KEY, MINUTE_IN_SECONDS ) ) {
throw new RuntimeException( 'Timeout for re-check not reached yet' );
}
// Request seller status via PayPal API, might throw an Exception.
self::$seller_status = $this->partners_endpoint->seller_status();
}
return self::$seller_status;
}
/**
* Whether the merchant was fully onboarded, and we have valid API credentials.
*
* @return bool True, if we can use the merchant API endpoints.
*/
public function is_onboarded() : bool {
return $this->is_connected;
}
/**
* Returns if there was a request failure.
*
* @return bool
*/
public function has_request_failure() : bool {
return $this->has_request_failure;
}
/**
* Clears the persisted result to force a recheck.
*
* Accepts a Settings object to don't override other sequential settings that are being updated
* elsewhere.
*
* @param Settings|null $settings The settings object.
* @return void
*/
public function clear( Settings $settings = null ) : void {
$this->is_eligible = null;
$this->has_request_failure = false;
$this->clear_state( $settings );
}
}

View file

@ -10,7 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Applepay;
use WooCommerce\PayPalCommerce\Applepay\Assets\PropertiesDictionary;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager;

View file

@ -19,7 +19,7 @@ use WooCommerce\PayPalCommerce\Applepay\Assets\PropertiesDictionary;
use WooCommerce\PayPalCommerce\Applepay\Helper\ApmApplies;
use WooCommerce\PayPalCommerce\Applepay\Helper\AvailabilityNotice;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
@ -79,7 +79,7 @@ return array(
return new AppleProductStatus(
$container->get( 'wcgateway.settings' ),
$container->get( 'api.endpoint.partners' ),
$container->get( 'onboarding.state' ),
$container->get( 'settings.flag.is-connected' ),
$container->get( 'api.helper.failure-registry' )
);
}

View file

@ -17,7 +17,7 @@ use WooCommerce\PayPalCommerce\Applepay\Assets\PropertiesDictionary;
use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Applepay\Helper\AvailabilityNotice;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
@ -183,7 +183,7 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
);
add_filter(
'woocommerce_paypal_payments_rest_common_merchant_data',
'woocommerce_paypal_payments_rest_common_merchant_features',
function( array $features ) use ( $c ): array {
$product_status = $c->get( 'applepay.apple-product-status' );
assert( $product_status instanceof AppleProductStatus );

View file

@ -5,134 +5,70 @@
* @package WooCommerce\PayPalCommerce\Applepay\Assets
*/
declare(strict_types=1);
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Applepay\Assets;
use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatusCapability;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\ApiClient\Helper\ProductStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatus;
/**
* Class AppleProductStatus
*/
class AppleProductStatus {
const CAPABILITY_NAME = 'APPLE_PAY';
const SETTINGS_KEY = 'products_apple_enabled';
class AppleProductStatus extends ProductStatus {
public const CAPABILITY_NAME = 'APPLE_PAY';
public const SETTINGS_KEY = 'products_apple_enabled';
const SETTINGS_VALUE_ENABLED = 'yes';
const SETTINGS_VALUE_DISABLED = 'no';
const SETTINGS_VALUE_UNDEFINED = '';
/**
* The current status stored in memory.
*
* @var bool|null
*/
private $current_status = null;
/**
* If there was a request failure.
*
* @var bool
*/
private $has_request_failure = false;
public const SETTINGS_VALUE_ENABLED = 'yes';
public const SETTINGS_VALUE_DISABLED = 'no';
public const SETTINGS_VALUE_UNDEFINED = '';
/**
* The settings.
*
* @var Settings
*/
private $settings;
/**
* The partners endpoint.
*
* @var PartnersEndpoint
*/
private $partners_endpoint;
/**
* The onboarding status
*
* @var State
*/
private $onboarding_state;
/**
* The API failure registry
*
* @var FailureRegistry
*/
private $api_failure_registry;
private Settings $settings;
/**
* AppleProductStatus constructor.
*
* @param Settings $settings The Settings.
* @param PartnersEndpoint $partners_endpoint The Partner Endpoint.
* @param State $onboarding_state The onboarding state.
* @param Settings $settings The Settings.
* @param PartnersEndpoint $partners_endpoint The Partner Endpoint.
* @param bool $is_connected The onboarding state.
* @param FailureRegistry $api_failure_registry The API failure registry.
*/
public function __construct(
Settings $settings,
PartnersEndpoint $partners_endpoint,
State $onboarding_state,
bool $is_connected,
FailureRegistry $api_failure_registry
) {
$this->settings = $settings;
$this->partners_endpoint = $partners_endpoint;
$this->onboarding_state = $onboarding_state;
$this->api_failure_registry = $api_failure_registry;
parent::__construct( $is_connected, $partners_endpoint, $api_failure_registry );
$this->settings = $settings;
}
/**
* Whether the active/subscribed products support Applepay.
*
* @return bool
*/
public function is_active() : bool {
// If not onboarded then makes no sense to check status.
if ( ! $this->is_onboarded() ) {
return false;
}
/** {@inheritDoc} */
protected function check_local_state() : ?bool {
$status_override = apply_filters( 'woocommerce_paypal_payments_apple_pay_product_status', null );
if ( null !== $status_override ) {
return $status_override;
}
// If status was already checked on this request return the same result.
if ( null !== $this->current_status ) {
return $this->current_status;
}
// Check if status was checked on previous requests.
if ( $this->settings->has( self::SETTINGS_KEY ) && ( $this->settings->get( self::SETTINGS_KEY ) ) ) {
$this->current_status = wc_string_to_bool( $this->settings->get( self::SETTINGS_KEY ) );
return $this->current_status;
return wc_string_to_bool( $this->settings->get( self::SETTINGS_KEY ) );
}
// Check API failure registry to prevent multiple failed API requests.
if ( $this->api_failure_registry->has_failure_in_timeframe( FailureRegistry::SELLER_STATUS_KEY, HOUR_IN_SECONDS ) ) {
$this->has_request_failure = true;
$this->current_status = false;
return $this->current_status;
}
// Request seller status via PayPal API.
try {
$seller_status = $this->partners_endpoint->seller_status();
} catch ( Throwable $error ) {
$this->has_request_failure = true;
$this->current_status = false;
return $this->current_status;
}
return null;
}
/** {@inheritDoc} */
protected function check_active_state( SellerStatus $seller_status ) : bool {
// Check the seller status for the intended capability.
$has_capability = false;
foreach ( $seller_status->products() as $product ) {
@ -145,65 +81,33 @@ class AppleProductStatus {
}
}
foreach ( $seller_status->capabilities() as $capability ) {
if ( $capability->name() === self::CAPABILITY_NAME && $capability->status() === SellerStatusCapability::STATUS_ACTIVE ) {
$has_capability = true;
if ( ! $has_capability ) {
foreach ( $seller_status->capabilities() as $capability ) {
if ( $capability->name() === self::CAPABILITY_NAME && $capability->status() === SellerStatusCapability::STATUS_ACTIVE ) {
$has_capability = true;
}
}
}
if ( $has_capability ) {
// Capability found, persist status and return true.
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED );
$this->settings->persist();
$this->current_status = true;
return $this->current_status;
} else {
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_DISABLED );
}
// Capability not found, persist status and return false.
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_DISABLED );
$this->settings->persist();
$this->current_status = false;
return $this->current_status;
return $has_capability;
}
/**
* Returns if the seller is onboarded.
*
* @return bool
*/
public function is_onboarded(): bool {
return $this->onboarding_state->current_state() >= State::STATE_ONBOARDED;
}
/**
* Returns if there was a request failure.
*
* @return bool
*/
public function has_request_failure(): bool {
return $this->has_request_failure;
}
/**
* Clears the persisted result to force a recheck.
*
* @param Settings|null $settings The settings object.
* We accept a Settings object to don't override other sequential settings that are being updated elsewhere.
* @return void
*/
public function clear( Settings $settings = null ): void {
/** {@inheritDoc} */
protected function clear_state( Settings $settings = null ) : void {
if ( null === $settings ) {
$settings = $this->settings;
}
$this->current_status = null;
if ( $settings->has( self::SETTINGS_KEY ) ) {
$settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_UNDEFINED );
$settings->persist();
}
}
}

View file

@ -13,7 +13,7 @@ use WC_Payment_Gateway;
use Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType;
use WooCommerce\PayPalCommerce\Axo\FrontendLoggerEndpoint;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration;

View file

@ -12,7 +12,7 @@ namespace WooCommerce\PayPalCommerce\Axo\Assets;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
use WooCommerce\PayPalCommerce\Axo\FrontendLoggerEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;

View file

@ -19,7 +19,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\GatewaySettingsRendererTrait;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;

View file

@ -35,7 +35,7 @@ use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration;

View file

@ -36,7 +36,7 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Button\Helper\DisabledFundingSources;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\PayLaterBlock\PayLaterBlockModule;
use WooCommerce\PayPalCommerce\PayLaterWCBlocks\PayLaterWCBlocksModule;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken;

View file

@ -128,13 +128,6 @@ return array(
}
return array(
new SettingsMap(
$container->get( 'settings.data.general' ),
array(
'client_id' => 'client_id',
'client_secret' => 'client_secret',
)
),
new SettingsMap(
$container->get( 'settings.data.general' ),
/**
@ -145,7 +138,10 @@ return array(
* the credentials are used for.
*/
array(
'is_sandbox' => 'sandbox_merchant',
'merchant_id' => 'merchant_id',
'client_id' => 'client_id',
'client_secret' => 'client_secret',
'sandbox_on' => 'sandbox_merchant',
'live_client_id' => 'client_id',
'live_client_secret' => 'client_secret',
'live_merchant_id' => 'merchant_id',

View file

@ -18,7 +18,7 @@ use WooCommerce\PayPalCommerce\Googlepay\Endpoint\UpdatePaymentDataEndpoint;
use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmApplies;
use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus;
use WooCommerce\PayPalCommerce\Googlepay\Helper\AvailabilityNotice;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
@ -75,7 +75,7 @@ return array(
return new ApmProductStatus(
$container->get( 'wcgateway.settings' ),
$container->get( 'api.endpoint.partners' ),
$container->get( 'onboarding.state' ),
$container->get( 'settings.flag.is-connected' ),
$container->get( 'api.helper.failure-registry' )
);
}

View file

@ -16,7 +16,7 @@ use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Googlepay\Endpoint\UpdatePaymentDataEndpoint;
use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;

View file

@ -233,7 +233,7 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul
);
add_filter(
'woocommerce_paypal_payments_rest_common_merchant_data',
'woocommerce_paypal_payments_rest_common_merchant_features',
function ( array $features ) use ( $c ): array {
$product_status = $c->get( 'googlepay.helpers.apm-product-status' );
assert( $product_status instanceof ApmProductStatus );

View file

@ -9,130 +9,66 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Googlepay\Helper;
use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatusCapability;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\ApiClient\Helper\ProductStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatus;
/**
* Class ApmProductStatus
*/
class ApmProductStatus {
const CAPABILITY_NAME = 'GOOGLE_PAY';
const SETTINGS_KEY = 'products_googlepay_enabled';
class ApmProductStatus extends ProductStatus {
public const CAPABILITY_NAME = 'GOOGLE_PAY';
public const SETTINGS_KEY = 'products_googlepay_enabled';
const SETTINGS_VALUE_ENABLED = 'yes';
const SETTINGS_VALUE_DISABLED = 'no';
const SETTINGS_VALUE_UNDEFINED = '';
/**
* The current status stored in memory.
*
* @var bool|null
*/
private $current_status = null;
/**
* If there was a request failure.
*
* @var bool
*/
private $has_request_failure = false;
public const SETTINGS_VALUE_ENABLED = 'yes';
public const SETTINGS_VALUE_DISABLED = 'no';
public const SETTINGS_VALUE_UNDEFINED = '';
/**
* The settings.
*
* @var Settings
*/
private $settings;
/**
* The partners endpoint.
*
* @var PartnersEndpoint
*/
private $partners_endpoint;
/**
* The onboarding status
*
* @var State
*/
private $onboarding_state;
/**
* The API failure registry
*
* @var FailureRegistry
*/
private $api_failure_registry;
private Settings $settings;
/**
* ApmProductStatus constructor.
*
* @param Settings $settings The Settings.
* @param PartnersEndpoint $partners_endpoint The Partner Endpoint.
* @param State $onboarding_state The onboarding state.
* @param Settings $settings The Settings.
* @param PartnersEndpoint $partners_endpoint The Partner Endpoint.
* @param bool $is_connected The onboarding state.
* @param FailureRegistry $api_failure_registry The API failure registry.
*/
public function __construct(
Settings $settings,
PartnersEndpoint $partners_endpoint,
State $onboarding_state,
bool $is_connected,
FailureRegistry $api_failure_registry
) {
$this->settings = $settings;
$this->partners_endpoint = $partners_endpoint;
$this->onboarding_state = $onboarding_state;
$this->api_failure_registry = $api_failure_registry;
parent::__construct( $is_connected, $partners_endpoint, $api_failure_registry );
$this->settings = $settings;
}
/**
* Whether the active/subscribed products support Googlepay.
*
* @return bool
*/
public function is_active() : bool {
// If not onboarded then makes no sense to check status.
if ( ! $this->is_onboarded() ) {
return false;
}
/** {@inheritDoc} */
protected function check_local_state() : ?bool {
$status_override = apply_filters( 'woocommerce_paypal_payments_google_pay_product_status', null );
if ( null !== $status_override ) {
return $status_override;
}
// If status was already checked on this request return the same result.
if ( null !== $this->current_status ) {
return $this->current_status;
}
// Check if status was checked on previous requests.
if ( $this->settings->has( self::SETTINGS_KEY ) && ( $this->settings->get( self::SETTINGS_KEY ) ) ) {
$this->current_status = wc_string_to_bool( $this->settings->get( self::SETTINGS_KEY ) );
return $this->current_status;
return wc_string_to_bool( $this->settings->get( self::SETTINGS_KEY ) );
}
// Check API failure registry to prevent multiple failed API requests.
if ( $this->api_failure_registry->has_failure_in_timeframe( FailureRegistry::SELLER_STATUS_KEY, HOUR_IN_SECONDS ) ) {
$this->has_request_failure = true;
$this->current_status = false;
return $this->current_status;
}
// Request seller status via PayPal API.
try {
$seller_status = $this->partners_endpoint->seller_status();
} catch ( Throwable $error ) {
$this->has_request_failure = true;
$this->current_status = false;
return $this->current_status;
}
return null;
}
/** {@inheritDoc} */
protected function check_active_state( SellerStatus $seller_status ) : bool {
// Check the seller status for the intended capability.
$has_capability = false;
foreach ( $seller_status->products() as $product ) {
@ -145,65 +81,33 @@ class ApmProductStatus {
}
}
foreach ( $seller_status->capabilities() as $capability ) {
if ( $capability->name() === self::CAPABILITY_NAME && $capability->status() === SellerStatusCapability::STATUS_ACTIVE ) {
$has_capability = true;
if ( ! $has_capability ) {
foreach ( $seller_status->capabilities() as $capability ) {
if ( $capability->name() === self::CAPABILITY_NAME && $capability->status() === SellerStatusCapability::STATUS_ACTIVE ) {
$has_capability = true;
}
}
}
if ( $has_capability ) {
// Capability found, persist status and return true.
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED );
$this->settings->persist();
$this->current_status = true;
return $this->current_status;
} else {
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_DISABLED );
}
// Capability not found, persist status and return false.
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_DISABLED );
$this->settings->persist();
$this->current_status = false;
return $this->current_status;
return $has_capability;
}
/**
* Returns if the seller is onboarded.
*
* @return bool
*/
public function is_onboarded(): bool {
return $this->onboarding_state->current_state() >= State::STATE_ONBOARDED;
}
/**
* Returns if there was a request failure.
*
* @return bool
*/
public function has_request_failure(): bool {
return $this->has_request_failure;
}
/**
* Clears the persisted result to force a recheck.
*
* @param Settings|null $settings The settings object.
* We accept a Settings object to don't override other sequential settings that are being updated elsewhere.
* @return void
*/
public function clear( Settings $settings = null ): void {
/** {@inheritDoc} */
protected function clear_state( Settings $settings = null ) : void {
if ( null === $settings ) {
$settings = $this->settings;
}
$this->current_status = null;
if ( $settings->has( self::SETTINGS_KEY ) ) {
$settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_UNDEFINED );
$settings->persist();
}
}
}

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\LocalApmProductStatus;
return array(
'ppcp-local-apms.url' => static function ( ContainerInterface $container ): string {
@ -67,6 +68,14 @@ return array(
),
);
},
'ppcp-local-apms.product-status' => static function ( ContainerInterface $container ): LocalApmProductStatus {
return new LocalApmProductStatus(
$container->get( 'wcgateway.settings' ),
$container->get( 'api.endpoint.partners' ),
$container->get( 'settings.flag.is-connected' ),
$container->get( 'api.helper.failure-registry' )
);
},
'ppcp-local-apms.bancontact.wc-gateway' => static function ( ContainerInterface $container ): BancontactGateway {
return new BancontactGateway(
$container->get( 'api.endpoint.orders' ),

View file

@ -0,0 +1,95 @@
<?php
/**
* Status of local alternative payment methods.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\ApiClient\Helper\ProductStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatus;
/**
* Class LocalApmProductStatus
*/
class LocalApmProductStatus extends ProductStatus {
public const SETTINGS_KEY = 'products_local_apms_enabled';
public const SETTINGS_VALUE_ENABLED = 'yes';
public const SETTINGS_VALUE_DISABLED = 'no';
public const SETTINGS_VALUE_UNDEFINED = '';
/**
* The settings.
*
* @var Settings
*/
private Settings $settings;
/**
* ApmProductStatus constructor.
*
* @param Settings $settings The Settings.
* @param PartnersEndpoint $partners_endpoint The Partner Endpoint.
* @param bool $is_connected The onboarding state.
* @param FailureRegistry $api_failure_registry The API failure registry.
*/
public function __construct(
Settings $settings,
PartnersEndpoint $partners_endpoint,
bool $is_connected,
FailureRegistry $api_failure_registry
) {
parent::__construct( $is_connected, $partners_endpoint, $api_failure_registry );
$this->settings = $settings;
}
/** {@inheritDoc} */
protected function check_local_state() : ?bool {
if ( $this->settings->has( self::SETTINGS_KEY ) && ( $this->settings->get( self::SETTINGS_KEY ) ) ) {
return wc_string_to_bool( $this->settings->get( self::SETTINGS_KEY ) );
}
return null;
}
/** {@inheritDoc} */
protected function check_active_state( SellerStatus $seller_status ) : bool {
$has_capability = false;
foreach ( $seller_status->products() as $product ) {
if ( $product->name() === 'PAYMENT_METHODS' ) {
$has_capability = true;
break;
}
}
if ( $has_capability ) {
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED );
} else {
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_DISABLED );
}
$this->settings->persist();
return $has_capability;
}
/** {@inheritDoc} */
protected function clear_state( Settings $settings = null ) : void {
if ( null === $settings ) {
$settings = $this->settings;
}
if ( $settings->has( self::SETTINGS_KEY ) ) {
$settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_UNDEFINED );
$settings->persist();
}
}
}

View file

@ -11,58 +11,15 @@ namespace WooCommerce\PayPalCommerce\Onboarding;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\UpdateSignupLinksEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingSendOnlyNoticeRenderer;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
return array(
'api.sandbox-host' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' );
/**
* The State object.
*
* @var State $state
*/
if ( $state->current_state() >= State::STATE_ONBOARDED ) {
return PAYPAL_SANDBOX_API_URL;
}
return CONNECT_WOO_SANDBOX_URL;
},
'api.production-host' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' );
/**
* The Environment and State variables.
*
* @var Environment $environment
* @var State $state
*/
if ( $state->current_state() >= State::STATE_ONBOARDED ) {
return PAYPAL_API_URL;
}
return CONNECT_WOO_URL;
},
'api.host' => static function ( ContainerInterface $container ): string {
$environment = $container->get( 'onboarding.environment' );
/**
* The Environment and State variables.
*
* @var Environment $environment
*/
return $environment->current_environment_is( Environment::SANDBOX )
? (string) $container->get( 'api.sandbox-host' ) : (string) $container->get( 'api.production-host' );
},
'api.paypal-host' => function( ContainerInterface $container ) : string {
$environment = $container->get( 'onboarding.environment' );
/**
@ -85,38 +42,20 @@ return array(
return $container->get( 'api.paypal-website-url-production' );
},
'api.bearer' => static function ( ContainerInterface $container ): Bearer {
$state = $container->get( 'onboarding.state' );
/**
* The State.
*
* @var State $state
*/
if ( $state->current_state() < State::STATE_ONBOARDED ) {
return new ConnectBearer();
}
$cache = new Cache( 'ppcp-paypal-bearer' );
$key = $container->get( 'api.key' );
$secret = $container->get( 'api.secret' );
$host = $container->get( 'api.host' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$settings = $container->get( 'wcgateway.settings' );
return new PayPalBearer(
$cache,
$host,
$key,
$secret,
$logger,
$settings
);
},
'onboarding.state' => function( ContainerInterface $container ) : State {
$settings = $container->get( 'wcgateway.settings' );
return new State( $settings );
},
/**
* Checks if the onboarding process is completed and the merchant API can be used.
* This service is overwritten by the ppcp-settings module, when it's active.
*/
'settings.flag.is-connected' => static function ( ContainerInterface $container ) : bool {
$state = $container->get( 'onboarding.state' );
assert( $state instanceof State );
return $state->current_state() >= State::STATE_ONBOARDED;
},
'onboarding.environment' => function( ContainerInterface $container ) : Environment {
$settings = $container->get( 'wcgateway.settings' );
return new Environment( $settings );
@ -149,8 +88,8 @@ return array(
$login_seller_sandbox = $container->get( 'api.endpoint.login-seller-sandbox' );
$partner_referrals_data = $container->get( 'api.repository.partner-referrals-data' );
$settings = $container->get( 'wcgateway.settings' );
$cache = new Cache( 'ppcp-paypal-bearer' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$cache = $container->get( 'api.paypal-bearer-cache' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new LoginSellerEndpoint(
$request_data,
$login_seller_production,

View file

@ -11,7 +11,7 @@ namespace WooCommerce\PayPalCommerce\Onboarding\Assets;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\UpdateSignupLinksEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;

View file

@ -1,60 +0,0 @@
<?php
/**
* Used to detect the current environment.
*
* @package WooCommerce\PayPalCommerce\Onboarding
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Onboarding;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
* Class Environment
*/
class Environment {
const PRODUCTION = 'production';
const SANDBOX = 'sandbox';
/**
* The Settings.
*
* @var ContainerInterface
*/
private $settings;
/**
* Environment constructor.
*
* @param ContainerInterface $settings The Settings.
*/
public function __construct( ContainerInterface $settings ) {
$this->settings = $settings;
}
/**
* Returns the current environment.
*
* @return string
*/
public function current_environment(): string {
return (
$this->settings->has( 'sandbox_on' ) && $this->settings->get( 'sandbox_on' )
) ? self::SANDBOX : self::PRODUCTION;
}
/**
* Detect whether the current environment equals $environment
*
* @param string $environment The value to check against.
*
* @return bool
*/
public function current_environment_is( string $environment ): bool {
return $this->current_environment() === $environment;
}
}

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Onboarding;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
/**
* Class State

View file

@ -94,7 +94,7 @@ class SaveConfig {
*
* @param array $config The configurator config.
*/
private function save_config( array $config ): void {
public function save_config( array $config ): void {
$this->settings->set( 'pay_later_enable_styling_per_messaging_location', true );
$this->settings->set( 'pay_later_messaging_enabled', true );

View file

@ -21,7 +21,7 @@ use WC_Subscriptions_Product;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;

View file

@ -1,24 +1,33 @@
$margin_bottom: 48px;
.ppcp-r-navigation-container {
// Theming.
--wp-components-color-accent: #{$color-blueberry};
--color-text: #{$color-gray-900};
--color-disabled: #CCC;
--navbar-height: 40px;
--navbar-vertical-padding: 10px;
--subnavigation-height: 40px;
// Styling.
position: sticky;
top: var(--wp-admin--admin-bar--height);
z-index: 10;
padding: 10px 48px;
margin: 0 -20px 48px -20px;
padding: 0 48px;
margin: 0 -20px #{$margin_bottom} -20px;
box-shadow: 0 -1px 0 0 $color-gray-300 inset;
background: var(--ppcp-color-app-bg);
transition: box-shadow 0.3s;
--wp-components-color-accent: #{$color-blueberry};
--color-text: #{$color-gray-900};
--color-disabled: #CCC;
.ppcp-r-navigation {
display: flex;
justify-content: space-between;
align-items: center;
height: 40px;
flex-direction: row;
height: calc(var(--navbar-height) + (2 * var(--navbar-vertical-padding)));
padding: var(--navbar-vertical-padding) 0;
.components-button {
@include font(13, 20, 400);
@ -57,32 +66,46 @@
}
}
}
}
&--left {
align-items: center;
display: inline-flex;
}
.ppcp-r-navigation--left {
align-items: center;
display: inline-flex;
}
&--right {
.is-link {
padding: 10px 16px;
}
.ppcp-r-navigation--right {
.is-link {
padding: 10px 16px;
}
}
&--progress-bar {
position: absolute;
bottom: 0;
left: 0;
background-color: var(--wp-components-color-accent);
height: 4px;
transition: width 0.3s;
}
.ppcp-r-navigation--progress-bar {
position: absolute;
bottom: 0;
left: 0;
background-color: var(--wp-components-color-accent);
height: 4px;
transition: width 0.3s;
}
&.ppcp--is-scrolled {
box-shadow: 0 -1px 0 0 $color-gray-300 inset, 0 8px 8px 0 rgba(85, 93, 102, .3);
}
.ppcp--top-sub-navigation {
height: var(--subnavigation-height);
margin: 0;
padding: 0;
.ppcp-r-tabs {
margin: 0;
}
.components-tab-panel__tabs-item {
height: var(--subnavigation-height);
}
}
@media screen and (max-width: 782px) {
padding: 10px 12px;

View file

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

View file

@ -12,6 +12,7 @@ body:has(.ppcp-r-container--onboarding) {
.woocommerce-layout__header,
.wrap.woocommerce form > h2,
#mainform .subsubsub,
#mainform .subsubsub + br.clear,
#screen-meta-links {
display: none !important;
visibility: hidden;

View file

@ -41,6 +41,27 @@
padding-top: 16px;
}
&.is-completed {
.ppcp-r-todo-item__icon {
border-style: solid;
background-color: $color-blueberry;
display: flex;
align-items: center;
justify-content: center;
.dashicons {
color: #fff;
font-size: 18px;
width: 18px;
height: 18px;
}
}
.ppcp-r-todo-item__content {
text-decoration: line-through;
}
}
p {
@include font(14, 20, 400);
}

View file

@ -1,4 +1,4 @@
import { useEffect, useMemo } from '@wordpress/element';
import { useEffect, useMemo, useState } from '@wordpress/element';
import classNames from 'classnames';
import { OnboardingHooks, CommonHooks } from '../data';
@ -31,6 +31,8 @@ const SettingsApp = () => {
loading: ! onboardingIsReady,
} );
const [ activePanel, setActivePanel ] = useState( 'overview' );
const Content = useMemo( () => {
if ( ! onboardingIsReady || ! merchantIsReady ) {
return <SpinnerOverlay />;
@ -44,12 +46,18 @@ const SettingsApp = () => {
return <OnboardingScreen />;
}
return <SettingsScreen />;
return (
<SettingsScreen
activePanel={ activePanel }
setActivePanel={ setActivePanel }
/>
);
}, [
isSendOnlyCountry,
merchantIsReady,
onboardingCompleted,
onboardingIsReady,
activePanel,
] );
return <div className={ wrapperClass }>{ Content }</div>;

View file

@ -1,4 +1,4 @@
import data from '../../utils/data';
import { PPIcon } from './Icons';
const ImageBadge = ( { images } ) => {
if ( ! images || ! images.length ) {
@ -8,7 +8,13 @@ const ImageBadge = ( { images } ) => {
return (
<BadgeContent>
<span className="ppcp-r-badge-box__title-image-badge">
{ images.map( ( badge ) => data().getImage( badge ) ) }
{ images.map( ( badge, index ) => (
<PPIcon
key={ `badge-${ index }` }
imageName={ badge }
className="ppcp-r-badge-box__image"
/>
) ) }
</span>
</BadgeContent>
);

View file

@ -5,7 +5,7 @@ const ControlToggleButton = ( { label, description, value, onChange } ) => (
<Action>
<ToggleControl
className="ppcp--control-toggle"
__nextHasNoMarginBottom={ true }
__nextHasNoMarginBottom
checked={ value }
onChange={ onChange }
label={ label }

View file

@ -65,7 +65,9 @@ const OptionItem = ( {
<div className="ppcp--box-content">
<div className="ppcp--box-content-inner">
<span className="ppcp--box-title">{ itemTitle }</span>
<p className="ppcp--box-description">{ itemDescription }</p>
<div className="ppcp--box-description">
{ itemDescription }
</div>
{ children && (
<div className="ppcp--box-details">{ children }</div>
) }

View file

@ -0,0 +1,15 @@
import React from 'react';
const GenericIcon = ( { imageName, className = '', alt = '' } ) => {
const pathToImages = global.ppcpSettings.assets.imagesUrl;
return (
<img
className={ className }
alt={ alt }
src={ `${ pathToImages }${ imageName }` }
/>
);
};
export default GenericIcon;

View file

@ -1,6 +1,6 @@
import { SVG, Path } from '@wordpress/primitives';
const logoPayPal = (
const LogoPayPal = (
<SVG fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 38">
<Path
d="M109.583.683v27.359h-6.225V.683h6.225Zm-8.516 9.234v18.175h-5.534v-1.567c-.7.683-1.5 1.2-2.383 1.567a7.259 7.259 0 0 1-2.892.583c-1.3 0-2.508-.242-3.616-.725a9.216 9.216 0 0 1-2.892-2.067 10.021 10.021 0 0 1-1.958-3.05c-.459-1.183-.684-2.458-.684-3.816 0-1.359.225-2.617.684-3.775.483-1.184 1.133-2.217 1.958-3.092a8.708 8.708 0 0 1 2.892-2.033c1.108-.509 2.316-.767 3.616-.767 1.034 0 2 .192 2.892.583a7.312 7.312 0 0 1 2.383 1.567V9.933h5.534v-.016Zm-9.809 13.225c1.134 0 2.059-.384 2.784-1.167.75-.775 1.125-1.767 1.125-2.975 0-1.208-.375-2.208-1.125-2.975-.725-.775-1.659-1.167-2.784-1.167-1.125 0-2.075.384-2.825 1.167-.725.775-1.083 1.767-1.083 2.975 0 1.208.367 2.208 1.083 2.975.75.775 1.692 1.167 2.825 1.167ZM72.225.683c1.642 0 3.042.234 4.2.692 1.158.458 2.133 1.1 2.933 1.925a9.439 9.439 0 0 1 1.917 2.908c.458 1.092.683 2.267.683 3.525 0 1.259-.225 2.434-.683 3.525a9.293 9.293 0 0 1-1.917 2.909c-.791.825-1.775 1.466-2.933 1.925-1.158.458-2.558.691-4.2.691h-3v9.3h-6.333V.683h9.333Zm-.908 12.467c.85 0 1.491-.083 1.958-.258a3.853 3.853 0 0 0 1.192-.725c.65-.609.975-1.417.975-2.434 0-1.016-.325-1.825-.975-2.433a3.329 3.329 0 0 0-1.192-.692c-.458-.191-1.108-.291-1.958-.291h-2.1v6.833h2.1ZM39.558 9.917h6.875l4.667 8.716h.075l4.158-8.716H61.7l-13.642 27.4h-6.333l6.225-12.534-8.392-14.866Zm-1.225 0v18.175H32.8v-1.567c-.7.683-1.5 1.2-2.383 1.567a7.258 7.258 0 0 1-2.892.583c-1.3 0-2.508-.242-3.617-.725a9.218 9.218 0 0 1-2.891-2.067 10.18 10.18 0 0 1-1.959-3.05c-.458-1.183-.683-2.458-.683-3.816 0-1.359.225-2.617.683-3.775.484-1.184 1.134-2.217 1.959-3.092a8.626 8.626 0 0 1 2.891-2.033c1.109-.509 2.317-.767 3.617-.767 1.033 0 2 .192 2.892.583A7.312 7.312 0 0 1 32.8 11.5V9.933h5.533v-.016Zm-9.808 13.225c1.133 0 2.058-.384 2.792-1.167.75-.775 1.125-1.767 1.125-2.975 0-1.208-.375-2.208-1.125-2.975-.725-.775-1.659-1.167-2.792-1.167-1.133 0-2.075.384-2.825 1.167-.725.775-1.083 1.767-1.083 2.975 0 1.208.366 2.208 1.083 2.975.75.775 1.692 1.167 2.825 1.167ZM9.75.683c1.642 0 3.042.234 4.2.692 1.158.458 2.133 1.1 2.933 1.925A9.439 9.439 0 0 1 18.8 6.208c.458 1.092.683 2.267.683 3.525 0 1.259-.225 2.434-.683 3.525a9.293 9.293 0 0 1-1.917 2.909c-.791.825-1.775 1.466-2.933 1.925-1.158.458-2.558.691-4.2.691h-3v9.3H.417V.683H9.75Zm-.9 12.467c.85 0 1.492-.083 1.958-.258A3.855 3.855 0 0 0 12 12.167c.65-.609.975-1.417.975-2.434 0-1.016-.325-1.825-.975-2.433a3.33 3.33 0 0 0-1.192-.692c-.458-.191-1.108-.291-1.958-.291h-2.1v6.833h2.1Z"
@ -9,4 +9,4 @@ const logoPayPal = (
</SVG>
);
export default logoPayPal;
export default LogoPayPal;

View file

@ -1,9 +1,9 @@
import { SVG, Path } from '@wordpress/primitives';
const openSignup = (
const OpenSignup = (
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 24">
<Path d="M12.4999 12.75V18.75C12.4999 18.9489 12.4209 19.1397 12.2803 19.2803C12.1396 19.421 11.9488 19.5 11.7499 19.5C11.551 19.5 11.3603 19.421 11.2196 19.2803C11.0789 19.1397 10.9999 18.9489 10.9999 18.75V14.5613L4.78055 20.7806C4.71087 20.8503 4.62815 20.9056 4.5371 20.9433C4.44606 20.981 4.34847 21.0004 4.24993 21.0004C4.15138 21.0004 4.0538 20.981 3.96276 20.9433C3.87171 20.9056 3.78899 20.8503 3.7193 20.7806C3.64962 20.7109 3.59435 20.6282 3.55663 20.5372C3.51892 20.4461 3.49951 20.3485 3.49951 20.25C3.49951 20.1515 3.51892 20.0539 3.55663 19.9628C3.59435 19.8718 3.64962 19.7891 3.7193 19.7194L9.93868 13.5H5.74993C5.55102 13.5 5.36025 13.421 5.2196 13.2803C5.07895 13.1397 4.99993 12.9489 4.99993 12.75C4.99993 12.5511 5.07895 12.3603 5.2196 12.2197C5.36025 12.079 5.55102 12 5.74993 12H11.7499C11.9488 12 12.1396 12.079 12.2803 12.2197C12.4209 12.3603 12.4999 12.5511 12.4999 12.75ZM19.9999 3H7.99993C7.6021 3 7.22057 3.15804 6.93927 3.43934C6.65796 3.72064 6.49993 4.10218 6.49993 4.5V9C6.49993 9.19891 6.57895 9.38968 6.7196 9.53033C6.86025 9.67098 7.05102 9.75 7.24993 9.75C7.44884 9.75 7.63961 9.67098 7.78026 9.53033C7.92091 9.38968 7.99993 9.19891 7.99993 9V4.5H19.9999V16.5H15.4999C15.301 16.5 15.1103 16.579 14.9696 16.7197C14.8289 16.8603 14.7499 17.0511 14.7499 17.25C14.7499 17.4489 14.8289 17.6397 14.9696 17.7803C15.1103 17.921 15.301 18 15.4999 18H19.9999C20.3978 18 20.7793 17.842 21.0606 17.5607C21.3419 17.2794 21.4999 16.8978 21.4999 16.5V4.5C21.4999 4.10218 21.3419 3.72064 21.0606 3.43934C20.7793 3.15804 20.3978 3 19.9999 3Z" />
</SVG>
);
export default openSignup;
export default OpenSignup;

View file

@ -1,5 +1,6 @@
export { default as openSignup } from './open-signup';
export { default as logoPayPal } from './logo-paypal';
export { default as PPIcon } from './GenericIcon';
export { default as OpenSignup } from './OpenSignup';
export { default as LogoPayPal } from './LogoPayPal';
export const NOTIFICATION_SUCCESS = '✔️';
export const NOTIFICATION_ERROR = '❌';

View file

@ -16,7 +16,7 @@ const SettingsBlock = ( {
} );
return (
<div className={ blockClassName }>
<div className={ blockClassName } id={ className }>
<BlockTitle
blockTitle={ title }
blockSuffix={ titleSuffix }

View file

@ -14,10 +14,12 @@ const PaymentMethodItemBlock = ( {
<SettingsBlock className="ppcp--method-item" separatorAndGap={ false }>
<div className="ppcp--method-inner">
<div className="ppcp--method-title-wrapper">
<PaymentMethodIcon
icons={ [ paymentMethod.icon ] }
type={ paymentMethod.icon }
/>
{ paymentMethod?.icon && (
<PaymentMethodIcon
icons={ [ paymentMethod.icon ] }
type={ paymentMethod.icon }
/>
) }
<span className="ppcp--method-title">
{ paymentMethod.itemTitle }
</span>
@ -27,7 +29,7 @@ const PaymentMethodItemBlock = ( {
</p>
<div className="ppcp--method-footer">
<ToggleControl
__nextHasNoMarginBottom={ true }
__nextHasNoMarginBottom
checked={ isSelected }
onChange={ onSelect }
/>

View file

@ -1,3 +1,5 @@
import { selectTab, TAB_IDS } from '../../../utils/tabSelector';
const TodoSettingsBlock = ( { todosData, className = '' } ) => {
if ( todosData.length === 0 ) {
return null;
@ -7,29 +9,50 @@ const TodoSettingsBlock = ( { todosData, className = '' } ) => {
<div
className={ `ppcp-r-settings-block__todo ppcp-r-todo-items ${ className }` }
>
{ todosData
.slice( 0, 5 )
.filter( ( todo ) => {
return ! todo.isCompleted();
} )
.map( ( todo ) => (
<TodoItem
key={ todo.id }
title={ todo.title }
onClick={ todo.onClick }
/>
) ) }
{ todosData.slice( 0, 5 ).map( ( todo ) => (
<TodoItem
key={ todo.id }
title={ todo.title }
description={ todo.description }
isCompleted={ todo.isCompleted }
onClick={ async () => {
if ( todo.action.type === 'tab' ) {
const tabId =
TAB_IDS[ todo.action.tab.toUpperCase() ];
await selectTab( tabId, todo.action.section );
} else if ( todo.action.type === 'external' ) {
window.open( todo.action.url, '_blank' );
}
} }
/>
) ) }
</div>
);
};
const TodoItem = ( props ) => {
const TodoItem = ( { title, description, isCompleted, onClick } ) => {
return (
<div className="ppcp-r-todo-item" onClick={ props.onClick }>
<div
className={ `ppcp-r-todo-item ${
isCompleted ? 'is-completed' : ''
}` }
onClick={ onClick }
>
<div className="ppcp-r-todo-item__inner">
<div className="ppcp-r-todo-item__icon"></div>
<div className="ppcp-r-todo-item__description">
{ props.title }
<div className="ppcp-r-todo-item__icon">
{ isCompleted && (
<span className="dashicons dashicons-yes"></span>
) }
</div>
<div className="ppcp-r-todo-item__content">
<div className="ppcp-r-todo-item__description">
{ title }
</div>
{ description && (
<div className="ppcp-r-todo-item__secondary-description">
{ description }
</div>
) }
</div>
</div>
</div>

View file

@ -43,6 +43,7 @@ const SettingsToggleBlock = ( {
</div>
<div className="ppcp-r-toggle-block__switch">
<ToggleControl
__nextHasNoMarginBottom
ref={ toggleRef }
checked={ isToggled }
onChange={ ( newState ) => setToggled( newState ) }

View file

@ -1,26 +1,14 @@
import { useCallback, useEffect, useState } from '@wordpress/element';
import { useCallback, useEffect } from '@wordpress/element';
// TODO: Migrate to Tabs (TabPanel v2) once its API is publicly available, as it provides programmatic tab switching support: https://github.com/WordPress/gutenberg/issues/52997
import { TabPanel } from '@wordpress/components';
import { getQuery, updateQueryString } from '../../utils/navigation';
const TabNavigation = ( { tabs } ) => {
const { panel } = getQuery();
import { updateQueryString } from '../../utils/navigation';
const TabBar = ( { tabs, activePanel, setActivePanel } ) => {
const isValidTab = ( tabsList, checkTab ) => {
return tabsList.some( ( tab ) => tab.name === checkTab );
};
const getValidInitialPanel = () => {
if ( ! panel || ! isValidTab( tabs, panel ) ) {
return tabs[ 0 ].name;
}
return panel;
};
const [ activePanel, setActivePanel ] = useState( getValidInitialPanel );
const updateActivePanel = useCallback(
( tabName ) => {
if ( isValidTab( tabs, tabName ) ) {
@ -29,7 +17,7 @@ const TabNavigation = ( { tabs } ) => {
console.warn( `Invalid tab name: ${ tabName }` );
}
},
[ tabs ]
[ tabs, setActivePanel ]
);
useEffect( () => {
@ -43,9 +31,9 @@ const TabNavigation = ( { tabs } ) => {
onSelect={ updateActivePanel }
tabs={ tabs }
>
{ ( { Component } ) => Component }
{ () => '' }
</TabPanel>
);
};
export default TabNavigation;
export default TabBar;

View file

@ -15,6 +15,7 @@ const TopNavigation = ( {
onTitleClick = null,
showProgressBar = false,
progressBarPercent = 0,
subNavigation = null,
} ) => {
const { goToWooCommercePaymentsTab } = useNavigation();
const { isScrolled } = useIsScrolled();
@ -40,35 +41,43 @@ const TopNavigation = ( {
}, [] );
return (
<div className={ className }>
<div className="ppcp-r-navigation">
<BusyStateWrapper
className="ppcp-r-navigation--left"
busySpinner={ false }
enabled={ ! exitOnTitleClick }
>
<Button
variant="link"
onClick={ handleTitleClick }
className="is-title"
<>
<nav className={ className }>
<div className="ppcp-r-navigation">
<BusyStateWrapper
className="ppcp-r-navigation--left"
busySpinner={ false }
enabled={ ! exitOnTitleClick }
>
<Icon icon={ chevronLeft } />
<span className={ titleClassName }>{ title }</span>
</Button>
</BusyStateWrapper>
<Button
variant="link"
onClick={ handleTitleClick }
className="is-title"
>
<Icon icon={ chevronLeft } />
<span className={ titleClassName }>{ title }</span>
</Button>
</BusyStateWrapper>
<BusyStateWrapper
className="ppcp-r-navigation--right"
busySpinner={ false }
>
{ children }
</BusyStateWrapper>
<BusyStateWrapper
className="ppcp-r-navigation--right"
busySpinner={ false }
>
{ children }
</BusyStateWrapper>
</div>
{ subNavigation && (
<section className="ppcp--top-sub-navigation">
{ subNavigation }
</section>
) }
{ showProgressBar && (
<ProgressBar percent={ progressBarPercent } />
) }
</div>
</div>
</nav>
</>
);
};

View file

@ -145,7 +145,7 @@ const AcdcOptionalPaymentMethods = ( {
'woocommerce-paypal-payments'
) }
imageBadge={ [
'icon-button-sepa.svg',
// 'icon-button-sepa.svg',
'icon-button-ideal.svg',
'icon-button-blik.svg',
'icon-button-bancontact.svg',
@ -211,7 +211,7 @@ const AcdcOptionalPaymentMethods = ( {
'woocommerce-paypal-payments'
) }
imageBadge={ [
'icon-button-sepa.svg',
// 'icon-button-sepa.svg',
'icon-button-ideal.svg',
'icon-button-blik.svg',
'icon-button-bancontact.svg',

View file

@ -1,7 +1,7 @@
import { Button } from '@wordpress/components';
import { useEffect } from '@wordpress/element';
import classNames from 'classnames';
import { openSignup } from '../../../ReusableComponents/Icons';
import { OpenSignup } from '../../../ReusableComponents/Icons';
import { useHandleOnboardingButton } from '../../../../hooks/useHandleConnections';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
@ -27,7 +27,7 @@ const ButtonOrPlaceholder = ( {
const buttonProps = {
className,
variant,
icon: showIcon ? openSignup : null,
icon: showIcon ? OpenSignup : null,
};
if ( href ) {

View file

@ -21,9 +21,37 @@ const OnboardingNavigation = ( { stepDetails, onNext, onPrev } ) => {
showProgressBar={ true }
progressBarPercent={ percentage * 0.9 }
>
<Button variant="link" onClick={ goToWooCommercePaymentsTab }>
<OnboardingNavigationActions
onExit={ goToWooCommercePaymentsTab }
isFirst={ isFirst }
isDisabled={ isDisabled }
showNext={ showNext }
onNext={ onNext }
/>
</TopNavigation>
);
};
export default OnboardingNavigation;
const OnboardingNavigationActions = ( {
isFirst,
showNext,
isDisabled,
onExit,
onNext,
} ) => {
// On first page we don't have any actions.
if ( isFirst ) {
return null;
}
return (
<>
<Button variant="link" onClick={ onExit }>
{ __( 'Save and exit', 'woocommerce-paypal-payments' ) }
</Button>
{ showNext && (
<Button
variant="primary"
@ -33,8 +61,6 @@ const OnboardingNavigation = ( { stepDetails, onNext, onPrev } ) => {
{ __( 'Continue', 'woocommerce-paypal-payments' ) }
</Button>
) }
</TopNavigation>
</>
);
};
export default OnboardingNavigation;

View file

@ -1,13 +1,13 @@
import { Icon } from '@wordpress/components';
import { logoPayPal } from '../../../ReusableComponents/Icons';
import { LogoPayPal } from '../../../ReusableComponents/Icons';
const OnboardingHeader = ( props ) => {
return (
<section className="ppcp-r-onboarding-header">
<div className="ppcp-r-onboarding-header__logo">
<div className="ppcp-r-onboarding-header__logo-wrapper">
<Icon icon={ logoPayPal } width="auto" height={ 38 } />
<Icon icon={ LogoPayPal } width={ 110 } height={ 38 } />
</div>
</div>
<div className="ppcp-r-onboarding-header__content">

View file

@ -1,7 +1,16 @@
import React, { useEffect } from 'react';
import { PayLaterMessagingHooks } from '../../../data';
const TabPayLaterMessaging = () => {
const config = {}; // Replace with the appropriate/saved configuration.
const {
config,
setCart,
setCheckout,
setProduct,
setShop,
setHome,
setCustom_placement,
} = PayLaterMessagingHooks.usePayLaterMessaging();
const PcpPayLaterConfigurator =
window.ppcpSettings?.PcpPayLaterConfigurator;
@ -27,17 +36,16 @@ const TabPayLaterMessaging = () => {
subheader: 'ppcp-r-paylater-configurator__subheader',
},
onSave: ( data ) => {
/*
TODO:
- The saving will be handled in a separate PR.
- One option could be:
- When saving the settings, programmatically click on the configurator's
"Save Changes" button and send the request to PHP.
*/
setCart( data.config.cart );
setCheckout( data.config.checkout );
setProduct( data.config.product );
setShop( data.config.shop );
setHome( data.config.home );
setCustom_placement( data.config.custom_placement );
},
} );
}
}, [ PcpPayLaterConfigurator ] );
}, [ PcpPayLaterConfigurator, config ] );
return (
<div

View file

@ -3,14 +3,30 @@ import { __ } from '@wordpress/i18n';
import TopNavigation from '../../../ReusableComponents/TopNavigation';
import { useSaveSettings } from '../../../../hooks/useSaveSettings';
import TabBar from '../../../ReusableComponents/TabBar';
const SettingsNavigation = ( { canSave = true } ) => {
const SettingsNavigation = ( {
canSave = true,
tabs,
activePanel,
setActivePanel,
} ) => {
const { persistAll } = useSaveSettings();
const title = __( 'PayPal Payments', 'woocommerce-paypal-payments' );
return (
<TopNavigation title={ title } exitOnTitleClick={ true }>
<TopNavigation
title={ title }
exitOnTitleClick={ true }
subNavigation={
<TabBar
tabs={ tabs }
activePanel={ activePanel }
setActivePanel={ setActivePanel }
/>
}
>
{ canSave && (
<Button variant="primary" onClick={ persistAll }>
{ __( 'Save', 'woocommerce-paypal-payments' ) }

View file

@ -18,11 +18,9 @@ export const getFeatures = ( setActiveModal ) => {
text: __( 'Configure', 'woocommerce-paypal-payments' ),
onClick: () => {
selectTab(
TAB_IDS.PAYMENT_METHODS,
'ppcp-paypal-checkout-card'
).then( () => {
setActiveModal( 'paypal' );
} );
TAB_IDS.SETTINGS,
'ppcp--save-payment-methods'
);
},
showWhen: 'enabled',
class: 'small-button',
@ -68,9 +66,7 @@ export const getFeatures = ( setActiveModal ) => {
TAB_IDS.PAYMENT_METHODS,
'ppcp-card-payments-card'
).then( () => {
setActiveModal(
'advanced_credit_and_debit_card_payments'
);
setActiveModal( 'ppcp-credit-card-gateway' );
} );
},
showWhen: 'enabled',
@ -149,7 +145,7 @@ export const getFeatures = ( setActiveModal ) => {
TAB_IDS.PAYMENT_METHODS,
'ppcp-card-payments-card'
).then( () => {
setActiveModal( 'google_pay' );
setActiveModal( 'ppcp-googlepay' );
} );
},
showWhen: 'enabled',
@ -196,7 +192,7 @@ export const getFeatures = ( setActiveModal ) => {
TAB_IDS.PAYMENT_METHODS,
'ppcp-card-payments-card'
).then( () => {
setActiveModal( 'apple_pay' );
setActiveModal( 'ppcp-applepay' );
} );
},
showWhen: 'enabled',
@ -239,7 +235,6 @@ export const getFeatures = ( setActiveModal ) => {
const countryData = payLaterMessaging[ storeCountry ] || {};
// Add "Pay Later Messaging" to the feature list, if it's available.
if (
!! window.ppcpSettings?.isPayLaterConfiguratorAvailable &&
countryData
@ -256,12 +251,7 @@ export const getFeatures = ( setActiveModal ) => {
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
onClick: () => {
selectTab(
TAB_IDS.PAYMENT_METHODS,
'ppcp-paypal-checkout-card'
).then( () => {
setActiveModal( 'paypal' );
} );
selectTab( TAB_IDS.PAY_LATER_MESSAGING );
},
showWhen: 'enabled',
class: 'small-button',

View file

@ -11,7 +11,7 @@ import PaymentMethodModal from '../../../../ReusableComponents/PaymentMethodModa
import { PaymentHooks } from '../../../../../data';
const Modal = ( { method, setModalIsVisible, onSave } ) => {
const { paymentMethods } = PaymentHooks.usePaymentMethods();
const { all: paymentMethods } = PaymentHooks.usePaymentMethods();
const {
paypalShowLogo,
threeDSecure,
@ -64,9 +64,9 @@ const Modal = ( { method, setModalIsVisible, onSave } ) => {
switch ( field.type ) {
case 'text':
return (
<div className="ppcp-r-modal__field-row">
<div key={ key } className="ppcp-r-modal__field-row">
<TextControl
__nextHasNoMarginBottom={ true }
__nextHasNoMarginBottom
className="ppcp-r-vertical-text-control"
label={ field.label }
value={ settings[ key ] }
@ -82,8 +82,9 @@ const Modal = ( { method, setModalIsVisible, onSave } ) => {
case 'toggle':
return (
<div className="ppcp-r-modal__field-row">
<div key={ key } className="ppcp-r-modal__field-row">
<ToggleControl
__nextHasNoMarginBottom
label={ field.label }
checked={ settings[ key ] }
onChange={ ( value ) =>

View file

@ -12,10 +12,10 @@ import {
import { Content, ContentWrapper } from '../../../ReusableComponents/Elements';
import SettingsCard from '../../../ReusableComponents/SettingsCard';
import { TITLE_BADGE_POSITIVE } from '../../../ReusableComponents/TitleBadge';
import { useTodos } from '../../../../data/todos/hooks';
import { useMerchantInfo } from '../../../../data/common/hooks';
import { STORE_NAME } from '../../../../data/common';
import { getFeatures } from '../Components/Overview/features-config';
import { todosData } from '../todo-items';
import {
NOTIFICATION_ERROR,
@ -23,9 +23,14 @@ import {
} from '../../../ReusableComponents/Icons';
const TabOverview = () => {
const { todos, isReady: areTodosReady } = useTodos();
// Don't render todos section until data is ready
const showTodos = areTodosReady && todos.length > 0;
return (
<div className="ppcp-r-tab-overview">
{ todosData.length > 0 && (
{ showTodos && (
<SettingsCard
className="ppcp-r-tab-overview-todo"
title={ __(
@ -37,7 +42,7 @@ const TabOverview = () => {
'woocommerce-paypal-payments'
) }
>
<TodoSettingsBlock todosData={ todosData } />
<TodoSettingsBlock todosData={ todos } />
</SettingsCard>
) }

View file

@ -1,17 +1,18 @@
import Container from '../../ReusableComponents/Container';
import TabNavigation from '../../ReusableComponents/TabNavigation';
import { getSettingsTabs } from './Tabs';
import SettingsNavigation from './Components/Navigation';
import { getSettingsTabs } from './Tabs';
const SettingsScreen = () => {
const SettingsScreen = ( { activePanel, setActivePanel } ) => {
const tabs = getSettingsTabs();
const { Component } = tabs.find( ( tab ) => tab.name === activePanel );
return (
<>
<SettingsNavigation />
<Container page="settings">
<TabNavigation tabs={ tabs }></TabNavigation>
</Container>
<SettingsNavigation
tabs={ tabs }
activePanel={ activePanel }
setActivePanel={ setActivePanel }
/>
<Container page="settings">{ Component }</Container>
</>
);
};

View file

@ -34,7 +34,7 @@ const useHooks = () => {
};
};
export const useState = () => {
export const useStore = () => {
const { persist, isReady } = useHooks();
return { persist, isReady };
};

View file

@ -4,16 +4,24 @@ import {
PaymentStoreName,
SettingsStoreName,
StylingStoreName,
TodosStoreName,
} from './index';
import { setCompleted } from './onboarding/actions';
export const addDebugTools = ( context, modules ) => {
if ( ! context || ! context?.debug ) {
if ( ! context ) {
return;
}
/*
// TODO - enable this condition for version 3.0.1
// In version 3.0.0 we want to have the debug tools available on every installation
if ( ! context.debug ) { return }
*/
const debugApi = ( window.ppcpDebugger = window.ppcpDebugger || {} );
// Dump the current state of all our Redux stores.
context.dumpStore = async () => {
debugApi.dumpStore = async () => {
/* eslint-disable no-console */
if ( ! console?.groupCollapsed ) {
console.error( 'console.groupCollapsed is not supported.' );
@ -41,7 +49,7 @@ export const addDebugTools = ( context, modules ) => {
};
// Reset all Redux stores to their initial state.
context.resetStore = () => {
debugApi.resetStore = () => {
const stores = [];
const { isConnected } = wp.data.select( CommonStoreName ).merchant();
@ -56,6 +64,7 @@ export const addDebugTools = ( context, modules ) => {
stores.push( PaymentStoreName );
stores.push( SettingsStoreName );
stores.push( StylingStoreName );
stores.push( TodosStoreName );
} else {
// Only reset the common & onboarding stores to restart the onboarding wizard.
stores.push( CommonStoreName );
@ -68,13 +77,17 @@ export const addDebugTools = ( context, modules ) => {
// eslint-disable-next-line no-console
console.log( `Reset store: ${ storeName }...` );
store.reset();
store.persist();
try {
store.reset();
store.persist();
} catch ( error ) {
console.error( ' ... Reset failed, skipping this store' );
}
} );
};
// Disconnect the merchant and display the onboarding wizard.
context.disconnect = () => {
debugApi.disconnect = () => {
const common = wp.data.dispatch( CommonStoreName );
common.disconnectMerchant();
@ -86,10 +99,13 @@ export const addDebugTools = ( context, modules ) => {
};
// Enters or completes the onboarding wizard without changing anything else.
context.onboardingMode = ( state ) => {
debugApi.onboardingMode = ( state ) => {
const onboarding = wp.data.dispatch( OnboardingStoreName );
onboarding.setCompleted( ! state );
onboarding.persist();
};
// Expose original debug API.
Object.assign( context, debugApi );
};

View file

@ -4,8 +4,18 @@ import * as Common from './common';
import * as Payment from './payment';
import * as Settings from './settings';
import * as Styling from './styling';
import * as Todos from './todos';
import * as PayLaterMessaging from './pay-later-messaging';
const stores = [ Onboarding, Common, Payment, Settings, Styling ];
const stores = [
Onboarding,
Common,
Payment,
Settings,
Styling,
Todos,
PayLaterMessaging,
];
stores.forEach( ( store ) => {
try {
@ -28,12 +38,16 @@ export const CommonHooks = Common.hooks;
export const PaymentHooks = Payment.hooks;
export const SettingsHooks = Settings.hooks;
export const StylingHooks = Styling.hooks;
export const TodosHooks = Todos.hooks;
export const PayLaterMessagingHooks = PayLaterMessaging.hooks;
export const OnboardingStoreName = Onboarding.STORE_NAME;
export const CommonStoreName = Common.STORE_NAME;
export const PaymentStoreName = Payment.STORE_NAME;
export const SettingsStoreName = Settings.STORE_NAME;
export const StylingStoreName = Styling.STORE_NAME;
export const TodosStoreName = Todos.STORE_NAME;
export const PayLaterMessagingStoreName = PayLaterMessaging.STORE_NAME;
export * from './configuration';

View file

@ -0,0 +1,18 @@
/**
* Action Types: Define unique identifiers for actions across all store modules.
*
* @file
*/
export default {
// Transient data.
SET_TRANSIENT: 'PAY_LATER_MESSAGING:SET_TRANSIENT',
// Persistent data.
SET_PERSISTENT: 'PAY_LATER_MESSAGING:SET_PERSISTENT',
RESET: 'PAY_LATER_MESSAGING:RESET',
HYDRATE: 'PAY_LATER_MESSAGING:HYDRATE',
// Controls - always start with "DO_".
DO_PERSIST_DATA: 'PAY_LATER_MESSAGING:DO_PERSIST_DATA',
};

View file

@ -0,0 +1,80 @@
/**
* Action Creators: Define functions to create action objects.
*
* These functions update state or trigger side effects (e.g., async operations).
* Actions are categorized as Transient, Persistent, or Side effect.
*
* @file
*/
import { select } from '@wordpress/data';
import ACTION_TYPES from './action-types';
import { STORE_NAME } from './constants';
/**
* @typedef {Object} Action An action object that is handled by a reducer or control.
* @property {string} type - The action type.
* @property {Object?} payload - Optional payload for the action.
*/
/**
* Special. Resets all values in the store to initial defaults.
*
* @return {Action} The action.
*/
export const reset = () => ( { type: ACTION_TYPES.RESET } );
/**
* Persistent. Set the full store details during app initialization.
*
* @param {{data: {}, flags?: {}}} payload
* @return {Action} The action.
*/
export const hydrate = ( payload ) => ( {
type: ACTION_TYPES.HYDRATE,
payload,
} );
/**
* Generic transient-data updater.
*
* @param {string} prop Name of the property to update.
* @param {any} value The new value of the property.
* @return {Action} The action.
*/
export const setTransient = ( prop, value ) => ( {
type: ACTION_TYPES.SET_TRANSIENT,
payload: { [ prop ]: value },
} );
/**
* Generic persistent-data updater.
*
* @param {string} prop Name of the property to update.
* @param {any} value The new value of the property.
* @return {Action} The action.
*/
export const setPersistent = ( prop, value ) => ( {
type: ACTION_TYPES.SET_PERSISTENT,
payload: { [ prop ]: value },
} );
/**
* Transient. Marks the store as "ready", i.e., fully initialized.
*
* @param {boolean} isReady
* @return {Action} The action.
*/
export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
/**
* Side effect. Triggers the persistence of store data to the server.
*
* @return {Action} The action.
*/
export const persist = function* () {
const data = yield select( STORE_NAME ).persistentData();
yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
};

View file

@ -0,0 +1,28 @@
/**
* Name of the Redux store module.
*
* Used by: Reducer, Selector, Index
*
* @type {string}
*/
export const STORE_NAME = 'wc/paypal/pay_later_messaging';
/**
* REST path to hydrate data of this module by loading data from the WP DB.
*
* Used by: Resolvers
* See: PayLaterMessagingEndpoint.php
*
* @type {string}
*/
export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/pay_later_messaging';
/**
* REST path to persist data of this module to the WP DB.
*
* Used by: Controls
* See: PayLaterMessagingEndpoint.php
*
* @type {string}
*/
export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/pay_later_messaging';

View file

@ -0,0 +1,23 @@
/**
* Controls: Implement side effects, typically asynchronous operations.
*
* Controls use ACTION_TYPES keys as identifiers.
* They are triggered by corresponding actions and handle external interactions.
*
* @file
*/
import apiFetch from '@wordpress/api-fetch';
import { REST_PERSIST_PATH } from './constants';
import ACTION_TYPES from './action-types';
export const controls = {
async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) {
return await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
},
};

View file

@ -0,0 +1,89 @@
/**
* Hooks: Provide the main API for components to interact with the store.
*
* These encapsulate store interactions, offering a consistent interface.
* Hooks simplify data access and manipulation for components.
*
* @file
*/
import { useDispatch } from '@wordpress/data';
import { createHooksForStore } from '../utils';
import { STORE_NAME } from './constants';
const useHooks = () => {
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
const { persist } = useDispatch( STORE_NAME );
// Read-only flags and derived state.
// Nothing here yet.
// Transient accessors.
const [ isReady ] = useTransient( 'isReady' );
// Persistent accessors.
const [ cart, setCart ] = usePersistent( 'cart' );
const [ checkout, setCheckout ] = usePersistent( 'checkout' );
const [ product, setProduct ] = usePersistent( 'product' );
const [ shop, setShop ] = usePersistent( 'shop' );
const [ home, setHome ] = usePersistent( 'home' );
const [ custom_placement, setCustom_placement ] =
usePersistent( 'custom_placement' );
return {
persist,
isReady,
cart,
setCart,
checkout,
setCheckout,
product,
setProduct,
shop,
setShop,
home,
setHome,
custom_placement,
setCustom_placement,
};
};
export const useStore = () => {
const { persist, isReady } = useHooks();
return { persist, isReady };
};
export const usePayLaterMessaging = () => {
const {
cart,
setCart,
checkout,
setCheckout,
product,
setProduct,
shop,
setShop,
home,
setHome,
custom_placement,
setCustom_placement,
} = useHooks();
return {
config: {
cart,
checkout,
product,
shop,
home,
custom_placement,
},
setCart,
setCheckout,
setProduct,
setShop,
setHome,
setCustom_placement,
};
};

View file

@ -0,0 +1,32 @@
import { createReduxStore, register } from '@wordpress/data';
import { controls as wpControls } from '@wordpress/data-controls';
import { STORE_NAME } from './constants';
import reducer from './reducer';
import * as selectors from './selectors';
import * as actions from './actions';
import * as hooks from './hooks';
import { resolvers } from './resolvers';
import { controls } from './controls';
/**
* Initializes and registers the settings store with WordPress data layer.
* Combines custom controls with WordPress data controls.
*
* @return {boolean} True if initialization succeeded, false otherwise.
*/
export const initStore = () => {
const store = createReduxStore( STORE_NAME, {
reducer,
controls: { ...wpControls, ...controls },
actions,
selectors,
resolvers,
} );
register( store );
return Boolean( wp.data.select( STORE_NAME ) );
};
export { hooks, selectors, STORE_NAME };

View file

@ -0,0 +1,60 @@
/**
* Reducer: Defines store structure and state updates for this module.
*
* Manages both transient (temporary) and persistent (saved) state.
* The initial state must define all properties, as dynamic additions are not supported.
*
* @file
*/
import { createReducer, createReducerSetters } from '../utils';
import ACTION_TYPES from './action-types';
// Store structure.
// Transient: Values that are _not_ saved to the DB (like app lifecycle-flags).
const defaultTransient = Object.freeze( {
isReady: false,
} );
// Persistent: Values that are loaded from the DB.
const defaultPersistent = Object.freeze( {
cart: {},
checkout: {},
product: {},
shop: {},
home: {},
custom_placement: [],
} );
// Reducer logic.
const [ changeTransient, changePersistent ] = createReducerSetters(
defaultTransient,
defaultPersistent
);
const reducer = createReducer( defaultTransient, defaultPersistent, {
[ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) =>
changeTransient( state, payload ),
[ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) =>
changePersistent( state, payload ),
[ ACTION_TYPES.RESET ]: ( state ) => {
const cleanState = changeTransient(
changePersistent( state, defaultPersistent ),
defaultTransient
);
// Keep "read-only" details and initialization flags.
cleanState.isReady = true;
return cleanState;
},
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) =>
changePersistent( state, payload.data ),
} );
export default reducer;

View file

@ -0,0 +1,37 @@
/**
* Resolvers: Handle asynchronous data fetching for the store.
*
* These functions update store state with data from external sources.
* Each resolver corresponds to a specific selector (selector with same name must exist).
* Resolvers are called automatically when selectors request unavailable data.
*
* @file
*/
import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls';
import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
export const resolvers = {
/**
* Retrieve settings from the site's REST API.
*/
*persistentData() {
try {
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
yield dispatch( STORE_NAME ).hydrate( result );
yield dispatch( STORE_NAME ).setIsReady( true );
} catch ( e ) {
yield dispatch( 'core/notices' ).createErrorNotice(
// TODO: Add the module name to the error message.
__(
'Error retrieving Pay Later Messaging config details.',
'woocommerce-paypal-payments'
)
);
}
},
};

View file

@ -0,0 +1,21 @@
/**
* Selectors: Extract specific pieces of state from the store.
*
* These functions provide a consistent interface for accessing store data.
* They allow components to retrieve data without knowing the store structure.
*
* @file
*/
const EMPTY_OBJ = Object.freeze( {} );
const getState = ( state ) => state || EMPTY_OBJ;
export const persistentData = ( state ) => {
return getState( state ).data || EMPTY_OBJ;
};
export const transientData = ( state ) => {
const { data, ...transientState } = getState( state );
return transientState || EMPTY_OBJ;
};

View file

@ -8,6 +8,7 @@ import * as actions from './actions';
import * as hooks from './hooks';
import { resolvers } from './resolvers';
import { controls } from './controls';
import { initTodoSync } from '../sync/todo-state-sync';
/**
* Initializes and registers the settings store with WordPress data layer.
@ -26,6 +27,9 @@ export const initStore = () => {
register( store );
// Initialize todo sync after store registration. Potentially should be moved elsewhere.
initTodoSync();
return Boolean( wp.data.select( STORE_NAME ) );
};

View file

@ -21,25 +21,28 @@ const useHooks = () => {
// Persistent accessors.
const [ invoicePrefix, setInvoicePrefix ] =
usePersistent( 'invoicePrefix' );
const [ brandName, setBrandName ] = usePersistent( 'brandName' );
const [ softDescriptor, setSoftDescriptor ] =
usePersistent( 'softDescriptor' );
const [ subtotalAdjustment, setSubtotalAdjustment ] =
usePersistent( 'subtotalAdjustment' );
const [ landingPage, setLandingPage ] = usePersistent( 'landingPage' );
const [ buttonLanguage, setButtonLanguage ] =
usePersistent( 'buttonLanguage' );
const [ authorizeOnly, setAuthorizeOnly ] =
usePersistent( 'authorizeOnly' );
const [ captureVirtualOnlyOrders, setCaptureVirtualOnlyOrders ] =
usePersistent( 'captureVirtualOnlyOrders' );
usePersistent( 'captureVirtualOrders' );
const [ savePaypalAndVenmo, setSavePaypalAndVenmo ] =
usePersistent( 'savePaypalAndVenmo' );
const [ saveCardDetails, setSaveCardDetails ] =
usePersistent( 'saveCardDetails' );
const [ payNowExperience, setPayNowExperience ] =
usePersistent( 'payNowExperience' );
const [ logging, setLogging ] = usePersistent( 'logging' );
const [ subtotalAdjustment, setSubtotalAdjustment ] =
usePersistent( 'subtotalAdjustment' );
const [ brandName, setBrandName ] = usePersistent( 'brandName' );
const [ softDescriptor, setSoftDescriptor ] =
usePersistent( 'softDescriptor' );
const [ landingPage, setLandingPage ] = usePersistent( 'landingPage' );
const [ buttonLanguage, setButtonLanguage ] =
usePersistent( 'buttonLanguage' );
usePersistent( 'enablePayNow' );
const [ logging, setLogging ] = usePersistent( 'enableLogging' );
const [ disabledCards, setDisabledCards ] =
usePersistent( 'disabledCards' );

View file

@ -25,18 +25,25 @@ const defaultTransient = Object.freeze( {
* These represent the core PayPal payment settings configuration.
*/
const defaultPersistent = Object.freeze( {
// String values.
invoicePrefix: '', // Prefix for PayPal invoice IDs
authorizeOnly: false, // Whether to only authorize payments initially
captureVirtualOnlyOrders: false, // Auto-capture virtual-only orders
savePaypalAndVenmo: false, // Enable PayPal & Venmo vaulting
saveCardDetails: false, // Enable card vaulting
payNowExperience: false, // Enable Pay Now experience
logging: false, // Enable debug logging
subtotalAdjustment: 'skip_details', // Handling for subtotal mismatches
brandName: '', // Merchant brand name for PayPal
softDescriptor: '', // Payment descriptor on statements
landingPage: 'any', // PayPal checkout landing page
// Limited value strings.
subtotalAdjustment: 'no_details', // [correction|no_details] Handling for subtotal mismatches
landingPage: 'any', // [any|login|guest_checkout] PayPal checkout landing page
buttonLanguage: '', // Language for PayPal buttons
// Boolean flags.
authorizeOnly: false, // Whether to only authorize payments initially
captureVirtualOrders: false, // Auto-capture virtual-only orders
savePaypalAndVenmo: false, // Enable PayPal & Venmo vaulting
saveCardDetails: false, // Enable card vaulting
enablePayNow: false, // Enable Pay Now experience
enableLogging: false, // Enable debug logging
// String arrays.
disabledCards: [], // Disabled credit card types
} );

View file

@ -0,0 +1,67 @@
import { subscribe, select, dispatch } from '@wordpress/data';
const TODO_TRIGGERS = {
'ppcp-applepay': 'enable_apple_pay',
'ppcp-googlepay': 'enable_google_pay',
'ppcp-axo-gateway': 'enable_fastlane',
'ppcp-card-button-gateway': 'enable_credit_debit_cards',
};
/**
* Initialize todo synchronization
*/
export const initTodoSync = () => {
let previousPaymentState = null;
let isProcessing = false;
subscribe( () => {
if ( isProcessing ) {
return;
}
isProcessing = true;
try {
const paymentState = select( 'wc/paypal/payment' ).persistentData();
const todosState = select( 'wc/paypal/todos' ).getTodos();
// Skip if states haven't been initialized yet
if ( ! paymentState || ! todosState || ! previousPaymentState ) {
previousPaymentState = paymentState;
return;
}
Object.entries( TODO_TRIGGERS ).forEach(
( [ paymentMethod, todoId ] ) => {
const wasEnabled =
previousPaymentState[ paymentMethod ]?.enabled;
const isEnabled = paymentState[ paymentMethod ]?.enabled;
if ( wasEnabled !== isEnabled ) {
const todoToUpdate = todosState.find(
( todo ) => todo.id === todoId
);
if ( todoToUpdate ) {
const updatedTodos = todosState.map( ( todo ) =>
todo.id === todoId
? { ...todo, isCompleted: isEnabled }
: todo
);
dispatch( 'wc/paypal/todos' ).setTodos(
updatedTodos
);
}
}
}
);
previousPaymentState = paymentState;
} catch ( error ) {
console.error( 'Error in todo sync:', error );
} finally {
isProcessing = false;
}
} );
};

View file

@ -0,0 +1,16 @@
/**
* Action Types: Define unique identifiers for actions across all store modules.
*
* @file
*/
export default {
// Transient data
SET_TRANSIENT: 'TODOS:SET_TRANSIENT',
// Persistent data
SET_TODOS: 'TODOS:SET_TODOS',
// Controls
DO_FETCH_TODOS: 'TODOS:DO_FETCH_TODOS',
};

View file

@ -0,0 +1,24 @@
/**
* Action Creators: Define functions to create action objects.
*
* These functions update state or trigger side effects (e.g., async operations).
* Actions are categorized as Transient, Persistent, or Side effect.
*
* @file
*/
import ACTION_TYPES from './action-types';
export const setIsReady = ( isReady ) => ( {
type: ACTION_TYPES.SET_TRANSIENT,
payload: { isReady },
} );
export const setTodos = ( todos ) => ( {
type: ACTION_TYPES.SET_TODOS,
payload: todos,
} );
export const fetchTodos = function* () {
yield { type: ACTION_TYPES.DO_FETCH_TODOS };
};

View file

@ -0,0 +1,8 @@
/**
* Constants: Define store configuration values.
*
* @file
*/
export const STORE_NAME = 'wc/paypal/todos';
export const REST_PATH = '/wc/v3/wc_paypal/todos';

View file

@ -0,0 +1,22 @@
/**
* Controls: Implement side effects, typically asynchronous operations.
*
* Controls use ACTION_TYPES keys as identifiers.
* They are triggered by corresponding actions and handle external interactions.
*
* @file
*/
import apiFetch from '@wordpress/api-fetch';
import { REST_PATH } from './constants';
import ACTION_TYPES from './action-types';
export const controls = {
async [ ACTION_TYPES.DO_FETCH_TODOS ]() {
const response = await apiFetch( {
path: REST_PATH,
method: 'GET',
} );
return response?.data || [];
},
};

View file

@ -0,0 +1,33 @@
/**
* Hooks: Provide the main API for components to interact with the store.
*
* These encapsulate store interactions, offering a consistent interface.
* Hooks simplify data access and manipulation for components.
*
* @file
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { STORE_NAME } from './constants';
const useTransient = ( key ) =>
useSelect(
( select ) => select( STORE_NAME ).transientData()?.[ key ],
[ key ]
);
export const useTodos = () => {
const todos = useSelect(
( select ) => select( STORE_NAME ).getTodos(),
[]
);
const isReady = useTransient( 'isReady' );
const { fetchTodos } = useDispatch( STORE_NAME );
return {
todos,
isReady,
fetchTodos,
};
};

View file

@ -0,0 +1,32 @@
import { createReduxStore, register } from '@wordpress/data';
import { controls as wpControls } from '@wordpress/data-controls';
import { STORE_NAME } from './constants';
import reducer from './reducer';
import * as selectors from './selectors';
import * as actions from './actions';
import * as hooks from './hooks';
import { resolvers } from './resolvers';
import { controls } from './controls';
/**
* Initializes and registers the todos store with WordPress data layer.
* Combines custom controls with WordPress data controls.
*
* @return {boolean} True if initialization succeeded, false otherwise.
*/
export const initStore = () => {
const store = createReduxStore( STORE_NAME, {
reducer,
controls: { ...wpControls, ...controls },
actions,
selectors,
resolvers,
} );
register( store );
return Boolean( wp.data.select( STORE_NAME ) );
};
export { hooks, selectors, STORE_NAME };

View file

@ -0,0 +1,90 @@
/**
* Reducer: Defines store structure and state updates for todos module.
*
* Manages both transient (temporary) and persistent (saved) state.
* The initial state must define all properties, as dynamic additions are not supported.
*
* @file
*/
import { createReducer, createReducerSetters } from '../utils';
import ACTION_TYPES from './action-types';
// Store structure.
/**
* Transient: Values that are _not_ saved to the DB (like app lifecycle-flags).
* These reset on page reload.
*/
const defaultTransient = Object.freeze( {
isReady: false,
} );
/**
* Persistent: Values that are loaded from and saved to the DB.
* These represent the core todos configuration.
*/
const defaultPersistent = Object.freeze( {
todos: [],
} );
// Reducer logic.
const [ changeTransient, changePersistent ] = createReducerSetters(
defaultTransient,
defaultPersistent
);
/**
* Reducer implementation mapping actions to state updates.
*/
const reducer = createReducer( defaultTransient, defaultPersistent, {
/**
* Updates temporary state values
*
* @param {Object} state Current state
* @param {Object} payload Update payload
* @return {Object} Updated state
*/
[ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) =>
changeTransient( state, payload ),
/**
* Updates todos list
*
* @param {Object} state Current state
* @param {Object} payload Update payload
* @return {Object} Updated state
*/
[ ACTION_TYPES.SET_TODOS ]: ( state, payload ) => {
return changePersistent( state, { todos: payload } );
},
/**
* Resets state to defaults while maintaining initialization status
*
* @param {Object} state Current state
* @return {Object} Reset state
*/
[ ACTION_TYPES.RESET ]: ( state ) => {
const cleanState = changeTransient(
changePersistent( state, defaultPersistent ),
defaultTransient
);
cleanState.isReady = true; // Keep initialization flag
return cleanState;
},
/**
* Initializes persistent state with data from the server
*
* @param {Object} state Current state
* @param {Object} payload Hydration payload containing server data
* @param {Object} payload.data The todos data to hydrate
* @return {Object} Hydrated state
*/
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) =>
changePersistent( state, payload.data ),
} );
export default reducer;

View file

@ -0,0 +1,35 @@
/**
* Resolvers: Handle asynchronous data fetching for the store.
*
* These functions update store state with data from external sources.
* Each resolver corresponds to a specific selector (selector with same name must exist).
* Resolvers are called automatically when selectors request unavailable data.
*
* @file
*/
import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls';
import { STORE_NAME, REST_PATH } from './constants';
export const resolvers = {
*getTodos() {
try {
const response = yield apiFetch( { path: REST_PATH } );
// Make sure we're accessing the correct part of the response
const todos = response?.data || [];
yield dispatch( STORE_NAME ).setTodos( todos );
yield dispatch( STORE_NAME ).setIsReady( true );
} catch ( e ) {
console.error( 'Resolver error:', e );
yield dispatch( STORE_NAME ).setIsReady( false );
yield dispatch( 'core/notices' ).createErrorNotice(
__( 'Error retrieving todos.', 'woocommerce-paypal-payments' )
);
}
},
};

View file

@ -0,0 +1,28 @@
/**
* Selectors: Extract specific pieces of state from the store.
*
* These functions provide a consistent interface for accessing store data.
* They allow components to retrieve data without knowing the store structure.
*
* @file
*/
const EMPTY_OBJ = Object.freeze( {} );
const EMPTY_ARR = Object.freeze( [] );
const getState = ( state ) => state || EMPTY_OBJ;
export const persistentData = ( state ) => {
return getState( state ).data || EMPTY_OBJ;
};
export const transientData = ( state ) => {
const { data, ...transientState } = getState( state );
return transientState || EMPTY_OBJ;
};
export const getTodos = ( state ) => {
// Access todos directly from state first
const todos = state?.todos || persistentData( state ).todos || EMPTY_ARR;
return todos;
};

View file

@ -2,6 +2,7 @@ import { useCallback } from '@wordpress/element';
import {
CommonHooks,
PayLaterMessagingHooks,
PaymentHooks,
SettingsHooks,
StylingHooks,
@ -13,8 +14,13 @@ export const useSaveSettings = () => {
const { persist: persistPayment } = PaymentHooks.useStore();
const { persist: persistSettings } = SettingsHooks.useStore();
const { persist: persistStyling } = StylingHooks.useStore();
const { persist: persistPayLaterMessaging } =
PayLaterMessagingHooks.useStore();
const persistAll = useCallback( () => {
// Executes onSave on TabPayLaterMessaging component.
document.getElementById( 'configurator-publishButton' )?.click();
withActivity(
'persist-methods',
'Save payment methods',
@ -30,7 +36,18 @@ export const useSaveSettings = () => {
'Save styling details',
persistStyling
);
}, [ persistPayment, persistSettings, persistStyling, withActivity ] );
withActivity(
'persist-pay-later-messaging',
'Save pay later messaging details',
persistPayLaterMessaging
);
}, [
persistPayment,
persistSettings,
persistStyling,
persistPayLaterMessaging,
withActivity,
] );
return { persistAll };
};

View file

@ -2,139 +2,139 @@ export const countryPriceInfo = {
US: {
fixedFee: {
USD: 0.49,
GBP: 0.39,
CAD: 0.59,
AUD: 0.59,
EUR: 0.39,
GBP: 0.39,
CAD: 0.59,
AUD: 0.59,
EUR: 0.39,
},
checkout: 3.49,
plater: 4.99,
ccf: {
percentage: 2.59,
fixedFee: 0.29,
},
plater: 4.99,
ccf: {
percentage: 2.59,
fixedFee: 0.29,
},
dw: {
percentage: 2.59,
fixedFee: 0.29,
},
percentage: 2.59,
fixedFee: 0.29,
},
apm: {
percentage: 2.89,
fixedFee: 0.29,
},
fast: {
percentage: 2.59,
fixedFee: 0.29,
},
percentage: 2.89,
fixedFee: 0.29,
},
fast: {
percentage: 2.59,
fixedFee: 0.29,
},
standardCardFields: 2.99,
},
UK: {
GB: {
fixedFee: {
GPB: 0.3,
USD: 0.3,
CAD: 0.3,
AUD: 0.3,
EUR: 0.35,
USD: 0.3,
CAD: 0.3,
AUD: 0.3,
EUR: 0.35,
},
checkout: 2.9,
plater: 2.9,
plater: 2.9,
ccf: 1.2,
dw: 1.2,
fast: 1.2,
fast: 1.2,
apm: 1.2,
standardCardFields: 1.2,
},
CA: {
fixedFee: {
CAD: 0.3,
USD: 0.3,
GBP: 0.2,
AUD: 0.3,
EUR: 0.35,
USD: 0.3,
GBP: 0.2,
AUD: 0.3,
EUR: 0.35,
},
checkout: 2.9,
ccf: 2.7,
dw: 2.7,
fast: 2.7,
fast: 2.7,
apm: 2.9,
standardCardFields: 2.9,
},
AU: {
fixedFee: {
AUD: 0.3,
USD: 0.3,
GBP: 0.2,
CAD: 0.3,
EUR: 0.35,
USD: 0.3,
GBP: 0.2,
CAD: 0.3,
EUR: 0.35,
},
checkout: 2.6,
plater: 2.6,
plater: 2.6,
ccf: 1.75,
dw: 1.75,
fast: 1.75,
fast: 1.75,
apm: 2.6,
standardCardFields: 2.6,
},
FR: {
fixedFee: {
EUR: 0.35,
USD: 0.3,
GBP: 0.3,
CAD: 0.3,
AUD: 0.3,
USD: 0.3,
GBP: 0.3,
CAD: 0.3,
AUD: 0.3,
},
checkout: 2.9,
plater: 2.9,
plater: 2.9,
ccf: 1.2,
dw: 1.2,
fast: 1.2,
fast: 1.2,
apm: 1.2,
standardCardFields: 1.2,
},
IT: {
fixedFee: {
EUR: 0.35,
USD: 0.3,
GBP: 0.3,
CAD: 0.3,
AUD: 0.3,
EUR: 0.35,
USD: 0.3,
GBP: 0.3,
CAD: 0.3,
AUD: 0.3,
},
checkout: 3.4,
plater: 3.4,
plater: 3.4,
ccf: 1.2,
dw: 1.2,
fast: 1.2,
fast: 1.2,
apm: 1.2,
standardCardFields: 1.2,
},
DE: {
fixedFee: {
EUR: 0.39,
USD: 0.49,
GBP: 0.29,
CAD: 0.59,
AUD: 0.59,
USD: 0.49,
GBP: 0.29,
CAD: 0.59,
AUD: 0.59,
},
checkout: 2.99,
plater: 2.99,
plater: 2.99,
ccf: 2.99,
dw: 2.99,
fast: 2.99,
fast: 2.99,
apm: 2.99,
standardCardFields: 2.99,
},
ES: {
fixedFee: {
EUR: 0.35,
USD: 0.3,
GBP: 0.3,
CAD: 0.3,
AUD: 0.3,
EUR: 0.35,
USD: 0.3,
GBP: 0.3,
CAD: 0.3,
AUD: 0.3,
},
checkout: 2.9,
plater: 2.9,
plater: 2.9,
ccf: 1.2,
dw: 1.2,
fast: 1.2,
fast: 1.2,
apm: 1.2,
standardCardFields: 1.2,
},

View file

@ -15,21 +15,24 @@ use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile;
use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings;
use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel;
use WooCommerce\PayPalCommerce\Settings\Data\StylingSettings;
use WooCommerce\PayPalCommerce\Settings\Data\TodosModel;
use WooCommerce\PayPalCommerce\Settings\Endpoint\AuthenticationRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\PayLaterMessagingEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\PaymentRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\WebhookSettingsEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\SettingsRestEndpoint;
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\ConnectionUrlGenerator;
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Settings\Endpoint\StylingRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Data\StylingSettings;
use WooCommerce\PayPalCommerce\Settings\Service\DataSanitizer;
return array(
@ -79,6 +82,20 @@ return array(
'settings.data.payment' => static function ( ContainerInterface $container ) : PaymentSettings {
return new PaymentSettings();
},
'settings.data.settings' => static function ( ContainerInterface $container ) : SettingsModel {
return new SettingsModel(
$container->get( 'settings.service.sanitizer' )
);
},
/**
* Checks if valid merchant connection details are stored in the DB.
*/
'settings.flag.is-connected' => static function ( ContainerInterface $container ) : bool {
$data = $container->get( 'settings.data.general' );
assert( $data instanceof GeneralSettings );
return $data->is_merchant_connected();
},
'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint {
return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) );
},
@ -118,6 +135,17 @@ return array(
$container->get( 'webhook.status.simulation' )
);
},
'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 {
return new SettingsRestEndpoint(
$container->get( 'settings.data.settings' )
);
},
'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array {
return array(
'AR',
@ -222,13 +250,12 @@ return array(
$container->get( 'api.merchant_id' ) !== ''
);
},
'settings.rest.settings' => static function( ContainerInterface $container ): SettingsRestEndpoint {
return new SettingsRestEndpoint(
$container->get( 'settings.data.settings' ),
$container->get( 'woocommerce.logger.woocommerce' ),
'settings.rest.todos' => static function ( ContainerInterface $container ) : TodosRestEndpoint {
return new TodosRestEndpoint(
$container->get( 'settings.data.todos' ),
);
},
'settings.data.settings' => static function() : SettingsModel {
return new SettingsModel();
'settings.data.todos' => static function ( ContainerInterface $container ) : TodosModel {
return new TodosModel();
},
);

View file

@ -9,81 +9,319 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Data;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use RuntimeException;
use WooCommerce\PayPalCommerce\Settings\Service\DataSanitizer;
/**
* Class SettingsModel
*
* Handles the storage and retrieval of PayPal Commerce settings in WordPress options table.
* Provides methods to get and update settings with proper type casting and default values.
*/
class SettingsModel {
class SettingsModel extends AbstractDataModel {
/**
* WordPress option name for storing the settings.
* Option key where settings are stored.
*
* @var string
*/
const OPTION_NAME = 'ppcp_settings';
protected const OPTION_KEY = 'woocommerce-ppcp-data-settings';
/**
* Retrieves the formatted settings from WordPress options.
* Valid options for subtotal adjustment.
*
* Loads the raw settings from wp_options table and formats them into a
* standardized array structure with proper type casting.
*
* @return array The formatted settings array.
* @var array
*/
public function get() : array {
$settings = get_option( self::OPTION_NAME, array() );
public const SUBTOTAL_ADJUSTMENT_OPTIONS = array( 'no_details', 'correction' );
$formatted = array(
'invoicePrefix' => $settings['invoice_prefix'] ?? '',
'authorizeOnly' => (bool) ( $settings['authorize_only'] ?? false ),
'captureVirtualOnlyOrders' => (bool) ( $settings['capture_virtual_only_orders'] ?? false ),
'savePaypalAndVenmo' => (bool) ( $settings['save_paypal_and_venmo'] ?? false ),
'saveCardDetails' => (bool) ( $settings['save_credit_card_and_debit_card'] ?? false ),
'payNowExperience' => (bool) ( $settings['pay_now_experience'] ?? false ),
'logging' => (bool) ( $settings['logging'] ?? false ),
'subtotalAdjustment' => $settings['subtotal_mismatch_fallback'] ?? null,
'brandName' => $settings['brand_name'] ?? '',
'softDescriptor' => $settings['soft_descriptor'] ?? '',
'landingPage' => $settings['paypal_landing_page'] ?? null,
'buttonLanguage' => $settings['button_language'] ?? '',
);
/**
* Valid options for landing page.
*
* @var array
*/
public const LANDING_PAGE_OPTIONS = array( 'any', 'login', 'guest_checkout' );
return $formatted;
/**
* Data sanitizer service.
*
* @var DataSanitizer
*/
protected DataSanitizer $sanitizer;
/**
* Constructor.
*
* @param DataSanitizer $sanitizer Data sanitizer service.
* @throws RuntimeException If the OPTION_KEY is not defined in the child class.
*/
public function __construct( DataSanitizer $sanitizer ) {
$this->sanitizer = $sanitizer;
parent::__construct();
}
/**
* Updates the settings in WordPress options.
* Get default values for the model.
*
* Converts the provided data array from camelCase to snake_case format
* and saves it to wp_options table. Throws an exception if update fails.
*
* @param array $data The settings data to update.
* @return void
* @throws RuntimeException When the settings update fails.
* @return array
*/
public function update( array $data ) : void {
$settings = array(
'invoice_prefix' => $data['invoicePrefix'] ?? '',
'authorize_only' => (bool) ( $data['authorizeOnly'] ?? false ),
'capture_virtual_only_orders' => (bool) ( $data['captureVirtualOnlyOrders'] ?? false ),
'save_paypal_and_venmo' => (bool) ( $data['savePaypalAndVenmo'] ?? false ),
'save_credit_card_and_debit_card' => (bool) ( $data['saveCardDetails'] ?? false ),
'pay_now_experience' => (bool) ( $data['payNowExperience'] ?? false ),
'logging' => (bool) ( $data['logging'] ?? false ),
'subtotal_mismatch_fallback' => $data['subtotalAdjustment'] ?? null,
'brand_name' => $data['brandName'] ?? '',
'soft_descriptor' => $data['softDescriptor'] ?? '',
'paypal_landing_page' => $data['landingPage'] ?? null,
'button_language' => $data['buttonLanguage'] ?? '',
protected function get_defaults() : array {
return array(
// Free-form string values.
'invoice_prefix' => '',
'brand_name' => '',
'soft_descriptor' => '',
// Enum-type string values.
'subtotal_adjustment' => 'skip_details', // Options: [correction|no_details].
'landing_page' => 'any', // Options: [any|login|guest_checkout].
'button_language' => '', // empty or a 2-letter language code.
// Boolean flags.
'authorize_only' => false,
'capture_virtual_orders' => false,
'save_paypal_and_venmo' => false,
'save_card_details' => false,
'enable_pay_now' => false,
'enable_logging' => false,
// Array of string values.
'disabled_cards' => array(),
);
}
$result = update_option( self::OPTION_NAME, $settings );
/**
* Gets the invoice prefix.
*
* @return string The invoice prefix.
*/
public function get_invoice_prefix() : string {
return $this->data['invoice_prefix'];
}
if ( ! $result ) {
throw new RuntimeException( 'Failed to update settings' );
}
/**
* Sets the invoice prefix.
*
* @param string $prefix The invoice prefix to set.
*/
public function set_invoice_prefix( string $prefix ) : void {
$this->data['invoice_prefix'] = $this->sanitizer->sanitize_text( $prefix );
}
/**
* Gets the brand name.
*
* @return string The brand name.
*/
public function get_brand_name() : string {
return $this->data['brand_name'];
}
/**
* Sets the brand name.
*
* @param string $name The brand name to set.
*/
public function set_brand_name( string $name ) : void {
$this->data['brand_name'] = $this->sanitizer->sanitize_text( $name );
}
/**
* Gets the soft descriptor.
*
* @return string The soft descriptor.
*/
public function get_soft_descriptor() : string {
return $this->data['soft_descriptor'];
}
/**
* Sets the soft descriptor.
*
* @param string $descriptor The soft descriptor to set.
*/
public function set_soft_descriptor( string $descriptor ) : void {
$this->data['soft_descriptor'] = $this->sanitizer->sanitize_text( $descriptor );
}
/**
* Gets the subtotal adjustment setting.
*
* @return string The subtotal adjustment setting.
*/
public function get_subtotal_adjustment() : string {
return $this->data['subtotal_adjustment'];
}
/**
* Sets the subtotal adjustment setting.
*
* @param string $adjustment The subtotal adjustment to set.
*/
public function set_subtotal_adjustment( string $adjustment ) : void {
$this->data['subtotal_adjustment'] = $this->sanitizer->sanitize_enum( $adjustment, self::SUBTOTAL_ADJUSTMENT_OPTIONS );
}
/**
* Gets the landing page setting.
*
* @return string The landing page setting.
*/
public function get_landing_page() : string {
return $this->data['landing_page'];
}
/**
* Sets the landing page setting.
*
* @param string $page The landing page to set.
*/
public function set_landing_page( string $page ) : void {
$this->data['landing_page'] = $this->sanitizer->sanitize_enum( $page, self::LANDING_PAGE_OPTIONS );
}
/**
* Gets the button language setting.
*
* @return string The button language.
*/
public function get_button_language() : string {
return $this->data['button_language'];
}
/**
* Sets the button language.
*
* @param string $language The button language to set.
*/
public function set_button_language( string $language ) : void {
$this->data['button_language'] = $this->sanitizer->sanitize_text( $language );
}
/**
* Gets the authorize only setting.
*
* @return bool True if authorize only is enabled, false otherwise.
*/
public function get_authorize_only() : bool {
return $this->data['authorize_only'];
}
/**
* Sets the authorize only setting.
*
* @param bool $authorize Whether to enable authorize only.
*/
public function set_authorize_only( bool $authorize ) : void {
$this->data['authorize_only'] = $this->sanitizer->sanitize_bool( $authorize );
}
/**
* Gets the capture virtual orders setting.
*
* @return bool True if capturing virtual orders is enabled, false otherwise.
*/
public function get_capture_virtual_orders() : bool {
return $this->data['capture_virtual_orders'];
}
/**
* Sets the capture virtual orders setting.
*
* @param bool $capture Whether to capture virtual orders.
*/
public function set_capture_virtual_orders( bool $capture ) : void {
$this->data['capture_virtual_orders'] = $this->sanitizer->sanitize_bool( $capture );
}
/**
* Gets the save PayPal and Venmo setting.
*
* @return bool True if saving PayPal and Venmo is enabled, false otherwise.
*/
public function get_save_paypal_and_venmo() : bool {
return $this->data['save_paypal_and_venmo'];
}
/**
* Sets the save PayPal and Venmo setting.
*
* @param bool $save Whether to save PayPal and Venmo.
*/
public function set_save_paypal_and_venmo( bool $save ) : void {
$this->data['save_paypal_and_venmo'] = $this->sanitizer->sanitize_bool( $save );
}
/**
* Gets the save card details setting.
*
* @return bool True if saving card details is enabled, false otherwise.
*/
public function get_save_card_details() : bool {
return $this->data['save_card_details'];
}
/**
* Sets the save card details setting.
*
* @param bool $save Whether to save card details.
*/
public function set_save_card_details( bool $save ) : void {
$this->data['save_card_details'] = $this->sanitizer->sanitize_bool( $save );
}
/**
* Gets the enable Pay Now setting.
*
* @return bool True if Pay Now is enabled, false otherwise.
*/
public function get_enable_pay_now() : bool {
return $this->data['enable_pay_now'];
}
/**
* Sets the enable Pay Now setting.
*
* @param bool $enable Whether to enable Pay Now.
*/
public function set_enable_pay_now( bool $enable ) : void {
$this->data['enable_pay_now'] = $this->sanitizer->sanitize_bool( $enable );
}
/**
* Gets the enable logging setting.
*
* @return bool True if logging is enabled, false otherwise.
*/
public function get_enable_logging() : bool {
return $this->data['enable_logging'];
}
/**
* Sets the enable logging setting.
*
* @param bool $enable Whether to enable logging.
*/
public function set_enable_logging( bool $enable ) : void {
$this->data['enable_logging'] = $this->sanitizer->sanitize_bool( $enable );
}
/**
* Gets the disabled cards.
*
* @return array The array of disabled cards.
*/
public function get_disabled_cards() : array {
return $this->data['disabled_cards'];
}
/**
* Sets the disabled cards.
*
* @param array $cards The array of cards to disable.
*/
public function set_disabled_cards( array $cards ) : void {
$this->data['disabled_cards'] = array_map(
array( $this->sanitizer, 'sanitize_text' ),
$cards
);
}
}

View file

@ -0,0 +1,74 @@
<?php
/**
* PayPal Commerce Todos Model
*
* @package WooCommerce\PayPalCommerce\Settings\Data
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Data;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class TodosModel
*
* Handles the storage and retrieval of task completion states in WordPress options table.
* Provides methods to get and update completion states with proper type casting.
*/
class TodosModel {
/**
* WordPress option name for storing the completion states.
*
* @var string
*/
protected const OPTION_NAME = 'ppcp_todos';
/**
* Retrieves the formatted completion states from WordPress options.
*
* Loads the raw completion states from wp_options table and formats them into a
* standardized array structure with proper type casting.
*
* @return array The formatted completion states array.
*/
public function get() : array {
$completion_states = get_option( self::OPTION_NAME, array() );
return array_map(
/**
* Ensures the task completion states are boolean values.
*
* @param mixed $state value to sanitize, as stored in the DB.
*/
static fn( $state ) => (bool) $state,
$completion_states
);
}
/**
* Updates the completion states in WordPress options.
*
* Converts the provided data array and saves it to wp_options table.
* Throws an exception if update fails.
*
* @param array $states Array of task IDs and their completion states.
* @return void
* @throws RuntimeException When the completion states update fails.
*/
public function update( array $states ) : void {
$completion_states = array_map(
static function ( $state ) {
return (bool) $state;
},
$states
);
$result = update_option( self::OPTION_NAME, $completion_states );
if ( ! $result ) {
throw new RuntimeException( 'Failed to update todo completion states' );
}
}
}

View file

@ -218,7 +218,7 @@ class CommonRestEndpoint extends RestEndpoint {
if ( $this->settings->is_merchant_connected() ) {
$extra_data['features'] = apply_filters(
'woocommerce_paypal_payments_rest_common_merchant_data',
'woocommerce_paypal_payments_rest_common_merchant_features',
array(),
);
}

View file

@ -0,0 +1,110 @@
<?php
/**
* REST endpoint to manage the Pay Later Messaging configurator page.
*
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Factory\ConfigFactory;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
/**
* REST controller for the "Pay Later Messaging" settings tab.
*
* This API acts as the intermediary between the "external world" and our
* internal data model.
*/
class PayLaterMessagingEndpoint extends RestEndpoint {
/**
* The base path for this REST controller.
*
* @var string
*/
protected $rest_base = 'pay_later_messaging';
/**
* The settings.
*
* @var Settings
*/
protected $settings;
/**
* Save config handler.
*
* @var SaveConfig
*/
private $save_config;
/**
* PayLaterMessagingEndpoint constructor.
*
* @param Settings $settings The settings.
* @param SaveConfig $save_config Save config handler.
*/
public function __construct( Settings $settings, SaveConfig $save_config ) {
$this->settings = $settings;
$this->save_config = $save_config;
}
/**
* Configure REST API routes.
*/
public function register_routes() : void {
/**
* GET wc/v3/wc_paypal/pay_later_messaging
*/
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_details' ),
'permission_callback' => array( $this, 'check_permission' ),
)
);
/**
* POST wc/v3/wc_paypal/pay_later_messaging
*/
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_details' ),
'permission_callback' => array( $this, 'check_permission' ),
)
);
}
/**
* Returns Pay Later Messaging configuration details.
*
* @return WP_REST_Response The current payment methods details.
*/
public function get_details() : WP_REST_Response {
return $this->return_success( ( new ConfigFactory() )->from_settings( $this->settings ) );
}
/**
* Updates Pay Later Messaging configuration details based on the request.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return WP_REST_Response The updated Pay Later Messaging configuration details.
*/
public function update_details( WP_REST_Request $request ) : WP_REST_Response {
$this->save_config->save_config( $request->get_json_params() );
return $this->get_details();
}
}

View file

@ -576,7 +576,7 @@ class PaymentRestEndpoint extends RestEndpoint {
'OXXO is a Mexican chain of convenience stores. *Get PayPal account permission to use OXXO payment functionality by contacting us at (+52) 8009250304',
'woocommerce-paypal-payments'
),
'icon' => '',
'icon' => 'payment-method-oxxo',
'itemTitle' => __( 'OXXO', 'woocommerce-paypal-payments' ),
'itemDescription' => __(
'OXXO is a Mexican chain of convenience stores. *Get PayPal account permission to use OXXO payment functionality by contacting us at (+52) 8009250304',
@ -669,7 +669,7 @@ class PaymentRestEndpoint extends RestEndpoint {
$gateway_settings[ $key ] = array(
'enabled' => 'yes' === $gateway->enabled,
'title' => $gateway->get_title(),
'title' => str_replace( '&amp;', '&', $gateway->get_title() ),
'description' => $gateway->get_description(),
'id' => $this->gateways()[ $key ]['id'] ?? $key,
'icon' => $this->gateways()[ $key ]['icon'] ?? '',

View file

@ -99,6 +99,7 @@ abstract class RestEndpoint extends WC_REST_Controller {
$source_key = $details['js_name'] ?? '';
$sanitation_cb = $details['sanitize'] ?? null;
// Skip missing values, skip "null" values, skip "read_only" values.
if (
! $source_key
|| ! isset( $params[ $source_key ] )
@ -111,8 +112,11 @@ abstract class RestEndpoint extends WC_REST_Controller {
if ( null === $sanitation_cb ) {
$sanitized[ $key ] = $value;
} elseif ( is_string( $sanitation_cb ) && method_exists( $this, $sanitation_cb ) ) {
$sanitized[ $key ] = $this->{$sanitation_cb}( $value, $key );
continue;
}
if ( is_string( $sanitation_cb ) && method_exists( $this, $sanitation_cb ) ) {
$sanitized[ $key ] = $this->{$sanitation_cb}( $value );
} elseif ( is_callable( $sanitation_cb ) ) {
$sanitized[ $key ] = $sanitation_cb( $value, $key );
}
@ -155,10 +159,11 @@ abstract class RestEndpoint extends WC_REST_Controller {
*
* @param mixed $value The value to sanitize.
*
* @return bool|null The boolean value, or null if not set.
* @return bool The boolean value.
* @todo Switch to the DataSanitizer class.
*/
public function to_boolean( $value ) : ?bool {
return $value !== null ? (bool) $value : null;
public function to_boolean( $value ) : bool {
return (bool) $value;
}
/**
@ -166,13 +171,10 @@ abstract class RestEndpoint extends WC_REST_Controller {
*
* @param mixed $value The value to sanitize.
*
* @return int|float|null The numeric value, or null if not set.
* @return int The numeric value.
* @todo Switch to the DataSanitizer class.
*/
public function to_number( $value ) {
if ( $value !== null ) {
$value = is_numeric( $value ) ? $value + 0 : null;
}
return $value;
public function to_number( $value ) : int {
return (int) $value;
}
}

View file

@ -8,14 +8,14 @@
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
*/
declare(strict_types=1);
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel;
use Psr\Log\LoggerInterface;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel;
/**
* Class SettingsRestEndpoint
@ -25,18 +25,11 @@ use WP_REST_Response;
class SettingsRestEndpoint extends RestEndpoint {
/**
* The REST API endpoint base.
* The base path for this REST controller.
*
* @var string
*/
private const ENDPOINT = 'settings';
/**
* The REST API namespace.
*
* @var string
*/
protected $namespace = 'wc/v3/wc_paypal';
protected $rest_base = 'settings';
/**
* The settings model instance.
@ -46,42 +39,87 @@ class SettingsRestEndpoint extends RestEndpoint {
private SettingsModel $settings;
/**
* The logger instance.
* Field mapping for request to profile transformation.
*
* @var LoggerInterface
* @var array
*/
private LoggerInterface $logger;
private array $field_map = array(
'invoice_prefix' => array(
'js_name' => 'invoicePrefix',
),
'brand_name' => array(
'js_name' => 'brandName',
),
'soft_descriptor' => array(
'js_name' => 'softDescriptor',
),
'subtotal_adjustment' => array(
'js_name' => 'subtotalAdjustment',
),
'landing_page' => array(
'js_name' => 'landingPage',
),
'button_language' => array(
'js_name' => 'buttonLanguage',
),
'authorize_only' => array(
'js_name' => 'authorizeOnly',
'sanitize' => 'to_boolean',
),
'capture_virtual_orders' => array(
'js_name' => 'captureVirtualOrders',
'sanitize' => 'to_boolean',
),
'save_paypal_and_venmo' => array(
'js_name' => 'savePaypalAndVenmo',
'sanitize' => 'to_boolean',
),
'save_card_details' => array(
'js_name' => 'saveCardDetails',
'sanitize' => 'to_boolean',
),
'enable_pay_now' => array(
'js_name' => 'enablePayNow',
'sanitize' => 'to_boolean',
),
'enable_logging' => array(
'js_name' => 'enableLogging',
'sanitize' => 'to_boolean',
),
'disabled_cards' => array(
'js_name' => 'disabledCards',
),
);
/**
* SettingsRestEndpoint constructor.
*
* @param SettingsModel $settings The settings model instance.
* @param LoggerInterface $logger The logger instance.
* @param SettingsModel $settings The settings model instance.
*/
public function __construct(
SettingsModel $settings,
LoggerInterface $logger
) {
public function __construct( SettingsModel $settings ) {
$this->settings = $settings;
$this->logger = $logger;
}
/**
* Registers the REST API routes for settings management.
*/
public function register_routes(): void {
public function register_routes() : void {
/**
* GET wc/v3/wc_paypal/settings
* POST wc/v3/wc_paypal/settings
*/
register_rest_route(
$this->namespace,
'/' . self::ENDPOINT,
'/' . $this->rest_base,
array(
array(
'methods' => 'GET',
'callback' => array( $this, 'get_settings' ),
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_details' ),
'permission_callback' => array( $this, 'check_permission' ),
),
array(
'methods' => 'POST',
'callback' => array( $this, 'update_settings' ),
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'update_details' ),
'permission_callback' => array( $this, 'check_permission' ),
),
)
@ -91,37 +129,15 @@ class SettingsRestEndpoint extends RestEndpoint {
/**
* Retrieves the current settings.
*
* @param WP_REST_Request $request The request instance.
* @return WP_REST_Response The response containing settings data or error details.
* @throws \Exception When encoding settings data fails.
*/
public function get_settings( WP_REST_Request $request ): WP_REST_Response {
try {
// Get settings data.
$data = $this->settings->get();
public function get_details() : WP_REST_Response {
$js_data = $this->sanitize_for_javascript(
$this->settings->to_array(),
$this->field_map
);
// Ensure the data is JSON-encodable.
$encoded = wp_json_encode( $data );
if ( $encoded === false ) {
throw new \Exception( 'Failed to encode settings data: ' . json_last_error_msg() );
}
// Create response with pre-verified JSON data.
$response_data = array(
'success' => true,
'data' => json_decode( $encoded, true ),
);
return new WP_REST_Response( $response_data, 200 );
} catch ( \Exception $error ) {
return new WP_REST_Response(
array(
'success' => false,
'message' => $error->getMessage(),
),
500
);
}
return $this->return_success( $js_data );
}
/**
@ -129,35 +145,16 @@ class SettingsRestEndpoint extends RestEndpoint {
*
* @param WP_REST_Request $request The request instance containing new settings.
* @return WP_REST_Response The response containing updated settings or error details.
* @throws \Exception When encoding updated settings fails.
*/
public function update_settings( WP_REST_Request $request ): WP_REST_Response {
try {
$data = $request->get_json_params();
$this->settings->update( $data );
$updated_data = $this->settings->get();
public function update_details( WP_REST_Request $request ) : WP_REST_Response {
$wp_data = $this->sanitize_for_wordpress(
$request->get_params(),
$this->field_map
);
// Verify JSON encoding.
$encoded = wp_json_encode( $updated_data );
if ( $encoded === false ) {
throw new \Exception( 'Failed to encode updated settings: ' . json_last_error_msg() );
}
$this->settings->from_array( $wp_data );
$this->settings->save();
return new WP_REST_Response(
array(
'success' => true,
'data' => json_decode( $encoded, true ),
),
200
);
} catch ( \Exception $error ) {
return new WP_REST_Response(
array(
'success' => false,
'message' => $error->getMessage(),
),
500
);
}
return $this->get_details();
}
}

View file

@ -0,0 +1,173 @@
<?php
/**
* REST endpoint to manage the things to do items.
*
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
use WP_REST_Response;
use WP_REST_Server;
use WooCommerce\PayPalCommerce\Settings\Data\TodosModel;
/**
* REST controller for the "Things To Do" items in the Overview tab.
*
* This API acts as the intermediary between the "external world" and our
* internal data model. It's responsible for checking eligibility and
* providing configuration data for things to do next.
*/
class TodosRestEndpoint extends RestEndpoint {
/**
* The base path for this REST controller.
*
* @var string
*/
protected $rest_base = 'todos';
/**
* The todos model instance.
*
* @var TodosModel
*/
protected TodosModel $todos;
/**
* Constructor.
*
* @param TodosModel $todos The todos model instance.
*/
public function __construct( TodosModel $todos ) {
$this->todos = $todos;
}
/**
* Configure REST API routes.
*/
public function register_routes(): void {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_todos' ),
'permission_callback' => array( $this, 'check_permission' ),
)
);
}
/**
* Returns the full list of todo definitions with their eligibility conditions.
*
* @return array The array of todo definitions.
*/
protected function get_todo_definitions(): array {
return array(
'enable_fastlane' => array(
'title' => __( 'Enable Fastlane', 'woocommerce-paypal-payments' ),
'description' => __( 'Accelerate your guest checkout with Fastlane by PayPal.', 'woocommerce-paypal-payments' ),
'isEligible' => fn() => true,
'action' => array(
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-card-payments-card',
),
),
'enable_credit_debit_cards' => array(
'title' => __( 'Enable Credit and Debit Cards on your checkout', 'woocommerce-paypal-payments' ),
'description' => __( 'Credit and Debit Cards is now available for Blocks checkout pages.', 'woocommerce-paypal-payments' ),
'isEligible' => fn() => true,
'action' => array(
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-card-payments-card',
),
),
'enable_pay_later_messaging' => array(
'title' => __( 'Enable Pay Later messaging', 'woocommerce-paypal-payments' ),
'description' => __( 'Show Pay Later messaging to boost conversion rate and increase cart size.', 'woocommerce-paypal-payments' ),
'isEligible' => fn() => true,
'action' => array(
'type' => 'tab',
'tab' => 'overview',
'section' => 'pay_later_messaging',
),
),
'configure_paypal_subscription' => array(
'title' => __( 'Configure a PayPal Subscription', 'woocommerce-paypal-payments' ),
'description' => __( 'Connect a subscriptions-type product from WooCommerce with PayPal.', 'woocommerce-paypal-payments' ),
'isEligible' => fn() => true,
'action' => array(
'type' => 'external',
'url' => admin_url( 'edit.php?post_type=product&product_type=subscription' ),
),
),
'register_domain_apple_pay' => array(
'title' => __( 'Register Domain for Apple Pay', 'woocommerce-paypal-payments' ),
'description' => __( 'To enable Apple Pay, you must register your domain with PayPal.', 'woocommerce-paypal-payments' ),
'isEligible' => fn() => true,
'action' => array(
'type' => 'tab',
'tab' => 'overview',
'section' => 'apple_pay',
),
),
'add_digital_wallets_to_account' => array(
'title' => __( 'Add digital wallets to your account', 'woocommerce-paypal-payments' ),
'description' => __( 'Add the ability to accept Apple Pay & Google Pay to your PayPal account.', 'woocommerce-paypal-payments' ),
'isEligible' => fn() => true,
'action' => array(
'type' => 'external',
'url' => 'https://www.paypal.com/businessmanage/account/settings',
),
),
'enable_apple_pay' => array(
'title' => __( 'Enable Apple Pay', 'woocommerce-paypal-payments' ),
'description' => __( 'Allow your buyers to check out via Apple Pay.', 'woocommerce-paypal-payments' ),
'isEligible' => fn() => true,
'action' => array(
'type' => 'tab',
'tab' => 'overview',
'section' => 'apple_pay',
),
),
'enable_google_pay' => array(
'title' => __( 'Enable Google Pay', 'woocommerce-paypal-payments' ),
'description' => __( 'Allow your buyers to check out via Google Pay.', 'woocommerce-paypal-payments' ),
'isEligible' => fn() => true,
'action' => array(
'type' => 'tab',
'tab' => 'overview',
'section' => 'google_pay',
),
),
);
}
/**
* Returns all eligible todo items.
*
* @return WP_REST_Response The response containing eligible todo items.
*/
public function get_todos(): WP_REST_Response {
$completion_states = $this->todos->get();
$todos = array();
foreach ( $this->get_todo_definitions() as $id => $todo ) {
if ( $todo['isEligible']() ) {
$todos[] = array_merge(
array(
'id' => $id,
'isCompleted' => $completion_states[ $id ] ?? false,
),
array_diff_key( $todo, array( 'isEligible' => true ) )
);
}
}
return $this->return_success( $todos );
}
}

View file

@ -74,20 +74,63 @@ class DataSanitizer {
* @param string $default Default value.
* @return string Sanitized string.
*/
protected function sanitize_text( $value, string $default = '' ) : string {
public function sanitize_text( $value, string $default = '' ) : string {
return sanitize_text_field( $value ?? $default );
}
/**
* Helper. Ensures the matches one of the provided enumerations.
*
* The comparison is case-insensitive, if no valid default is given, the
* first $valid_values entry is returned on failure.
*
* @param mixed $value Value to sanitize.
* @param string[] $valid_values List of allowed return values. Must use ASCII-only characters.
* @param string $default Default value.
* @return string Sanitized string.
*/
public function sanitize_enum( $value, array $valid_values, string $default = '' ) : string {
if ( empty( $valid_values ) ) {
return $default;
}
$value = $this->sanitize_text( $value );
$match = $this->find_enum_value( $value, $valid_values );
if ( $match ) {
return $match;
}
$default_match = $this->find_enum_value( $default, $valid_values );
if ( $default_match ) {
return $default_match;
}
return $valid_values[0];
}
/**
* Helper. Ensures the value is a boolean.
*
* @param mixed $value Value to sanitize.
* @return bool Sanitized boolean.
*/
protected function sanitize_bool( $value ) : bool {
public function sanitize_bool( $value ) : bool {
return filter_var( $value, FILTER_VALIDATE_BOOLEAN );
}
/**
* Helper. Ensures the value is an integer.
*
* Attention: When passing a non-integer value (like 12.5 or "12a") the
* function will return 0.
*
* @param mixed $value Value to sanitize.
* @return int Sanitized integer.
*/
public function sanitize_int( $value ) : int {
return (int) filter_var( $value, FILTER_VALIDATE_INT );
}
/**
* Helper. Ensures the value is an array and all items are sanitized.
*
@ -95,11 +138,30 @@ class DataSanitizer {
* @param callable $sanitize_callback Callback to sanitize each item in the array.
* @return array Array with sanitized items.
*/
protected function sanitize_array( ?array $array, callable $sanitize_callback ) : array {
public function sanitize_array( ?array $array, callable $sanitize_callback ) : array {
if ( ! is_array( $array ) ) {
return array();
}
return array_map( $sanitize_callback, $array );
}
/**
* Helper function to find a case-insensitive match in the valid values array.
*
* @param string $value Value to find.
* @param string[] $valid_values List of allowed values.
* @return string|null Matching value if found, null otherwise.
*/
private function find_enum_value( string $value, array $valid_values ) : ?string {
foreach ( $valid_values as $valid_value ) {
// Compare both strings case-insensitive and binary safe.
// Note: This function is safe for ASCII but can fail for unicode-characters.
if ( 0 === strcasecmp( $value, $valid_value ) ) {
return $valid_value;
}
}
return null;
}
}

View file

@ -185,7 +185,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
'wcPaymentsTabUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout' ),
'debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'isPayLaterConfiguratorAvailable' => $is_pay_later_configurator_available,
'storeCountry' => $container->get( 'wcgateway.store-country' ),
'storeCountry' => $container->get( 'wcgateway.store-country' ),
);
if ( $is_pay_later_configurator_available ) {
@ -237,6 +237,8 @@ class SettingsModule implements ServiceModule, ExecutableModule {
'payment' => $container->get( 'settings.rest.payment' ),
'settings' => $container->get( 'settings.rest.settings' ),
'styling' => $container->get( 'settings.rest.styling' ),
'todos' => $container->get( 'settings.rest.todos' ),
'pay_later_messaging' => $container->get( 'settings.rest.pay_later_messaging' ),
);
foreach ( $endpoints as $endpoint ) {
@ -298,7 +300,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
assert( $dcc_applies instanceof DCCApplies );
// Unset BCDC if merchant is eligible for ACDC.
if ( $dcc_product_status->dcc_is_active() && ! $container->get( 'wcgateway.settings.allow_card_button_gateway' ) ) {
if ( $dcc_product_status->is_active() && ! $container->get( 'wcgateway.settings.allow_card_button_gateway' ) ) {
unset( $payment_methods[ CardButtonGateway::ID ] );
}
@ -318,7 +320,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
}
// Unset Fastlane if store location is not United States or merchant is not eligible for ACDC.
if ( $container->get( 'api.shop.country' ) !== 'US' || ! $dcc_product_status->dcc_is_active() ) {
if ( $container->get( 'api.shop.country' ) !== 'US' || ! $dcc_product_status->is_active() ) {
unset( $payment_methods['ppcp-axo-gateway'] );
}

View file

@ -18,7 +18,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;

View file

@ -9,7 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
use WooCommerce\WooCommerce\Logging\Logger\WooCommerceLogger;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;

View file

@ -21,7 +21,7 @@ use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesDisclaimers;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Settings\SettingsModule;
@ -570,10 +570,10 @@ return array(
$settings = $container->get( 'wcgateway.settings' );
$fields = $container->get( 'wcgateway.settings.fields' );
$webhook_registrar = $container->get( 'webhook.registrar' );
$state = $container->get( 'onboarding.state' );
$cache = new Cache( 'ppcp-paypal-bearer' );
$bearer = $container->get( 'api.bearer' );
$page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' );
$state = $container->get( 'onboarding.state' );
$cache = $container->get( 'api.paypal-bearer-cache' );
$bearer = $container->get( 'api.bearer' );
$page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' );
$signup_link_cache = $container->get( 'onboarding.signup-link-cache' );
$signup_link_ids = $container->get( 'onboarding.signup-link-ids' );
$pui_status_cache = $container->get( 'pui.status-cache' );
@ -1431,7 +1431,7 @@ return array(
$partner_endpoint,
$container->get( 'dcc.status-cache' ),
$container->get( 'api.helpers.dccapplies' ),
$container->get( 'onboarding.state' ),
$container->get( 'settings.flag.is-connected' ),
$container->get( 'api.helper.failure-registry' )
);
},
@ -1515,7 +1515,7 @@ return array(
$container->get( 'wcgateway.settings' ),
$container->get( 'api.endpoint.partners' ),
$container->get( 'pui.status-cache' ),
$container->get( 'onboarding.state' ),
$container->get( 'settings.flag.is-connected' ),
$container->get( 'api.helper.failure-registry' )
);
},
@ -1728,7 +1728,7 @@ return array(
$environment = $container->get( 'onboarding.environment' );
assert( $environment instanceof Environment );
$dcc_enabled = $dcc_product_status->dcc_is_active();
$dcc_enabled = $dcc_product_status->is_active();
$enabled_status_text = esc_html__( 'Status: Available', 'woocommerce-paypal-payments' );
$disabled_status_text = esc_html__( 'Status: Not yet enabled', 'woocommerce-paypal-payments' );
@ -1799,7 +1799,7 @@ return array(
$environment = $container->get( 'onboarding.environment' );
assert( $environment instanceof Environment );
$pui_enabled = $pui_product_status->pui_is_active();
$pui_enabled = $pui_product_status->is_active();
$enabled_status_text = esc_html__( 'Status: Available', 'woocommerce-paypal-payments' );
$disabled_status_text = esc_html__( 'Status: Not yet enabled', 'woocommerce-paypal-payments' );

View file

@ -10,7 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Assets;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet;

View file

@ -11,7 +11,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Assets;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;

View file

@ -13,8 +13,7 @@ use Exception;
use Psr\Log\LoggerInterface;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;

View file

@ -18,7 +18,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;

View file

@ -17,7 +17,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait;

View file

@ -18,7 +18,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens;

View file

@ -445,7 +445,7 @@ class PayUponInvoice {
}
if (
! $this->pui_product_status->pui_is_active()
! $this->pui_product_status->is_active()
|| ! $this->pui_helper->is_checkout_ready_for_pui()
) {
unset( $methods[ PayUponInvoiceGateway::ID ] );
@ -478,7 +478,7 @@ class PayUponInvoice {
function() {
if (
PayUponInvoiceGateway::ID === $this->current_ppcp_settings_page_id
&& $this->pui_product_status->pui_is_active()
&& $this->pui_product_status->is_active()
) {
$error_messages = array();
$pui_gateway = WC()->payment_gateways->payment_gateways()[ PayUponInvoiceGateway::ID ];

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