Merge branch 'trunk' into modularity-full-migration

# Conflicts:
#	modules/ppcp-blocks/src/BlocksModule.php
This commit is contained in:
Pedro Silva 2023-12-19 10:28:47 +00:00
commit 5837d65b18
No known key found for this signature in database
GPG key ID: E2EE20C0669D24B3
150 changed files with 8479 additions and 3005 deletions

View file

@ -2,6 +2,12 @@
if (!defined('PAYPAL_INTEGRATION_DATE')) { if (!defined('PAYPAL_INTEGRATION_DATE')) {
define('PAYPAL_INTEGRATION_DATE', '2023-06-02'); define('PAYPAL_INTEGRATION_DATE', '2023-06-02');
} }
if (!defined('PAYPAL_URL')) {
define( 'PAYPAL_URL', 'https://www.paypal.com' );
}
if (!defined('PAYPAL_SANDBOX_URL')) {
define( 'PAYPAL_SANDBOX_URL', 'https://www.sandbox.paypal.com' );
}
if (!defined('EP_PAGES')) { if (!defined('EP_PAGES')) {
define('EP_PAGES', 4096); define('EP_PAGES', 4096);
} }

View file

@ -21,6 +21,7 @@ use WooCommerce\PayPalCommerce\PPCP;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater; use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
/** /**
@ -47,6 +48,19 @@ function ppcp_get_paypal_order( $paypal_id_or_wc_order ): Order {
return $order_endpoint->order( $paypal_id_or_wc_order ); return $order_endpoint->order( $paypal_id_or_wc_order );
} }
/**
* Creates a PayPal order for the given WC order.
*
* @param WC_Order $wc_order The WC order.
* @throws Exception When the operation fails.
*/
function ppcp_create_paypal_order_for_wc_order( WC_Order $wc_order ): Order {
$order_processor = PPCP::container()->get( 'wcgateway.order-processor' );
assert( $order_processor instanceof OrderProcessor );
return $order_processor->create_order( $wc_order );
}
/** /**
* Captures the PayPal order. * Captures the PayPal order.
* *

View file

@ -1,5 +1,37 @@
*** Changelog *** *** Changelog ***
= 2.4.3 - xxxx-xx-xx =
* Fix - PayPal Subscription initiated without a WooCommerce order #1907
* Fix - Block Checkout reloads when submitting order with empty fields #1904
* Fix - "Send checkout billing and shipping data to Apple Pay" displayed when Apple Pay is disabled #1883
* Fix - "Order does not contain intent" error for ACDC renewals when triggering 3D Secure #1888
* Enhancement - Add button to reload feature eligibility status from Connection tab #1902
* Enhancement - Apple Pay validation message improvements #1901
* Enhancement - Improve support for Classic Cart & Classic Checkout blocks #1894
* Enhancement - Ensure uniform button appearance for PayPal, Google Pay, and Apple Pay buttons #1900
* Enhancement - remove string translations for package tracking carriers from repository #1885
* Enhancement - Incorrect margins when PayPal buttons are rendered as separate gateways. #1908
* Feature preview - Save payment methods (Vault v3) integration #1779
= 2.4.2 - 2023-12-04 =
* Fix - Action callback arguments count in ShipStation tracking integration #1841
* Fix - Google Pay scripts loading on unrelated admin pages #1834
* Fix - Do not ignore disabled APMs list in blocks #1865
* Fix - Display Package Tracking metabox below Order actions when HPOS is active #1850
* Fix - ApplePay use checkout form data to update shipping and billing #1832
* Fix - Fix Apple Pay CSS #1872
* Enhancement - Allow redirect to PayPal with "Place order" button if smart buttons failed to load #1840 #1870
* Enhancement - Extend list of supported countries for Package Tracking v2 integration #1848
* Enhancement - Improve Block Theme support for Pay Later messaging #1855
* Enhancement - Render block buttons separately and add block style settings #1858
* Enhancement - Enable Block Cart and Block Express Checkout button locations by default #1852
* Enhancement - Improve single product page button placement with Block themes #1847
* Enhancement - Remove the Home location from default enabled Pay Later messaging locations #1856
* Enhancement - Chrome browser detected as eligible for Apple Pay on settings page #1828
* Enhancement - Hide Apple Pay & Google Pay for subscription type products #1835
* Enhancement - Add Standard Card Button gateway styling settings & preview #1827
* Feature preview - Upgrade to new Hosted Card Fields for Advanced Card Processing #1843
= 2.4.1 - 2023-11-14 = = 2.4.1 - 2023-11-14 =
* Fix - Error "PayPal order ID not found in meta" prevents automations from triggering when buying subscription via third-party payment gateway #1822 * Fix - Error "PayPal order ID not found in meta" prevents automations from triggering when buying subscription via third-party payment gateway #1822
* Fix - Card button subscription support declaration #1796 * Fix - Card button subscription support declaration #1796

View file

@ -20,13 +20,14 @@ return function ( string $root_dir ): iterable {
( require "$modules_dir/ppcp-onboarding/module.php" )(), ( require "$modules_dir/ppcp-onboarding/module.php" )(),
( require "$modules_dir/ppcp-session/module.php" )(), ( require "$modules_dir/ppcp-session/module.php" )(),
( require "$modules_dir/ppcp-status-report/module.php" )(), ( require "$modules_dir/ppcp-status-report/module.php" )(),
( require "$modules_dir/ppcp-subscription/module.php" )(), ( require "$modules_dir/ppcp-wc-subscriptions/module.php" )(),
( require "$modules_dir/ppcp-wc-gateway/module.php" )(), ( require "$modules_dir/ppcp-wc-gateway/module.php" )(),
( require "$modules_dir/ppcp-webhooks/module.php" )(), ( require "$modules_dir/ppcp-webhooks/module.php" )(),
( require "$modules_dir/ppcp-vaulting/module.php" )(), ( require "$modules_dir/ppcp-vaulting/module.php" )(),
( require "$modules_dir/ppcp-order-tracking/module.php" )(), ( require "$modules_dir/ppcp-order-tracking/module.php" )(),
( require "$modules_dir/ppcp-uninstall/module.php" )(), ( require "$modules_dir/ppcp-uninstall/module.php" )(),
( require "$modules_dir/ppcp-blocks/module.php" )(), ( require "$modules_dir/ppcp-blocks/module.php" )(),
( require "$modules_dir/ppcp-paypal-subscriptions/module.php" )(),
); );
if ( apply_filters( if ( apply_filters(
// phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
@ -60,5 +61,13 @@ return function ( string $root_dir ): iterable {
$modules[] = ( require "$modules_dir/ppcp-card-fields/module.php" )(); $modules[] = ( require "$modules_dir/ppcp-card-fields/module.php" )();
} }
if ( apply_filters(
//phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores
'woocommerce.feature-flags.woocommerce_paypal_payments.save_payment_methods_enabled',
getenv( 'PCP_SAVE_PAYMENT_METHODS' ) === '1'
) ) {
$modules[] = ( require "$modules_dir/ppcp-save-payment-methods/module.php" )();
}
return $modules; return $modules;
}; };

View file

@ -9,12 +9,16 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient; namespace WooCommerce\PayPalCommerce\ApiClient;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator; use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerPayableBreakdown;
use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundFactory;
@ -50,7 +54,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PatchCollectionFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayeeFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayeeFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentsFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentsFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentSourceFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenActionLinksFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenActionLinksFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PlatformFeeFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PlatformFeeFactory;
@ -80,6 +83,15 @@ return array(
'api.paypal-host' => function( ContainerInterface $container ) : string { 'api.paypal-host' => function( ContainerInterface $container ) : string {
return PAYPAL_API_URL; return PAYPAL_API_URL;
}, },
// It seems this 'api.paypal-website-url' key is always overridden in ppcp-onboarding/services.php.
'api.paypal-website-url' => function( ContainerInterface $container ) : string {
return PAYPAL_URL;
},
'api.factory.paypal-checkout-url' => function( ContainerInterface $container ) : callable {
return function ( string $id ) use ( $container ): string {
return $container->get( 'api.paypal-website-url' ) . '/checkoutnow?token=' . $id;
};
},
'api.partner_merchant_id' => static function () : string { 'api.partner_merchant_id' => static function () : string {
return ''; return '';
}, },
@ -138,6 +150,13 @@ return array(
$container->get( 'api.repository.customer' ) $container->get( 'api.repository.customer' )
); );
}, },
'api.endpoint.payment-tokens' => static function( ContainerInterface $container ) : PaymentTokensEndpoint {
return new PaymentTokensEndpoint(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.endpoint.webhook' => static function ( ContainerInterface $container ) : WebhookEndpoint { 'api.endpoint.webhook' => static function ( ContainerInterface $container ) : WebhookEndpoint {
return new WebhookEndpoint( return new WebhookEndpoint(
@ -204,7 +223,7 @@ return array(
$intent = $settings->has( 'intent' ) && strtoupper( (string) $settings->get( 'intent' ) ) === 'AUTHORIZE' ? 'AUTHORIZE' : 'CAPTURE'; $intent = $settings->has( 'intent' ) && strtoupper( (string) $settings->get( 'intent' ) ) === 'AUTHORIZE' ? 'AUTHORIZE' : 'CAPTURE';
$application_context_repository = $container->get( 'api.repository.application-context' ); $application_context_repository = $container->get( 'api.repository.application-context' );
$subscription_helper = $container->get( 'subscription.helper' ); $subscription_helper = $container->get( 'wc-subscriptions.helper' );
return new OrderEndpoint( return new OrderEndpoint(
$container->get( 'api.host' ), $container->get( 'api.host' ),
$container->get( 'api.bearer' ), $container->get( 'api.bearer' ),
@ -250,6 +269,13 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },
'api.endpoint.payment-method-tokens' => static function( ContainerInterface $container ): PaymentMethodTokensEndpoint {
return new PaymentMethodTokensEndpoint(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.repository.application-context' => static function( ContainerInterface $container ) : ApplicationContextRepository { 'api.repository.application-context' => static function( ContainerInterface $container ) : ApplicationContextRepository {
$settings = $container->get( 'wcgateway.settings' ); $settings = $container->get( 'wcgateway.settings' );
@ -372,21 +398,16 @@ return array(
'api.factory.address' => static function ( ContainerInterface $container ): AddressFactory { 'api.factory.address' => static function ( ContainerInterface $container ): AddressFactory {
return new AddressFactory(); return new AddressFactory();
}, },
'api.factory.payment-source' => static function ( ContainerInterface $container ): PaymentSourceFactory {
return new PaymentSourceFactory();
},
'api.factory.order' => static function ( ContainerInterface $container ): OrderFactory { 'api.factory.order' => static function ( ContainerInterface $container ): OrderFactory {
$purchase_unit_factory = $container->get( 'api.factory.purchase-unit' ); $purchase_unit_factory = $container->get( 'api.factory.purchase-unit' );
$payer_factory = $container->get( 'api.factory.payer' ); $payer_factory = $container->get( 'api.factory.payer' );
$application_context_repository = $container->get( 'api.repository.application-context' ); $application_context_repository = $container->get( 'api.repository.application-context' );
$application_context_factory = $container->get( 'api.factory.application-context' ); $application_context_factory = $container->get( 'api.factory.application-context' );
$payment_source_factory = $container->get( 'api.factory.payment-source' );
return new OrderFactory( return new OrderFactory(
$purchase_unit_factory, $purchase_unit_factory,
$payer_factory, $payer_factory,
$application_context_repository, $application_context_repository,
$application_context_factory, $application_context_factory
$payment_source_factory
); );
}, },
'api.factory.payments' => static function ( ContainerInterface $container ): PaymentsFactory { 'api.factory.payments' => static function ( ContainerInterface $container ): PaymentsFactory {
@ -438,6 +459,9 @@ return array(
$container->get( 'api.factory.payment-preferences' ) $container->get( 'api.factory.payment-preferences' )
); );
}, },
'api.factory.card-authentication-result-factory' => static function( ContainerInterface $container ): CardAuthenticationResultFactory {
return new CardAuthenticationResultFactory();
},
'api.helpers.dccapplies' => static function ( ContainerInterface $container ) : DccApplies { 'api.helpers.dccapplies' => static function ( ContainerInterface $container ) : DccApplies {
return new DccApplies( return new DccApplies(
$container->get( 'api.dcc-supported-country-currency-matrix' ), $container->get( 'api.dcc-supported-country-currency-matrix' ),
@ -1116,4 +1140,11 @@ return array(
return new PurchaseUnitSanitizer( $behavior, $line_name ); return new PurchaseUnitSanitizer( $behavior, $line_name );
} }
), ),
'api.user-id-token' => static function( ContainerInterface $container ): UserIdToken {
return new UserIdToken(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
); );

View file

@ -0,0 +1,105 @@
<?php
/**
* Generates user ID token for payer.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Authentication
*/
namespace WooCommerce\PayPalCommerce\ApiClient\Authentication;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\RequestTrait;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WP_Error;
/**
* Class UserIdToken
*/
class UserIdToken {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* UserIdToken constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $host,
Bearer $bearer,
LoggerInterface $logger
) {
$this->host = $host;
$this->bearer = $bearer;
$this->logger = $logger;
}
/**
* Returns `id_token` which uniquely identifies the payer.
*
* @param string $target_customer_id Vaulted customer id.
*
* @return string
*
* @throws PayPalApiException If the request fails.
* @throws RuntimeException If something unexpected happens.
*/
public function id_token( string $target_customer_id = '' ): string {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/oauth2/token?grant_type=client_credentials&response_type=id_token';
if ( $target_customer_id ) {
$url = add_query_arg(
array(
'target_customer_id' => $target_customer_id,
),
$url
);
}
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/x-www-form-urlencoded',
),
);
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
throw new RuntimeException( $response->get_error_message() );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
throw new PayPalApiException( $json, $status_code );
}
return $json->id_token;
}
}

View file

@ -18,6 +18,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PatchCollection; use WooCommerce\PayPalCommerce\ApiClient\Entity\PatchCollection;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit; use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
@ -27,7 +28,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PatchCollectionFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\ErrorResponse; use WooCommerce\PayPalCommerce\ApiClient\Helper\ErrorResponse;
use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet; use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet;
use WP_Error; use WP_Error;
@ -178,8 +179,11 @@ class OrderEndpoint {
* @param string $shipping_preference One of ApplicationContext::SHIPPING_PREFERENCE_ values. * @param string $shipping_preference One of ApplicationContext::SHIPPING_PREFERENCE_ values.
* @param Payer|null $payer The payer off the order. * @param Payer|null $payer The payer off the order.
* @param PaymentToken|null $payment_token The payment token. * @param PaymentToken|null $payment_token The payment token.
* @param string $paypal_request_id The paypal request id. * @param string $paypal_request_id The PayPal request id.
* @param string $user_action The user action. * @param string $user_action The user action.
* @param string $payment_method WC payment method.
* @param array $request_data Request data.
* @param PaymentSource|null $payment_source The payment source.
* *
* @return Order * @return Order
* @throws RuntimeException If the request fails. * @throws RuntimeException If the request fails.
@ -190,7 +194,10 @@ class OrderEndpoint {
Payer $payer = null, Payer $payer = null,
PaymentToken $payment_token = null, PaymentToken $payment_token = null,
string $paypal_request_id = '', string $paypal_request_id = '',
string $user_action = ApplicationContext::USER_ACTION_CONTINUE string $user_action = ApplicationContext::USER_ACTION_CONTINUE,
string $payment_method = '',
array $request_data = array(),
PaymentSource $payment_source = null
): Order { ): Order {
$bearer = $this->bearer->bearer(); $bearer = $this->bearer->bearer();
$data = array( $data = array(
@ -217,11 +224,16 @@ class OrderEndpoint {
if ( $payment_token ) { if ( $payment_token ) {
$data['payment_source']['token'] = $payment_token->to_array(); $data['payment_source']['token'] = $payment_token->to_array();
} }
if ( $payment_source ) {
$data['payment_source'] = array(
$payment_source->name() => $payment_source->properties(),
);
}
/** /**
* The filter can be used to modify the order creation request body data. * The filter can be used to modify the order creation request body data.
*/ */
$data = apply_filters( 'ppcp_create_order_request_body_data', $data ); $data = apply_filters( 'ppcp_create_order_request_body_data', $data, $payment_method, $request_data );
$url = trailingslashit( $this->host ) . 'v2/checkout/orders'; $url = trailingslashit( $this->host ) . 'v2/checkout/orders';
$args = array( $args = array(
'method' => 'POST', 'method' => 'POST',
@ -260,26 +272,25 @@ class OrderEndpoint {
); );
throw $error; throw $error;
} }
$json = json_decode( $response['body'] ); $json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response ); $status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 201 !== $status_code ) { if ( ! in_array( $status_code, array( 200, 201 ), true ) ) {
$error = new PayPalApiException( $error = new PayPalApiException(
$json, $json,
$status_code $status_code
); );
$this->logger->log(
'warning', $this->logger->warning(
sprintf( sprintf(
'Failed to create order. PayPal API response: %1$s', 'Failed to create order. PayPal API response: %1$s',
$error->getMessage() $error->getMessage()
),
array(
'args' => $args,
'response' => $response,
) )
); );
throw $error; throw $error;
} }
$order = $this->order_factory->from_paypal_response( $json ); $order = $this->order_factory->from_paypal_response( $json );
do_action( 'woocommerce_paypal_payments_paypal_order_created', $order ); do_action( 'woocommerce_paypal_payments_paypal_order_created', $order );

View file

@ -0,0 +1,155 @@
<?php
/**
* The Payment Method Tokens endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Psr\Log\LoggerInterface;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class PaymentMethodTokensEndpoint
*/
class PaymentMethodTokensEndpoint {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* PaymentMethodTokensEndpoint constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param LoggerInterface $logger The logger.
*/
public function __construct( string $host, Bearer $bearer, LoggerInterface $logger ) {
$this->host = $host;
$this->bearer = $bearer;
$this->logger = $logger;
}
/**
* Creates a setup token.
*
* @param PaymentSource $payment_source The payment source.
*
* @return stdClass
*
* @throws RuntimeException When something when wrong with the request.
* @throws PayPalApiException When something when wrong setting up the token.
*/
public function setup_tokens( PaymentSource $payment_source ): stdClass {
$data = array(
'payment_source' => array(
$payment_source->name() => $payment_source->properties(),
),
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v3/vault/setup-tokens';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'PayPal-Request-Id' => uniqid( 'ppcp-', true ),
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to create setup token.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( ! in_array( $status_code, array( 200, 201 ), true ) ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $json;
}
/**
* Creates a payment token for the given payment source.
*
* @param PaymentSource $payment_source The payment source.
*
* @return stdClass
*
* @throws RuntimeException When something when wrong with the request.
* @throws PayPalApiException When something when wrong setting up the token.
*/
public function payment_tokens( PaymentSource $payment_source ): stdClass {
$data = array(
'payment_source' => array(
$payment_source->name() => $payment_source->properties(),
),
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v3/vault/payment-tokens';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'PayPal-Request-Id' => uniqid( 'ppcp-', true ),
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to create setup token.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( ! in_array( $status_code, array( 200, 201 ), true ) ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $json;
}
}

View file

@ -0,0 +1,95 @@
<?php
/**
* Payment tokens version 3 endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WP_Error;
/**
* Class PaymentTokensEndpoint
*/
class PaymentTokensEndpoint {
use RequestTrait;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* PaymentTokensEndpoint constructor.
*
* @param string $host The bearer.
* @param Bearer $bearer The bearer.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $host,
Bearer $bearer,
LoggerInterface $logger
) {
$this->host = $host;
$this->bearer = $bearer;
$this->logger = $logger;
}
/**
* Deletes a payment token with the given id.
*
* @param string $id Payment token id.
*
* @return void
*
* @throws RuntimeException When something went wrong with the request.
* @throws PayPalApiException When something went wrong deleting the payment token.
*/
public function delete( string $id ): void {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v3/vault/payment-tokens/' . $id;
$args = array(
'method' => 'DELETE',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
);
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
throw new RuntimeException( $response->get_error_message() );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
throw new PayPalApiException( $json, $status_code );
}
}
}

View file

@ -14,7 +14,6 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
*/ */
class CardAuthenticationResult { class CardAuthenticationResult {
const LIABILITY_SHIFT_POSSIBLE = 'POSSIBLE'; const LIABILITY_SHIFT_POSSIBLE = 'POSSIBLE';
const LIABILITY_SHIFT_NO = 'NO'; const LIABILITY_SHIFT_NO = 'NO';
const LIABILITY_SHIFT_UNKNOWN = 'UNKNOWN'; const LIABILITY_SHIFT_UNKNOWN = 'UNKNOWN';

View file

@ -107,14 +107,6 @@ class Order {
$this->id = $id; $this->id = $id;
$this->application_context = $application_context; $this->application_context = $application_context;
$this->purchase_units = array_values(
array_filter(
$purchase_units,
static function ( $unit ): bool {
return is_a( $unit, PurchaseUnit::class );
}
)
);
$this->payer = $payer; $this->payer = $payer;
$this->order_status = $order_status; $this->order_status = $order_status;
$this->intent = ( 'CAPTURE' === $intent ) ? 'CAPTURE' : 'AUTHORIZE'; $this->intent = ( 'CAPTURE' === $intent ) ? 'CAPTURE' : 'AUTHORIZE';
@ -236,9 +228,6 @@ class Order {
if ( $this->application_context() ) { if ( $this->application_context() ) {
$order['application_context'] = $this->application_context()->to_array(); $order['application_context'] = $this->application_context()->to_array();
} }
if ( $this->payment_source() ) {
$order['payment_source'] = $this->payment_source()->to_array();
}
return $order; return $order;
} }

View file

@ -22,6 +22,7 @@ class OrderStatus {
const VOIDED = 'VOIDED'; const VOIDED = 'VOIDED';
const COMPLETED = 'COMPLETED'; const COMPLETED = 'COMPLETED';
const PENDING_APPROVAL = 'PENDING_APPROVAL'; const PENDING_APPROVAL = 'PENDING_APPROVAL';
const PAYER_ACTION_REQUIRED = 'PAYER_ACTION_REQUIRED';
const VALID_STATUS = array( const VALID_STATUS = array(
self::INTERNAL, self::INTERNAL,
self::CREATED, self::CREATED,
@ -30,6 +31,7 @@ class OrderStatus {
self::VOIDED, self::VOIDED,
self::COMPLETED, self::COMPLETED,
self::PENDING_APPROVAL, self::PENDING_APPROVAL,
self::PAYER_ACTION_REQUIRED,
); );
/** /**

View file

@ -9,74 +9,53 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity; namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use stdClass;
/** /**
* Class PaymentSource * Class PaymentSource
*/ */
class PaymentSource { class PaymentSource {
/** /**
* The card. * Payment source name.
* *
* @var PaymentSourceCard|null * @var string
*/ */
private $card; private $name;
/** /**
* The wallet. * Payment source properties.
* *
* @var PaymentSourceWallet|null * @var object
*/ */
private $wallet; private $properties;
/** /**
* PaymentSource constructor. * PaymentSource constructor.
* *
* @param PaymentSourceCard|null $card The card. * @param string $name Payment source name.
* @param PaymentSourceWallet|null $wallet The wallet. * @param object $properties Payment source properties.
*/ */
public function __construct( public function __construct( string $name, object $properties ) {
PaymentSourceCard $card = null, $this->name = $name;
PaymentSourceWallet $wallet = null $this->properties = $properties;
) {
$this->card = $card;
$this->wallet = $wallet;
} }
/** /**
* Returns the card. * Payment source name.
* *
* @return PaymentSourceCard|null * @return string
*/ */
public function card() { public function name(): string {
return $this->name;
return $this->card;
} }
/** /**
* Returns the wallet. * Payment source properties.
* *
* @return PaymentSourceWallet|null * @return object
*/ */
public function wallet() { public function properties(): object {
return $this->properties;
return $this->wallet;
}
/**
* Returns the array of the object.
*
* @return array
*/
public function to_array(): array {
$data = array();
if ( $this->card() ) {
$data['card'] = $this->card()->to_array();
}
if ( $this->wallet() ) {
$data['wallet'] = $this->wallet()->to_array();
}
return $data;
} }
} }

View file

@ -1,123 +0,0 @@
<?php
/**
* The PaymentSourceCard object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class PaymentSourceCard
*/
class PaymentSourceCard {
/**
* The last digits of the card.
*
* @var string
*/
private $last_digits;
/**
* The brand.
*
* @var string
*/
private $brand;
/**
* The type.
*
* @var string
*/
private $type;
/**
* The authentication result.
*
* @var CardAuthenticationResult|null
*/
private $authentication_result;
/**
* PaymentSourceCard constructor.
*
* @param string $last_digits The last digits of the card.
* @param string $brand The brand of the card.
* @param string $type The type of the card.
* @param CardAuthenticationResult|null $authentication_result The authentication result.
*/
public function __construct(
string $last_digits,
string $brand,
string $type,
CardAuthenticationResult $authentication_result = null
) {
$this->last_digits = $last_digits;
$this->brand = $brand;
$this->type = $type;
$this->authentication_result = $authentication_result;
}
/**
* Returns the last digits.
*
* @return string
*/
public function last_digits(): string {
return $this->last_digits;
}
/**
* Returns the brand.
*
* @return string
*/
public function brand(): string {
return $this->brand;
}
/**
* Returns the type.
*
* @return string
*/
public function type(): string {
return $this->type;
}
/**
* Returns the authentication result.
*
* @return CardAuthenticationResult|null
*/
public function authentication_result() {
return $this->authentication_result;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
$data = array(
'last_digits' => $this->last_digits(),
'brand' => $this->brand(),
'type' => $this->type(),
);
if ( $this->authentication_result() ) {
$data['authentication_result'] = $this->authentication_result()->to_array();
}
return $data;
}
}

View file

@ -1,25 +0,0 @@
<?php
/**
* The PaymentSourcewallet.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class PaymentSourceWallet
*/
class PaymentSourceWallet {
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
return array();
}
}

View file

@ -14,7 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\AmountBreakdown;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item; use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;

View file

@ -0,0 +1,33 @@
<?php
/**
* The card authentication result factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult;
/**
* Class CardAuthenticationResultFactory
*/
class CardAuthenticationResultFactory {
/**
* Returns a card authentication result from the given response object.
*
* @param stdClass $authentication_result The authentication result object.
* @return CardAuthenticationResult
*/
public function from_paypal_response( stdClass $authentication_result ): CardAuthenticationResult {
return new CardAuthenticationResult(
$authentication_result->liability_shift ?? '',
$authentication_result->three_d_secure->enrollment_status ?? '',
$authentication_result->three_d_secure->authentication_status ?? ''
);
}
}

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit; use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository;
@ -48,13 +49,6 @@ class OrderFactory {
*/ */
private $application_context_factory; private $application_context_factory;
/**
* The PaymentSource factory.
*
* @var PaymentSourceFactory
*/
private $payment_source_factory;
/** /**
* OrderFactory constructor. * OrderFactory constructor.
* *
@ -62,21 +56,18 @@ class OrderFactory {
* @param PayerFactory $payer_factory The Payer factory. * @param PayerFactory $payer_factory The Payer factory.
* @param ApplicationContextRepository $application_context_repository The Application Context repository. * @param ApplicationContextRepository $application_context_repository The Application Context repository.
* @param ApplicationContextFactory $application_context_factory The Application Context factory. * @param ApplicationContextFactory $application_context_factory The Application Context factory.
* @param PaymentSourceFactory $payment_source_factory The Payment Source factory.
*/ */
public function __construct( public function __construct(
PurchaseUnitFactory $purchase_unit_factory, PurchaseUnitFactory $purchase_unit_factory,
PayerFactory $payer_factory, PayerFactory $payer_factory,
ApplicationContextRepository $application_context_repository, ApplicationContextRepository $application_context_repository,
ApplicationContextFactory $application_context_factory, ApplicationContextFactory $application_context_factory
PaymentSourceFactory $payment_source_factory
) { ) {
$this->purchase_unit_factory = $purchase_unit_factory; $this->purchase_unit_factory = $purchase_unit_factory;
$this->payer_factory = $payer_factory; $this->payer_factory = $payer_factory;
$this->application_context_repository = $application_context_repository; $this->application_context_repository = $application_context_repository;
$this->application_context_factory = $application_context_factory; $this->application_context_factory = $application_context_factory;
$this->payment_source_factory = $payment_source_factory;
} }
/** /**
@ -152,9 +143,23 @@ class OrderFactory {
$application_context = ( isset( $order_data->application_context ) ) ? $application_context = ( isset( $order_data->application_context ) ) ?
$this->application_context_factory->from_paypal_response( $order_data->application_context ) $this->application_context_factory->from_paypal_response( $order_data->application_context )
: null; : null;
$payment_source = ( isset( $order_data->payment_source ) ) ?
$this->payment_source_factory->from_paypal_response( $order_data->payment_source ) : $payment_source = null;
null; if ( isset( $order_data->payment_source ) ) {
$json_encoded_payment_source = wp_json_encode( $order_data->payment_source );
if ( $json_encoded_payment_source ) {
$payment_source_as_array = json_decode( $json_encoded_payment_source, true );
if ( $payment_source_as_array ) {
$name = array_key_first( $payment_source_as_array );
if ( $name ) {
$payment_source = new PaymentSource(
$name,
$order_data->payment_source->$name
);
}
}
}
}
return new Order( return new Order(
$order_data->id, $order_data->id,

View file

@ -1,53 +0,0 @@
<?php
/**
* The PaymentSource factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSourceCard;
/**
* Class PaymentSourceFactory
*/
class PaymentSourceFactory {
/**
* Returns a PaymentSource for a PayPal Response.
*
* @param \stdClass $data The JSON object.
*
* @return PaymentSource
*/
public function from_paypal_response( \stdClass $data ): PaymentSource {
$card = null;
$wallet = null;
if ( isset( $data->card ) ) {
$authentication_result = null;
if ( isset( $data->card->authentication_result ) ) {
$authentication_result = new CardAuthenticationResult(
isset( $data->card->authentication_result->liability_shift ) ?
(string) $data->card->authentication_result->liability_shift : '',
isset( $data->card->authentication_result->three_d_secure->enrollment_status ) ?
(string) $data->card->authentication_result->three_d_secure->enrollment_status : '',
isset( $data->card->authentication_result->three_d_secure->authentication_status ) ?
(string) $data->card->authentication_result->three_d_secure->authentication_status : ''
);
}
$card = new PaymentSourceCard(
isset( $data->card->last_digits ) ? (string) $data->card->last_digits : '',
isset( $data->card->brand ) ? (string) $data->card->brand : '',
isset( $data->card->type ) ? (string) $data->card->type : '',
$authentication_result
);
}
return new PaymentSource( $card, $wallet );
}
}

View file

@ -49,7 +49,9 @@ return array(
// Domain validation. // Domain validation.
$domain_validation_text = __( 'Status: Domain validation failed ❌', 'woocommerce-paypal-payments' ); $domain_validation_text = __( 'Status: Domain validation failed ❌', 'woocommerce-paypal-payments' );
if ( $container->get( 'applepay.is_validated' ) ) { if ( ! $container->get( 'applepay.has_validated' ) ) {
$domain_validation_text = __( 'The domain has not yet been validated. Use the Apple Pay button to validate the domain ❌', 'woocommerce-paypal-payments' );
} elseif ( $container->get( 'applepay.is_validated' ) ) {
$domain_validation_text = __( 'Status: Domain successfully validated ✔️', 'woocommerce-paypal-payments' ); $domain_validation_text = __( 'Status: Domain successfully validated ✔️', 'woocommerce-paypal-payments' );
} }
@ -157,6 +159,7 @@ return array(
->action_visible( 'applepay_button_color' ) ->action_visible( 'applepay_button_color' )
->action_visible( 'applepay_button_type' ) ->action_visible( 'applepay_button_type' )
->action_visible( 'applepay_button_language' ) ->action_visible( 'applepay_button_language' )
->action_visible( 'applepay_checkout_data_mode' )
->to_array(), ->to_array(),
) )
), ),

View file

@ -1,63 +1,44 @@
#applepay-container, .ppcp-button-applepay { .ppcp-button-applepay {
// Should replicate apm-button.scss sizes.
--apple-pay-button-height: 45px; --apple-pay-button-height: 45px;
--apple-pay-button-min-height: 40px; --apple-pay-button-min-height: 35px;
--apple-pay-button-width: 100%; --apple-pay-button-width: 100%;
--apple-pay-button-max-width: 750px; --apple-pay-button-max-width: 750px;
--apple-pay-button-border-radius: 4px; --apple-pay-button-border-radius: 4px;
--apple-pay-button-overflow: hidden; --apple-pay-button-overflow: hidden;
margin:7px 0;
.ppcp-width-min & {
--apple-pay-button-height: 35px;
}
.ppcp-width-300 & {
--apple-pay-button-height: 45px;
}
.ppcp-width-500 & {
--apple-pay-button-height: 55px;
}
&.ppcp-button-pill { &.ppcp-button-pill {
--apple-pay-button-border-radius: 50px; --apple-pay-button-border-radius: 50px;
} }
&.ppcp-button-minicart { &.ppcp-button-minicart {
--apple-pay-button-display: block; --apple-pay-button-display: block;
--apple-pay-button-height: 40px;
} }
} }
.woocommerce-checkout { .wp-block-woocommerce-checkout, .wp-block-woocommerce-cart {
#applepay-container, .ppcp-button-applepay { .ppcp-button-applepay {
margin-top: 0.5em;
--apple-pay-button-border-radius: 4px;
--apple-pay-button-height: 45px;
&.ppcp-button-pill {
--apple-pay-button-border-radius: 50px;
}
}
}
.ppcp-has-applepay-block {
.wp-block-woocommerce-checkout {
#applepay-container, .ppcp-button-applepay {
--apple-pay-button-margin: 0; --apple-pay-button-margin: 0;
--apple-pay-button-height: 40px;
&.ppcp-button-pill {
--apple-pay-button-border-radius: 50px;
}
}
}
.wp-block-woocommerce-cart { apple-pay-button {
#applepay-container, .ppcp-button-applepay { min-width: 0;
--apple-pay-button-margin: 0; width: 100%;
--apple-pay-button-height: 40px; --apple-pay-button-width-default: 100%;
}
/* Workaround for blocks grid */
.wc-block-components-express-payment__event-buttons {
--apple-pay-button-display: block;
li[id*="express-payment-method-ppcp-"] {
--apple-pay-button-padding-bottom: 0;
}
} }
} }
} }
.wp-admin { .wp-admin {
.ppcp-button-applepay {
pointer-events: none;
}
&.ppcp-non-ios-device { &.ppcp-non-ios-device {
.ppcp-button-applepay { .ppcp-button-applepay {
apple-pay-button { apple-pay-button {

View file

@ -5,10 +5,13 @@ import {setEnabled} from '../../../ppcp-button/resources/js/modules/Helper/Butto
import FormValidator from "../../../ppcp-button/resources/js/modules/Helper/FormValidator"; import FormValidator from "../../../ppcp-button/resources/js/modules/Helper/FormValidator";
import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler'; import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler';
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder"; import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder";
import {apmButtonsInit} from "../../../ppcp-button/resources/js/modules/Helper/ApmButtons";
class ApplepayButton { class ApplepayButton {
constructor(context, externalHandler, buttonConfig, ppcpConfig) { constructor(context, externalHandler, buttonConfig, ppcpConfig) {
apmButtonsInit(ppcpConfig);
this.isInitialized = false; this.isInitialized = false;
this.context = context; this.context = context;
@ -60,7 +63,7 @@ class ApplepayButton {
this.initEventHandlers(); this.initEventHandlers();
this.isInitialized = true; this.isInitialized = true;
this.applePayConfig = config; this.applePayConfig = config;
const isEligible = this.applePayConfig.isEligible; const isEligible = (this.applePayConfig.isEligible && window.ApplePaySession) || this.buttonConfig.is_admin;
if (isEligible) { if (isEligible) {
this.fetchTransactionInfo().then(() => { this.fetchTransactionInfo().then(() => {
@ -84,6 +87,10 @@ class ApplepayButton {
}); });
} }
}); });
} else {
jQuery('#' + this.buttonConfig.button.wrapper).hide();
jQuery('#' + this.buttonConfig.button.mini_cart_wrapper).hide();
jQuery('#express-payment-method-ppcp-applepay').hide();
} }
} }
@ -107,7 +114,7 @@ class ApplepayButton {
let config = { let config = {
wrapper: this.buttonConfig.button.wrapper, wrapper: this.buttonConfig.button.wrapper,
ppcpStyle: this.ppcpConfig.button.style, ppcpStyle: this.ppcpConfig.button.style,
//buttonStyle: this.buttonConfig.button.style, buttonStyle: this.buttonConfig.button.style,
ppcpButtonWrapper: this.ppcpConfig.button.wrapper ppcpButtonWrapper: this.ppcpConfig.button.wrapper
} }
@ -119,7 +126,7 @@ class ApplepayButton {
} }
if (['cart-block', 'checkout-block'].indexOf(this.context) !== -1) { if (['cart-block', 'checkout-block'].indexOf(this.context) !== -1) {
config.ppcpButtonWrapper = '#express-payment-method-ppcp-gateway'; config.ppcpButtonWrapper = '#express-payment-method-ppcp-gateway-paypal';
} }
return config; return config;
@ -167,14 +174,8 @@ class ApplepayButton {
addButton() { addButton() {
this.log('addButton', this.context); this.log('addButton', this.context);
const wrapper = const { wrapper, ppcpStyle } = this.contextConfig();
(this.context === 'mini-cart')
? this.buttonConfig.button.mini_cart_wrapper
: this.buttonConfig.button.wrapper;
const shape =
(this.context === 'mini-cart')
? this.ppcpConfig.button.mini_cart_style.shape
: this.ppcpConfig.button.style.shape;
const appleContainer = document.getElementById(wrapper); const appleContainer = document.getElementById(wrapper);
const type = this.buttonConfig.button.type; const type = this.buttonConfig.button.type;
const language = this.buttonConfig.button.lang; const language = this.buttonConfig.button.lang;
@ -185,8 +186,13 @@ class ApplepayButton {
appleContainer.innerHTML = `<apple-pay-button id="${id}" buttonstyle="${color}" type="${type}" locale="${language}">`; appleContainer.innerHTML = `<apple-pay-button id="${id}" buttonstyle="${color}" type="${type}" locale="${language}">`;
} }
jQuery('#' + wrapper).addClass('ppcp-button-' + shape); const $wrapper = jQuery('#' + wrapper);
jQuery(wrapper).append(appleContainer); $wrapper.addClass('ppcp-button-' + ppcpStyle.shape);
if (ppcpStyle.height) {
$wrapper.css('--apple-pay-button-height', `${ppcpStyle.height}px`)
$wrapper.css('height', `${ppcpStyle.height}px`)
}
} }
//------------------------ //------------------------

View file

@ -65,7 +65,7 @@ import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/Wi
buttonConfig.button.wrapper = selector.replace('#', ''); buttonConfig.button.wrapper = selector.replace('#', '');
applyConfigOptions(buttonConfig); applyConfigOptions(buttonConfig);
const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-applepay"></div>`; const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-apm ppcp-button-applepay"></div>`;
if (!jQuery(selector).length) { if (!jQuery(selector).length) {
jQuery(ppcpConfig.button.wrapper).after(wrapperElement); jQuery(ppcpConfig.button.wrapper).after(wrapperElement);

View file

@ -23,12 +23,6 @@ const ApplePayComponent = () => {
const manager = new ApplepayManager(buttonConfig, ppcpConfig); const manager = new ApplepayManager(buttonConfig, ppcpConfig);
manager.init(); manager.init();
}; };
useEffect(() => {
const bodyClass = 'ppcp-has-applepay-block';
if (!document.body.classList.contains(bodyClass)) {
document.body.classList.add(bodyClass);
}
}, []);
useEffect(() => { useEffect(() => {
// Load ApplePay SDK // Load ApplePay SDK
@ -50,14 +44,13 @@ const ApplePayComponent = () => {
}, [paypalLoaded, applePayLoaded]); }, [paypalLoaded, applePayLoaded]);
return ( return (
<div id={buttonConfig.button.wrapper.replace('#', '')}></div> <div id={buttonConfig.button.wrapper.replace('#', '')} className="ppcp-button-apm ppcp-button-applepay"></div>
); );
} }
const features = ['products']; const features = ['products'];
let registerMethod = registerExpressPaymentMethod;
registerMethod({ registerExpressPaymentMethod({
name: buttonData.id, name: buttonData.id,
label: <div dangerouslySetInnerHTML={{__html: buttonData.title}}/>, label: <div dangerouslySetInnerHTML={{__html: buttonData.title}}/>,
content: <ApplePayComponent isEditing={false}/>, content: <ApplePayComponent isEditing={false}/>,

View file

@ -62,6 +62,11 @@ return array(
); );
}, },
'applepay.has_validated' => static function ( ContainerInterface $container ): bool {
$settings = $container->get( 'wcgateway.settings' );
return $settings->has( 'applepay_validated' );
},
'applepay.is_validated' => static function ( ContainerInterface $container ): bool { 'applepay.is_validated' => static function ( ContainerInterface $container ): bool {
$settings = $container->get( 'wcgateway.settings' ); $settings = $container->get( 'wcgateway.settings' );
return $settings->has( 'applepay_validated' ) ? $settings->get( 'applepay_validated' ) === true : false; return $settings->has( 'applepay_validated' ) ? $settings->get( 'applepay_validated' ) === true : false;

View file

@ -968,7 +968,7 @@ class ApplePayButton implements ButtonInterface {
add_action( add_action(
$render_placeholder, $render_placeholder,
function () { function () {
echo '<span id="applepay-container-minicart" class="ppcp-button-applepay ppcp-button-minicart"></span>'; echo '<span id="applepay-container-minicart" class="ppcp-button-apm ppcp-button-applepay ppcp-button-minicart"></span>';
}, },
21 21
); );
@ -981,7 +981,7 @@ class ApplePayButton implements ButtonInterface {
*/ */
protected function applepay_button(): void { protected function applepay_button(): void {
?> ?>
<div id="applepay-container"> <div id="applepay-container" class="ppcp-button-apm ppcp-button-applepay">
<?php wp_nonce_field( 'woocommerce-process_checkout', 'woocommerce-process-checkout-nonce' ); ?> <?php wp_nonce_field( 'woocommerce-process_checkout', 'woocommerce-process-checkout-nonce' ); ?>
</div> </div>
<?php <?php

View file

@ -149,6 +149,7 @@ class DataToAppleButtonScripts {
return array( return array(
'sdk_url' => $this->sdk_url, 'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'is_admin' => false,
'preferences' => array( 'preferences' => array(
'checkout_data_mode' => $checkout_data_mode, 'checkout_data_mode' => $checkout_data_mode,
), ),
@ -204,6 +205,7 @@ class DataToAppleButtonScripts {
return array( return array(
'sdk_url' => $this->sdk_url, 'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'is_admin' => false,
'preferences' => array( 'preferences' => array(
'checkout_data_mode' => $checkout_data_mode, 'checkout_data_mode' => $checkout_data_mode,
), ),
@ -252,6 +254,7 @@ class DataToAppleButtonScripts {
return array( return array(
'sdk_url' => $this->sdk_url, 'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'is_admin' => true,
'preferences' => array( 'preferences' => array(
'checkout_data_mode' => $checkout_data_mode, 'checkout_data_mode' => $checkout_data_mode,
), ),

View file

@ -1,6 +1,7 @@
<?php <?php
/** /**
* ApmApplies helper. * ApmApplies helper.
* Checks if ApplePay is available for a given country and currency.
* *
* @package WooCommerce\PayPalCommerce\ApplePay\Helper * @package WooCommerce\PayPalCommerce\ApplePay\Helper
*/ */
@ -15,7 +16,7 @@ namespace WooCommerce\PayPalCommerce\Applepay\Helper;
class ApmApplies { class ApmApplies {
/** /**
* The matrix which countries and currency combinations can be used for DCC. * The matrix which countries and currency combinations can be used for ApplePay.
* *
* @var array * @var array
*/ */
@ -38,7 +39,7 @@ class ApmApplies {
/** /**
* ApmApplies constructor. * ApmApplies constructor.
* *
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for DCC. * @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for ApplePay.
* @param string $currency 3-letter currency code of the shop. * @param string $currency 3-letter currency code of the shop.
* @param string $country 2-letter country code of the shop. * @param string $country 2-letter country code of the shop.
*/ */

View file

@ -0,0 +1,15 @@
@use "../../../ppcp-button/resources/css/mixins/apm-button" as apm-button;
li[id^="express-payment-method-ppcp-"] {
line-height: 0;
// Set min-width to 0 as the buttons need to fit in a tight grid.
.paypal-buttons {
min-width: 0 !important;
}
}
.ppcp-button-apm {
@include apm-button.button;
}

View file

@ -76,8 +76,8 @@ export const paypalShippingToWc = (shipping) => {
* @returns {Object} * @returns {Object}
*/ */
export const paypalPayerToWc = (payer) => { export const paypalPayerToWc = (payer) => {
const firstName = payer.name.given_name; const firstName = payer?.name?.given_name ?? '';
const lastName = payer.name.surname; const lastName = payer?.name?.surname ?? '';
const address = payer.address ? paypalAddressToWc(payer.address) : {}; const address = payer.address ? paypalAddressToWc(payer.address) : {};
return { return {
...address, ...address,
@ -100,11 +100,13 @@ export const paypalOrderToWcShippingAddress = (order) => {
const res = paypalShippingToWc(shipping); const res = paypalShippingToWc(shipping);
// use the name from billing if the same, to avoid possible mistakes when splitting full_name // use the name from billing if the same, to avoid possible mistakes when splitting full_name
if (order.payer) {
const billingAddress = paypalPayerToWc(order.payer); const billingAddress = paypalPayerToWc(order.payer);
if (`${res.first_name} ${res.last_name}` === `${billingAddress.first_name} ${billingAddress.last_name}`) { if (`${res.first_name} ${res.last_name}` === `${billingAddress.first_name} ${billingAddress.last_name}`) {
res.first_name = billingAddress.first_name; res.first_name = billingAddress.first_name;
res.last_name = billingAddress.last_name; res.last_name = billingAddress.last_name;
} }
}
return res; return res;
} }
@ -116,11 +118,14 @@ export const paypalOrderToWcShippingAddress = (order) => {
*/ */
export const paypalOrderToWcAddresses = (order) => { export const paypalOrderToWcAddresses = (order) => {
const shippingAddress = paypalOrderToWcShippingAddress(order); const shippingAddress = paypalOrderToWcShippingAddress(order);
let billingAddress = paypalPayerToWc(order.payer); let billingAddress = shippingAddress;
if (order.payer) {
billingAddress = paypalPayerToWc(order.payer);
// no billing address, such as if billing address retrieval is not allowed in the merchant account // no billing address, such as if billing address retrieval is not allowed in the merchant account
if (!billingAddress.address_line_1) { if (!billingAddress.address_line_1) {
billingAddress = {...shippingAddress, ...paypalPayerToWc(order.payer)}; billingAddress = {...shippingAddress, ...paypalPayerToWc(order.payer)};
} }
}
return {billingAddress, shippingAddress}; return {billingAddress, shippingAddress};
} }

View file

@ -1,13 +1,20 @@
import {useEffect, useState} from '@wordpress/element'; import {useEffect, useState} from '@wordpress/element';
import {registerExpressPaymentMethod, registerPaymentMethod} from '@woocommerce/blocks-registry'; import {registerExpressPaymentMethod, registerPaymentMethod} from '@woocommerce/blocks-registry';
import {mergeWcAddress, paypalAddressToWc, paypalOrderToWcAddresses} from "./Helper/Address"; import {mergeWcAddress, paypalAddressToWc, paypalOrderToWcAddresses} from "./Helper/Address";
import {loadPaypalScript} from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading' import {
loadPaypalScriptPromise
} from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'
import {
normalizeStyleForFundingSource
} from '../../../ppcp-button/resources/js/modules/Helper/Style'
import buttonModuleWatcher from "../../../ppcp-button/resources/js/modules/ButtonModuleWatcher"; import buttonModuleWatcher from "../../../ppcp-button/resources/js/modules/ButtonModuleWatcher";
const config = wc.wcSettings.getSetting('ppcp-gateway_data'); const config = wc.wcSettings.getSetting('ppcp-gateway_data');
window.ppcpFundingSource = config.fundingSource; window.ppcpFundingSource = config.fundingSource;
let registeredContext = false;
const PayPalComponent = ({ const PayPalComponent = ({
onClick, onClick,
onClose, onClose,
@ -18,17 +25,22 @@ const PayPalComponent = ({
activePaymentMethod, activePaymentMethod,
shippingData, shippingData,
isEditing, isEditing,
fundingSource,
}) => { }) => {
const {onPaymentSetup, onCheckoutFail, onCheckoutValidation} = eventRegistration; const {onPaymentSetup, onCheckoutFail, onCheckoutValidation} = eventRegistration;
const {responseTypes} = emitResponse; const {responseTypes} = emitResponse;
const [paypalOrder, setPaypalOrder] = useState(null); const [paypalOrder, setPaypalOrder] = useState(null);
const [gotoContinuationOnError, setGotoContinuationOnError] = useState(false);
const methodId = fundingSource ? `${config.id}-${fundingSource}` : config.id;
useEffect(() => { useEffect(() => {
// fill the form if in continuation (for product or mini-cart buttons) // fill the form if in continuation (for product or mini-cart buttons)
if (!config.scriptData.continuation || !config.scriptData.continuation.order || window.ppcpContinuationFilled) { if (!config.scriptData.continuation || !config.scriptData.continuation.order || window.ppcpContinuationFilled) {
return; return;
} }
try {
const paypalAddresses = paypalOrderToWcAddresses(config.scriptData.continuation.order); const paypalAddresses = paypalOrderToWcAddresses(config.scriptData.continuation.order);
const wcAddresses = wp.data.select('wc/store/cart').getCustomerData(); const wcAddresses = wp.data.select('wc/store/cart').getCustomerData();
const addresses = mergeWcAddress(wcAddresses, paypalAddresses); const addresses = mergeWcAddress(wcAddresses, paypalAddresses);
@ -36,28 +48,14 @@ const PayPalComponent = ({
if (shippingData.needsShipping) { if (shippingData.needsShipping) {
wp.data.dispatch('wc/store/cart').setShippingAddress(addresses.shippingAddress); wp.data.dispatch('wc/store/cart').setShippingAddress(addresses.shippingAddress);
} }
} catch (err) {
// sometimes the PayPal address is missing, skip in this case.
console.log(err);
}
// this useEffect should run only once, but adding this in case of some kind of full re-rendering // this useEffect should run only once, but adding this in case of some kind of full re-rendering
window.ppcpContinuationFilled = true; window.ppcpContinuationFilled = true;
}, []) }, [])
const [loaded, setLoaded] = useState(false);
useEffect(() => {
if (!loaded) {
loadPaypalScript(config.scriptData, () => {
setLoaded(true);
buttonModuleWatcher.registerContextBootstrap(config.scriptData.context, {
createOrder: () => {
return createOrder();
},
onApprove: (data, actions) => {
return handleApprove(data, actions);
},
});
});
}
}, [loaded]);
const createOrder = async () => { const createOrder = async () => {
try { try {
const res = await fetch(config.scriptData.ajax.create_order.endpoint, { const res = await fetch(config.scriptData.ajax.create_order.endpoint, {
@ -155,6 +153,7 @@ const PayPalComponent = ({
if (config.finalReviewEnabled) { if (config.finalReviewEnabled) {
location.href = getCheckoutRedirectUrl(); location.href = getCheckoutRedirectUrl();
} else { } else {
setGotoContinuationOnError(true);
onSubmit(); onSubmit();
} }
} catch (err) { } catch (err) {
@ -173,7 +172,7 @@ const PayPalComponent = ({
if (config.scriptData.continuation) { if (config.scriptData.continuation) {
return true; return true;
} }
if (wp.data.select('wc/store/validation').hasValidationErrors()) { if (gotoContinuationOnError && wp.data.select('wc/store/validation').hasValidationErrors()) {
location.href = getCheckoutRedirectUrl(); location.href = getCheckoutRedirectUrl();
return { type: responseTypes.ERROR }; return { type: responseTypes.ERROR };
} }
@ -181,7 +180,7 @@ const PayPalComponent = ({
return true; return true;
}); });
return unsubscribe; return unsubscribe;
}, [onCheckoutValidation] ); }, [onCheckoutValidation, gotoContinuationOnError] );
const handleClick = (data, actions) => { const handleClick = (data, actions) => {
if (isEditing) { if (isEditing) {
@ -233,7 +232,7 @@ const PayPalComponent = ({
} }
useEffect(() => { useEffect(() => {
if (activePaymentMethod !== config.id) { if (activePaymentMethod !== methodId) {
return; return;
} }
@ -269,7 +268,7 @@ const PayPalComponent = ({
}, [onPaymentSetup, paypalOrder, activePaymentMethod]); }, [onPaymentSetup, paypalOrder, activePaymentMethod]);
useEffect(() => { useEffect(() => {
if (activePaymentMethod !== config.id) { if (activePaymentMethod !== methodId) {
return; return;
} }
const unsubscribe = onCheckoutFail(({ processingResponse }) => { const unsubscribe = onCheckoutFail(({ processingResponse }) => {
@ -296,15 +295,26 @@ const PayPalComponent = ({
) )
} }
if (!loaded) { if (!registeredContext) {
return null; buttonModuleWatcher.registerContextBootstrap(config.scriptData.context, {
createOrder: () => {
return createOrder();
},
onApprove: (data, actions) => {
return handleApprove(data, actions);
},
});
registeredContext = true;
} }
const style = normalizeStyleForFundingSource(config.scriptData.button.style, fundingSource);
const PayPalButton = window.paypal.Buttons.driver("react", { React, ReactDOM }); const PayPalButton = window.paypal.Buttons.driver("react", { React, ReactDOM });
return ( return (
<PayPalButton <PayPalButton
style={config.scriptData.button.style} fundingSource={fundingSource}
style={style}
onClick={handleClick} onClick={handleClick}
onCancel={onClose} onCancel={onClose}
onError={onClose} onError={onClose}
@ -316,20 +326,61 @@ const PayPalComponent = ({
} }
const features = ['products']; const features = ['products'];
let registerMethod = registerExpressPaymentMethod;
if (config.scriptData.continuation) {
features.push('ppcp_continuation');
registerMethod = registerPaymentMethod;
}
registerMethod({ if ((config.addPlaceOrderMethod || config.usePlaceOrder) && !config.scriptData.continuation) {
let descriptionElement = <div dangerouslySetInnerHTML={{__html: config.description}}></div>;
if (config.placeOrderButtonDescription) {
descriptionElement = <div>
<p dangerouslySetInnerHTML={{__html: config.description}}></p>
<p style={{textAlign: 'center'}} className={'ppcp-place-order-description'} dangerouslySetInnerHTML={{__html: config.placeOrderButtonDescription}}></p>
</div>;
}
registerPaymentMethod({
name: config.id, name: config.id,
label: <div dangerouslySetInnerHTML={{__html: config.title}}/>, label: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
content: <PayPalComponent isEditing={false}/>, content: descriptionElement,
edit: <PayPalComponent isEditing={true}/>, edit: descriptionElement,
placeOrderButtonLabel: config.placeOrderButtonText,
ariaLabel: config.title, ariaLabel: config.title,
canMakePayment: () => config.enabled, canMakePayment: () => config.enabled,
supports: { supports: {
features: features, features: features,
}, },
}); });
}
if (config.scriptData.continuation) {
registerPaymentMethod({
name: config.id,
label: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
content: <PayPalComponent isEditing={false}/>,
edit: <PayPalComponent isEditing={true}/>,
ariaLabel: config.title,
canMakePayment: () => true,
supports: {
features: [...features, 'ppcp_continuation'],
},
});
} else if (!config.usePlaceOrder) {
const paypalScriptPromise = loadPaypalScriptPromise(config.scriptData);
for (const fundingSource of ['paypal', ...config.enabledFundingSources]) {
registerExpressPaymentMethod({
name: `${config.id}-${fundingSource}`,
paymentMethodId: config.id,
label: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
content: <PayPalComponent isEditing={false} fundingSource={fundingSource}/>,
edit: <PayPalComponent isEditing={true} fundingSource={fundingSource}/>,
ariaLabel: config.title,
canMakePayment: async () => {
await paypalScriptPromise;
return paypal.Buttons({fundingSource}).isEligible();
},
supports: {
features: features,
},
});
}
}

View file

@ -34,7 +34,12 @@ return array(
$container->get( 'wcgateway.paypal-gateway' ), $container->get( 'wcgateway.paypal-gateway' ),
$container->get( 'blocks.settings.final_review_enabled' ), $container->get( 'blocks.settings.final_review_enabled' ),
$container->get( 'session.cancellation.view' ), $container->get( 'session.cancellation.view' ),
$container->get( 'session.handler' ) $container->get( 'session.handler' ),
$container->get( 'blocks.add-place-order-method' ),
$container->get( 'wcgateway.use-place-order-button' ),
$container->get( 'wcgateway.place-order-button-text' ),
$container->get( 'wcgateway.place-order-button-description' ),
$container->get( 'wcgateway.all-funding-sources' )
); );
}, },
'blocks.settings.final_review_enabled' => static function ( ContainerInterface $container ): bool { 'blocks.settings.final_review_enabled' => static function ( ContainerInterface $container ): bool {
@ -54,4 +59,14 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },
'blocks.add-place-order-method' => function ( ContainerInterface $container ) : bool {
/**
* Whether to create a non-express method with the standard "Place order" button redirecting to PayPal.
*/
return apply_filters(
'woocommerce_paypal_payments_blocks_add_place_order_method',
true
);
},
); );

View file

@ -96,6 +96,27 @@ class BlocksModule implements ServiceModule, ExtendingModule, ExecutableModule {
} }
); );
// Enqueue frontend scripts.
add_action(
'wp_enqueue_scripts',
static function () use ( $c ) {
if ( ! has_block( 'woocommerce/checkout' ) && ! has_block( 'woocommerce/cart' ) ) {
return;
}
$module_url = $c->get( 'blocks.url' );
$asset_version = $c->get( 'ppcp.asset-version' );
wp_register_style(
'wc-ppcp-blocks',
untrailingslashit( $module_url ) . '/assets/css/gateway.css',
array(),
$asset_version
);
wp_enqueue_style( 'wc-ppcp-blocks' );
}
);
return true; return true;
} }
} }

View file

@ -87,6 +87,41 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
*/ */
private $session_handler; private $session_handler;
/**
* Whether to create a non-express method with the standard "Place order" button.
*
* @var bool
*/
protected $add_place_order_method;
/**
* Whether to use the standard "Place order" button instead of PayPal buttons.
*
* @var bool
*/
protected $use_place_order;
/**
* The text for the standard "Place order" button.
*
* @var string
*/
protected $place_order_button_text;
/**
* The text for additional "Place order" description.
*
* @var string
*/
protected $place_order_button_description;
/**
* All existing funding sources for PayPal buttons.
*
* @var array
*/
private $all_funding_sources;
/** /**
* Assets constructor. * Assets constructor.
* *
@ -99,6 +134,11 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
* @param bool $final_review_enabled Whether the final review is enabled. * @param bool $final_review_enabled Whether the final review is enabled.
* @param CancelView $cancellation_view The cancellation view. * @param CancelView $cancellation_view The cancellation view.
* @param SessionHandler $session_handler The Session handler. * @param SessionHandler $session_handler The Session handler.
* @param bool $add_place_order_method Whether to create a non-express method with the standard "Place order" button.
* @param bool $use_place_order Whether to use the standard "Place order" button instead of PayPal buttons.
* @param string $place_order_button_text The text for the standard "Place order" button.
* @param string $place_order_button_description The text for additional "Place order" description.
* @param array $all_funding_sources All existing funding sources for PayPal buttons.
*/ */
public function __construct( public function __construct(
string $module_url, string $module_url,
@ -109,7 +149,12 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
PayPalGateway $gateway, PayPalGateway $gateway,
bool $final_review_enabled, bool $final_review_enabled,
CancelView $cancellation_view, CancelView $cancellation_view,
SessionHandler $session_handler SessionHandler $session_handler,
bool $add_place_order_method,
bool $use_place_order,
string $place_order_button_text,
string $place_order_button_description,
array $all_funding_sources
) { ) {
$this->name = PayPalGateway::ID; $this->name = PayPalGateway::ID;
$this->module_url = $module_url; $this->module_url = $module_url;
@ -121,6 +166,11 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
$this->final_review_enabled = $final_review_enabled; $this->final_review_enabled = $final_review_enabled;
$this->cancellation_view = $cancellation_view; $this->cancellation_view = $cancellation_view;
$this->session_handler = $session_handler; $this->session_handler = $session_handler;
$this->add_place_order_method = $add_place_order_method;
$this->use_place_order = $use_place_order;
$this->place_order_button_text = $place_order_button_text;
$this->place_order_button_description = $place_order_button_description;
$this->all_funding_sources = $all_funding_sources;
} }
/** /**
@ -174,6 +224,14 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
} }
} }
$disabled_funding_sources = explode( ',', $script_data['url_params']['disable-funding'] ?? '' ) ?: array();
$funding_sources = array_values(
array_diff(
array_keys( $this->all_funding_sources ),
$disabled_funding_sources
)
);
return array( return array(
'id' => $this->gateway->id, 'id' => $this->gateway->id,
'title' => $this->gateway->title, 'title' => $this->gateway->title,
@ -181,6 +239,11 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
'enabled' => $this->settings_status->is_smart_button_enabled_for_location( $script_data['context'] ), 'enabled' => $this->settings_status->is_smart_button_enabled_for_location( $script_data['context'] ),
'fundingSource' => $this->session_handler->funding_source(), 'fundingSource' => $this->session_handler->funding_source(),
'finalReviewEnabled' => $this->final_review_enabled, 'finalReviewEnabled' => $this->final_review_enabled,
'addPlaceOrderMethod' => $this->add_place_order_method,
'usePlaceOrder' => $this->use_place_order,
'placeOrderButtonText' => $this->place_order_button_text,
'placeOrderButtonDescription' => $this->place_order_button_description,
'enabledFundingSources' => $funding_sources,
'ajax' => array( 'ajax' => array(
'update_shipping' => array( 'update_shipping' => array(
'endpoint' => WC_AJAX::get_endpoint( UpdateShippingEndpoint::ENDPOINT ), 'endpoint' => WC_AJAX::get_endpoint( UpdateShippingEndpoint::ENDPOINT ),

View file

@ -9,7 +9,8 @@ module.exports = {
target: 'web', target: 'web',
plugins: [ new DependencyExtractionWebpackPlugin() ], plugins: [ new DependencyExtractionWebpackPlugin() ],
entry: { entry: {
'checkout-block': path.resolve('./resources/js/checkout-block.js') 'checkout-block': path.resolve('./resources/js/checkout-block.js'),
"gateway": path.resolve('./resources/css/gateway.scss')
}, },
output: { output: {
path: path.resolve(__dirname, 'assets/'), path: path.resolve(__dirname, 'assets/'),

View file

@ -1,3 +1,5 @@
@use "mixins/apm-button" as apm-button;
#place_order.ppcp-hidden { #place_order.ppcp-hidden {
display: none !important; display: none !important;
} }
@ -15,3 +17,24 @@
.ppc-button-wrapper #ppcp-messages:first-child { .ppc-button-wrapper #ppcp-messages:first-child {
padding-top: 10px; padding-top: 10px;
} }
// Prevents spacing after button group.
#ppc-button-ppcp-gateway {
line-height: 0;
div[class^="item-"] {
margin-top: 14px;
&:first-child {
margin-top: 0;
}
}
}
#ppc-button-minicart {
line-height: 0;
display: block;
}
.ppcp-button-apm {
@include apm-button.button;
}

View file

@ -0,0 +1,42 @@
@mixin button {
overflow: hidden;
min-width: 0;
max-width: 750px;
line-height: 0;
border-radius: 4px;
// Defaults
height: 45px;
margin-top: 14px;
&.ppcp-button-pill {
border-radius: 50px;
}
&.ppcp-button-minicart {
display: block;
}
.ppcp-width-min & {
height: 35px;
}
.ppcp-width-300 & {
height: 45px;
}
.ppcp-width-500 & {
height: 55px;
}
// No margin on block layout.
.wp-block-woocommerce-checkout &, .wp-block-woocommerce-cart & {
margin: 0;
min-width: 0;
}
.wp-admin & {
pointer-events: none;
}
}

View file

@ -22,6 +22,7 @@ import FormValidator from "./modules/Helper/FormValidator";
import {loadPaypalScript} from "./modules/Helper/ScriptLoading"; import {loadPaypalScript} from "./modules/Helper/ScriptLoading";
import buttonModuleWatcher from "./modules/ButtonModuleWatcher"; import buttonModuleWatcher from "./modules/ButtonModuleWatcher";
import MessagesBootstrap from "./modules/ContextBootstrap/MessagesBootstap"; import MessagesBootstrap from "./modules/ContextBootstrap/MessagesBootstap";
import {apmButtonsInit} from "./modules/Helper/ApmButtons";
// TODO: could be a good idea to have a separate spinner for each gateway, // TODO: could be a good idea to have a separate spinner for each gateway,
// but I think we care mainly about the script loading, so one spinner should be enough. // but I think we care mainly about the script loading, so one spinner should be enough.
@ -145,6 +146,7 @@ const bootstrap = () => {
}; };
const onSmartButtonsInit = () => { const onSmartButtonsInit = () => {
jQuery(document).trigger('ppcp-smart-buttons-init', this);
buttonsSpinner.unblock(); buttonsSpinner.unblock();
}; };
const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway, onSmartButtonClick, onSmartButtonsInit); const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway, onSmartButtonClick, onSmartButtonsInit);
@ -217,6 +219,8 @@ const bootstrap = () => {
messageRenderer, messageRenderer,
); );
messagesBootstrap.init(); messagesBootstrap.init();
apmButtonsInit(PayPalCommerceGateway);
}; };
document.addEventListener( document.addEventListener(
@ -279,11 +283,12 @@ document.addEventListener(
}); });
let bootstrapped = false; let bootstrapped = false;
let failed = false;
hideOrderButtonIfPpcpGateway(); hideOrderButtonIfPpcpGateway();
jQuery(document.body).on('updated_checkout payment_method_selected', () => { jQuery(document.body).on('updated_checkout payment_method_selected', () => {
if (bootstrapped) { if (bootstrapped || failed) {
return; return;
} }
@ -294,6 +299,12 @@ document.addEventListener(
bootstrapped = true; bootstrapped = true;
bootstrap(); bootstrap();
}, () => {
failed = true;
setVisibleByClass(ORDER_BUTTON_SELECTOR, true, 'ppcp-hidden');
buttonsSpinner.unblock();
cardsSpinner.unblock();
}); });
}, },
); );

View file

@ -2,6 +2,7 @@ import 'formdata-polyfill';
import onApprove from '../OnApproveHandler/onApproveForPayNow.js'; import onApprove from '../OnApproveHandler/onApproveForPayNow.js';
import {payerData} from "../Helper/PayerData"; import {payerData} from "../Helper/PayerData";
import {getCurrentPaymentMethod} from "../Helper/CheckoutMethodState"; import {getCurrentPaymentMethod} from "../Helper/CheckoutMethodState";
import validateCheckoutForm from "../Helper/CheckoutFormValidation";
class CheckoutActionHandler { class CheckoutActionHandler {
@ -13,7 +14,13 @@ class CheckoutActionHandler {
subscriptionsConfiguration() { subscriptionsConfiguration() {
return { return {
createSubscription: (data, actions) => { createSubscription: async (data, actions) => {
try {
await validateCheckoutForm(this.config);
} catch (error) {
throw {type: 'form-validation-error'};
}
return actions.subscription.create({ return actions.subscription.create({
'plan_id': this.config.subscription_plan_id 'plan_id': this.config.subscription_plan_id
}); });
@ -56,6 +63,8 @@ class CheckoutActionHandler {
const paymentMethod = getCurrentPaymentMethod(); const paymentMethod = getCurrentPaymentMethod();
const fundingSource = window.ppcpFundingSource; const fundingSource = window.ppcpFundingSource;
const savePaymentMethod = !!document.getElementById('wc-ppcp-credit-card-gateway-new-payment-method')?.checked;
return fetch(this.config.ajax.create_order.endpoint, { return fetch(this.config.ajax.create_order.endpoint, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -72,7 +81,8 @@ class CheckoutActionHandler {
funding_source: fundingSource, funding_source: fundingSource,
// send as urlencoded string to handle complex fields via PHP functions the same as normal form submit // send as urlencoded string to handle complex fields via PHP functions the same as normal form submit
form_encoded: new URLSearchParams(formData).toString(), form_encoded: new URLSearchParams(formData).toString(),
createaccount: createaccount createaccount: createaccount,
save_payment_method: savePaymentMethod
}) })
}).then(function (res) { }).then(function (res) {
return res.json(); return res.json();

View file

@ -26,7 +26,7 @@ const storeToken = (token) => {
sessionStorage.setItem(storageKey, JSON.stringify(token)); sessionStorage.setItem(storageKey, JSON.stringify(token));
} }
const dataClientIdAttributeHandler = (scriptOptions, config, callback) => { const dataClientIdAttributeHandler = (scriptOptions, config, callback, errorCallback = null) => {
fetch(config.endpoint, { fetch(config.endpoint, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -51,6 +51,10 @@ const dataClientIdAttributeHandler = (scriptOptions, config, callback) => {
if (typeof callback === 'function') { if (typeof callback === 'function') {
callback(paypal); callback(paypal);
} }
}).catch(err => {
if (typeof errorCallback === 'function') {
errorCallback(err);
}
}); });
}); });
} }

View file

@ -0,0 +1,114 @@
export const apmButtonsInit = (config, selector = '.ppcp-button-apm') => {
let selectorInContainer = selector;
if (window.ppcpApmButtons) {
return;
}
if (config && config.button) {
// If it's separate gateways, modify wrapper to account for the individual buttons as individual APMs.
const wrapper = config.button.wrapper;
const isSeparateGateways = jQuery(wrapper).children('div[class^="item-"]').length > 0;
if (isSeparateGateways) {
selector += `, ${wrapper} div[class^="item-"]`;
selectorInContainer += `, div[class^="item-"]`;
}
}
window.ppcpApmButtons = new ApmButtons(selector, selectorInContainer);
}
export class ApmButtons {
constructor(selector, selectorInContainer) {
this.selector = selector;
this.selectorInContainer = selectorInContainer;
this.containers = [];
// Reloads button containers.
this.reloadContainers();
// Refresh button layout.
jQuery(window).resize(() => {
this.refresh();
}).resize();
jQuery(document).on('ppcp-smart-buttons-init', () => {
this.refresh();
});
// Observes for new buttons.
(new MutationObserver(this.observeElementsCallback.bind(this)))
.observe(document.body, { childList: true, subtree: true });
}
observeElementsCallback(mutationsList, observer) {
const observeSelector = this.selector + ', .widget_shopping_cart, .widget_shopping_cart_content';
let shouldReload = false;
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.matches && node.matches(observeSelector)) {
shouldReload = true;
}
});
}
}
if (shouldReload) {
this.reloadContainers();
this.refresh();
}
};
reloadContainers() {
jQuery(this.selector).each((index, el) => {
const parent = jQuery(el).parent();
if (!this.containers.some($el => $el.is(parent))) {
this.containers.push(parent);
}
});
console.log('this.containers', this.containers);
}
refresh() {
for (const container of this.containers) {
const $container = jQuery(container);
// Check width and add classes
const width = $container.width();
$container.removeClass('ppcp-width-500 ppcp-width-300 ppcp-width-min');
if (width >= 500) {
$container.addClass('ppcp-width-500');
} else if (width >= 300) {
$container.addClass('ppcp-width-300');
} else {
$container.addClass('ppcp-width-min');
}
// Check first apm button
const $firstElement = $container.children(':visible').first();
// Assign margins to buttons
$container.find(this.selectorInContainer).each((index, el) => {
const $el = jQuery(el);
if ($el.is($firstElement)) {
$el.css('margin-top', `0px`);
return true;
}
const height = $el.height();
$el.css('margin-top', `${Math.round(height * 0.3)}px`);
});
}
}
}

View file

@ -0,0 +1,50 @@
export const cardFieldStyles = (field) => {
const allowedProperties = [
'appearance',
'color',
'direction',
'font',
'font-family',
'font-size',
'font-size-adjust',
'font-stretch',
'font-style',
'font-variant',
'font-variant-alternates',
'font-variant-caps',
'font-variant-east-asian',
'font-variant-ligatures',
'font-variant-numeric',
'font-weight',
'letter-spacing',
'line-height',
'opacity',
'outline',
'padding',
'padding-bottom',
'padding-left',
'padding-right',
'padding-top',
'text-shadow',
'transition',
'-moz-appearance',
'-moz-osx-font-smoothing',
'-moz-tap-highlight-color',
'-moz-transition',
'-webkit-appearance',
'-webkit-osx-font-smoothing',
'-webkit-tap-highlight-color',
'-webkit-transition',
];
const stylesRaw = window.getComputedStyle(field);
const styles = {};
Object.values(stylesRaw).forEach((prop) => {
if (!stylesRaw[prop] || !allowedProperties.includes(prop)) {
return;
}
styles[prop] = '' + stylesRaw[prop];
});
return styles;
}

View file

@ -0,0 +1,48 @@
import Spinner from "./Spinner";
import FormValidator from "./FormValidator";
import ErrorHandler from "../ErrorHandler";
const validateCheckoutForm = function (config) {
return new Promise(async (resolve, reject) => {
try {
const spinner = new Spinner();
const errorHandler = new ErrorHandler(
config.labels.error.generic,
document.querySelector('.woocommerce-notices-wrapper')
);
const formSelector = config.context === 'checkout' ? 'form.checkout' : 'form#order_review';
const formValidator = config.early_checkout_validation_enabled ?
new FormValidator(
config.ajax.validate_checkout.endpoint,
config.ajax.validate_checkout.nonce,
) : null;
if (!formValidator) {
resolve();
return;
}
formValidator.validate(document.querySelector(formSelector)).then((errors) => {
if (errors.length > 0) {
spinner.unblock();
errorHandler.clear();
errorHandler.messages(errors);
// fire WC event for other plugins
jQuery( document.body ).trigger( 'checkout_error' , [ errorHandler.currentHtml() ] );
reject();
} else {
resolve();
}
});
} catch (error) {
console.error(error);
reject();
}
});
}
export default validateCheckoutForm;

View file

@ -7,10 +7,11 @@ import {keysToCamelCase} from "./Utils";
// This component may be used by multiple modules. This assures that options are shared between all instances. // This component may be used by multiple modules. This assures that options are shared between all instances.
let options = window.ppcpWidgetBuilder = window.ppcpWidgetBuilder || { let options = window.ppcpWidgetBuilder = window.ppcpWidgetBuilder || {
isLoading: false, isLoading: false,
onLoadedCallbacks: [] onLoadedCallbacks: [],
onErrorCallbacks: [],
}; };
export const loadPaypalScript = (config, onLoaded) => { export const loadPaypalScript = (config, onLoaded, onError = null) => {
// If PayPal is already loaded call the onLoaded callback and return. // If PayPal is already loaded call the onLoaded callback and return.
if (typeof paypal !== 'undefined') { if (typeof paypal !== 'undefined') {
onLoaded(); onLoaded();
@ -19,6 +20,9 @@ export const loadPaypalScript = (config, onLoaded) => {
// Add the onLoaded callback to the onLoadedCallbacks stack. // Add the onLoaded callback to the onLoadedCallbacks stack.
options.onLoadedCallbacks.push(onLoaded); options.onLoadedCallbacks.push(onLoaded);
if (onError) {
options.onErrorCallbacks.push(onError);
}
// Return if it's still loading. // Return if it's still loading.
if (options.isLoading) { if (options.isLoading) {
@ -26,6 +30,12 @@ export const loadPaypalScript = (config, onLoaded) => {
} }
options.isLoading = true; options.isLoading = true;
const resetState = () => {
options.isLoading = false;
options.onLoadedCallbacks = [];
options.onErrorCallbacks = [];
}
// Callback to be called once the PayPal script is loaded. // Callback to be called once the PayPal script is loaded.
const callback = (paypal) => { const callback = (paypal) => {
widgetBuilder.setPaypal(paypal); widgetBuilder.setPaypal(paypal);
@ -34,8 +44,14 @@ export const loadPaypalScript = (config, onLoaded) => {
onLoadedCallback(); onLoadedCallback();
} }
options.isLoading = false; resetState();
options.onLoadedCallbacks = []; }
const errorCallback = (err) => {
for (const onErrorCallback of options.onErrorCallbacks) {
onErrorCallback(err);
}
resetState();
} }
// Build the PayPal script options. // Build the PayPal script options.
@ -44,12 +60,26 @@ export const loadPaypalScript = (config, onLoaded) => {
// Load PayPal script for special case with data-client-token // Load PayPal script for special case with data-client-token
if (config.data_client_id.set_attribute) { if (config.data_client_id.set_attribute) {
dataClientIdAttributeHandler(scriptOptions, config.data_client_id, callback); dataClientIdAttributeHandler(scriptOptions, config.data_client_id, callback, errorCallback);
return; return;
} }
// Adds data-user-id-token to script options.
const userIdToken = config?.save_payment_methods?.id_token;
if(userIdToken) {
scriptOptions['data-user-id-token'] = userIdToken;
}
// Load PayPal script // Load PayPal script
loadScript(scriptOptions).then(callback); loadScript(scriptOptions)
.then(callback)
.catch(errorCallback);
}
export const loadPaypalScriptPromise = (config) => {
return new Promise((resolve, reject) => {
loadPaypalScript(config, resolve, reject)
});
} }
export const loadPaypalJsScript = (options, buttons, container) => { export const loadPaypalJsScript = (options, buttons, container) => {
@ -57,3 +87,11 @@ export const loadPaypalJsScript = (options, buttons, container) => {
paypal.Buttons(buttons).render(container); paypal.Buttons(buttons).render(container);
}); });
} }
export const loadPaypalJsScriptPromise = (options) => {
return new Promise((resolve, reject) => {
loadScript(options)
.then(resolve)
.catch(reject);
});
}

View file

@ -0,0 +1,20 @@
export const normalizeStyleForFundingSource = (style, fundingSource) => {
const commonProps = {};
['shape', 'height'].forEach(prop => {
if (style[prop]) {
commonProps[prop] = style[prop];
}
});
switch (fundingSource) {
case 'paypal':
return style;
case 'paylater':
return {
color: style.color,
...commonProps
};
default:
return commonProps;
}
}

View file

@ -1,4 +1,5 @@
import {show} from "../Helper/Hiding"; import {show} from "../Helper/Hiding";
import {cardFieldStyles} from "../Helper/CardFieldsHelper";
class CardFieldsRenderer { class CardFieldsRenderer {
@ -53,28 +54,28 @@ class CardFieldsRenderer {
if (cardField.isEligible()) { if (cardField.isEligible()) {
const nameField = document.getElementById('ppcp-credit-card-gateway-card-name'); const nameField = document.getElementById('ppcp-credit-card-gateway-card-name');
if (nameField) { if (nameField) {
let styles = this.cardFieldStyles(nameField); let styles = cardFieldStyles(nameField);
cardField.NameField({style: {'input': styles}}).render(nameField.parentNode); cardField.NameField({style: {'input': styles}}).render(nameField.parentNode);
nameField.remove(); nameField.remove();
} }
const numberField = document.getElementById('ppcp-credit-card-gateway-card-number'); const numberField = document.getElementById('ppcp-credit-card-gateway-card-number');
if (numberField) { if (numberField) {
let styles = this.cardFieldStyles(numberField); let styles = cardFieldStyles(numberField);
cardField.NumberField({style: {'input': styles}}).render(numberField.parentNode); cardField.NumberField({style: {'input': styles}}).render(numberField.parentNode);
numberField.remove(); numberField.remove();
} }
const expiryField = document.getElementById('ppcp-credit-card-gateway-card-expiry'); const expiryField = document.getElementById('ppcp-credit-card-gateway-card-expiry');
if (expiryField) { if (expiryField) {
let styles = this.cardFieldStyles(expiryField); let styles = cardFieldStyles(expiryField);
cardField.ExpiryField({style: {'input': styles}}).render(expiryField.parentNode); cardField.ExpiryField({style: {'input': styles}}).render(expiryField.parentNode);
expiryField.remove(); expiryField.remove();
} }
const cvvField = document.getElementById('ppcp-credit-card-gateway-card-cvc'); const cvvField = document.getElementById('ppcp-credit-card-gateway-card-cvc');
if (cvvField) { if (cvvField) {
let styles = this.cardFieldStyles(cvvField); let styles = cardFieldStyles(cvvField);
cardField.CVVField({style: {'input': styles}}).render(cvvField.parentNode); cardField.CVVField({style: {'input': styles}}).render(cvvField.parentNode);
cvvField.remove(); cvvField.remove();
} }
@ -91,65 +92,35 @@ class CardFieldsRenderer {
this.spinner.block(); this.spinner.block();
this.errorHandler.clear(); this.errorHandler.clear();
const paymentToken = document.querySelector('input[name="wc-ppcp-credit-card-gateway-payment-token"]:checked')?.value
if(paymentToken && paymentToken !== 'new') {
fetch(this.defaultConfig.ajax.capture_card_payment.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.defaultConfig.ajax.capture_card_payment.nonce,
payment_token: paymentToken
})
}).then((res) => {
return res.json();
}).then((data) => {
document.querySelector('#place_order').click();
});
return;
}
cardField.submit() cardField.submit()
.catch((error) => { .catch((error) => {
this.spinner.unblock(); this.spinner.unblock();
console.error(error) console.error(error)
this.errorHandler.message(this.defaultConfig.hosted_fields.labels.fields_not_valid); this.errorHandler.message(this.defaultConfig.hosted_fields.labels.fields_not_valid);
}) });
}); });
} }
cardFieldStyles(field) { disableFields() {}
const allowedProperties = [ enableFields() {}
'appearance',
'color',
'direction',
'font',
'font-family',
'font-size',
'font-size-adjust',
'font-stretch',
'font-style',
'font-variant',
'font-variant-alternates',
'font-variant-caps',
'font-variant-east-asian',
'font-variant-ligatures',
'font-variant-numeric',
'font-weight',
'letter-spacing',
'line-height',
'opacity',
'outline',
'padding',
'padding-bottom',
'padding-left',
'padding-right',
'padding-top',
'text-shadow',
'transition',
'-moz-appearance',
'-moz-osx-font-smoothing',
'-moz-tap-highlight-color',
'-moz-transition',
'-webkit-appearance',
'-webkit-osx-font-smoothing',
'-webkit-tap-highlight-color',
'-webkit-transition',
];
const stylesRaw = window.getComputedStyle(field);
const styles = {};
Object.values(stylesRaw).forEach((prop) => {
if (!stylesRaw[prop] || !allowedProperties.includes(prop)) {
return;
}
styles[prop] = '' + stylesRaw[prop];
});
return styles;
}
} }
export default CardFieldsRenderer; export default CardFieldsRenderer;

View file

@ -2,6 +2,7 @@ import merge from "deepmerge";
import {loadScript} from "@paypal/paypal-js"; import {loadScript} from "@paypal/paypal-js";
import {keysToCamelCase} from "../Helper/Utils"; import {keysToCamelCase} from "../Helper/Utils";
import widgetBuilder from "./WidgetBuilder"; import widgetBuilder from "./WidgetBuilder";
import {normalizeStyleForFundingSource} from "../Helper/Style";
class Renderer { class Renderer {
constructor(creditCardRenderer, defaultSettings, onSmartButtonClick, onSmartButtonsInit) { constructor(creditCardRenderer, defaultSettings, onSmartButtonClick, onSmartButtonsInit) {
@ -36,16 +37,7 @@ class Renderer {
} else { } else {
// render each button separately // render each button separately
for (const fundingSource of paypal.getFundingSources().filter(s => !(s in enabledSeparateGateways))) { for (const fundingSource of paypal.getFundingSources().filter(s => !(s in enabledSeparateGateways))) {
let style = settings.button.style; const style = normalizeStyleForFundingSource(settings.button.style, fundingSource);
if (fundingSource !== 'paypal') {
style = {
shape: style.shape,
color: style.color,
};
if (fundingSource !== 'paylater') {
delete style.color;
}
}
this.renderButtons( this.renderButtons(
settings.button.wrapper, settings.button.wrapper,

View file

@ -15,8 +15,10 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper; use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper;
use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver; use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator; use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton; use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton; use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
@ -68,8 +70,41 @@ return array(
return $dummy_ids[ $shop_country ] ?? $container->get( 'button.client_id' ); return $dummy_ids[ $shop_country ] ?? $container->get( 'button.client_id' );
}, },
// This service may not work correctly when called too early.
'button.context' => static function ( ContainerInterface $container ): string {
$obj = new class() {
use ContextTrait;
/**
* Session handler.
*
* @var SessionHandler
*/
protected $session_handler;
/** Constructor. */
public function __construct() {
// phpcs:ignore PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundInStatic
$this->session_handler = new SessionHandler();
}
/**
* Wrapper for a non-public function.
*/
public function get_context(): string {
// phpcs:ignore PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundInStatic
return $this->context();
}
};
return $obj->get_context();
},
'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface { 'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface {
$state = $container->get( 'onboarding.state' ); $state = $container->get( 'onboarding.state' );
if ( $container->get( 'wcgateway.use-place-order-button' )
&& in_array( $container->get( 'button.context' ), array( 'checkout', 'pay-now' ), true )
) {
return new DisabledSmartButton();
}
if ( $state->current_state() !== State::STATE_ONBOARDED ) { if ( $state->current_state() !== State::STATE_ONBOARDED ) {
return new DisabledSmartButton(); return new DisabledSmartButton();
} }
@ -84,7 +119,7 @@ return array(
$request_data = $container->get( 'button.request-data' ); $request_data = $container->get( 'button.request-data' );
$client_id = $container->get( 'button.client_id' ); $client_id = $container->get( 'button.client_id' );
$dcc_applies = $container->get( 'api.helpers.dccapplies' ); $dcc_applies = $container->get( 'api.helpers.dccapplies' );
$subscription_helper = $container->get( 'subscription.helper' ); $subscription_helper = $container->get( 'wc-subscriptions.helper' );
$messages_apply = $container->get( 'button.helper.messages-apply' ); $messages_apply = $container->get( 'button.helper.messages-apply' );
$environment = $container->get( 'onboarding.environment' ); $environment = $container->get( 'onboarding.environment' );
$payment_token_repository = $container->get( 'vaulting.repository.payment-token' ); $payment_token_repository = $container->get( 'vaulting.repository.payment-token' );
@ -259,8 +294,10 @@ return array(
}, },
'button.helper.three-d-secure' => static function ( ContainerInterface $container ): ThreeDSecure { 'button.helper.three-d-secure' => static function ( ContainerInterface $container ): ThreeDSecure {
$logger = $container->get( 'woocommerce.logger.woocommerce' ); return new ThreeDSecure(
return new ThreeDSecure( $logger ); $container->get( 'api.factory.card-authentication-result-factory' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
}, },
'button.helper.messages-apply' => static function ( ContainerInterface $container ): MessagesApply { 'button.helper.messages-apply' => static function ( ContainerInterface $container ): MessagesApply {
return new MessagesApply( return new MessagesApply(

View file

@ -33,8 +33,8 @@ use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply; use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
@ -272,6 +272,8 @@ class SmartButton implements SmartButtonInterface {
* @return bool * @return bool
*/ */
public function render_wrapper(): bool { public function render_wrapper(): bool {
$this->init_context();
if ( $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ) ) { if ( $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ) ) {
$this->render_button_wrapper_registrar(); $this->render_button_wrapper_registrar();
$this->render_message_wrapper_registrar(); $this->render_message_wrapper_registrar();
@ -303,7 +305,13 @@ class SmartButton implements SmartButtonInterface {
add_filter( add_filter(
'woocommerce_credit_card_form_fields', 'woocommerce_credit_card_form_fields',
function ( array $default_fields, $id ) use ( $subscription_helper ) : array { function ( array $default_fields, $id ) use ( $subscription_helper ) : array {
if ( is_user_logged_in() && $this->settings->has( 'vault_enabled_dcc' ) && $this->settings->get( 'vault_enabled_dcc' ) && CreditCardGateway::ID === $id ) { if (
is_user_logged_in()
&& $this->settings->has( 'vault_enabled_dcc' )
&& $this->settings->get( 'vault_enabled_dcc' )
&& CreditCardGateway::ID === $id
&& apply_filters( 'woocommerce_paypal_payments_should_render_card_custom_fields', true )
) {
$default_fields['card-vault'] = sprintf( $default_fields['card-vault'] = sprintf(
'<p class="form-row form-row-wide"><label for="ppcp-credit-card-vault"><input class="ppcp-credit-card-vault" type="checkbox" id="ppcp-credit-card-vault" name="vault">%s</label></p>', '<p class="form-row form-row-wide"><label for="ppcp-credit-card-vault"><input class="ppcp-credit-card-vault" type="checkbox" id="ppcp-credit-card-vault" name="vault">%s</label></p>',
@ -638,7 +646,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
return $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) return $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' )
&& $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' ) && $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' )
&& $this->dcc_applies->for_country_currency() && $this->dcc_applies->for_country_currency()
&& in_array( $this->context(), array( 'checkout', 'pay-now' ), true ); && in_array( $this->context(), array( 'checkout', 'pay-now', 'add-payment-method' ), true );
} }
/** /**
@ -1049,30 +1057,39 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
'mini_cart_wrapper' => '#ppc-button-minicart', 'mini_cart_wrapper' => '#ppc-button-minicart',
'is_mini_cart_disabled' => $this->is_button_disabled( 'mini-cart' ), 'is_mini_cart_disabled' => $this->is_button_disabled( 'mini-cart' ),
'cancel_wrapper' => '#ppcp-cancel', 'cancel_wrapper' => '#ppcp-cancel',
'mini_cart_style' => array( 'mini_cart_style' => $this->normalize_style(
array(
'layout' => $this->style_for_context( 'layout', 'mini-cart' ), 'layout' => $this->style_for_context( 'layout', 'mini-cart' ),
'color' => $this->style_for_context( 'color', 'mini-cart' ), 'color' => $this->style_for_context( 'color', 'mini-cart' ),
'shape' => $this->style_for_context( 'shape', 'mini-cart' ), 'shape' => $this->style_for_context( 'shape', 'mini-cart' ),
'label' => $this->style_for_context( 'label', 'mini-cart' ), 'label' => $this->style_for_context( 'label', 'mini-cart' ),
'tagline' => $this->style_for_context( 'tagline', 'mini-cart' ), 'tagline' => $this->style_for_context( 'tagline', 'mini-cart' ),
'height' => $this->settings->has( 'button_mini-cart_height' ) && $this->settings->get( 'button_mini-cart_height' ) ? $this->normalize_height( (int) $this->settings->get( 'button_mini-cart_height' ) ) : 35, 'height' => $this->normalize_height( $this->style_for_context( 'height', 'mini-cart', 35 ), 25, 55 ),
)
), ),
'style' => array( 'style' => $this->normalize_style(
array(
'layout' => $this->style_for_context( 'layout', $this->context() ), 'layout' => $this->style_for_context( 'layout', $this->context() ),
'color' => $this->style_for_context( 'color', $this->context() ), 'color' => $this->style_for_context( 'color', $this->context() ),
'shape' => $this->style_for_context( 'shape', $this->context() ), 'shape' => $this->style_for_context( 'shape', $this->context() ),
'label' => $this->style_for_context( 'label', $this->context() ), 'label' => $this->style_for_context( 'label', $this->context() ),
'tagline' => $this->style_for_context( 'tagline', $this->context() ), 'tagline' => $this->style_for_context( 'tagline', $this->context() ),
'height' => in_array( $this->context(), array( 'cart-block', 'checkout-block' ), true )
? $this->normalize_height( $this->style_for_context( 'height', $this->context(), 48 ), 40, 55 )
: null,
)
), ),
), ),
'separate_buttons' => array( 'separate_buttons' => array(
'card' => array( 'card' => array(
'id' => CardButtonGateway::ID, 'id' => CardButtonGateway::ID,
'wrapper' => '#ppc-button-' . CardButtonGateway::ID, 'wrapper' => '#ppc-button-' . CardButtonGateway::ID,
'style' => array( 'style' => $this->normalize_style(
array(
'shape' => $this->style_for_apm( 'shape', 'card' ), 'shape' => $this->style_for_apm( 'shape', 'card' ),
'color' => $this->style_for_apm( 'color', 'card', 'black' ), 'color' => $this->style_for_apm( 'color', 'card', 'black' ),
'layout' => $this->style_for_apm( 'poweredby_tagline', 'card', false ) === $this->normalize_style_value( true ) ? 'vertical' : 'horizontal', 'layout' => $this->style_for_apm( 'poweredby_tagline', 'card', false ) === $this->normalize_style_value( true ) ? 'vertical' : 'horizontal',
)
), ),
), ),
), ),
@ -1143,13 +1160,6 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
$localize['pay_now'] = $this->pay_now_script_data(); $localize['pay_now'] = $this->pay_now_script_data();
} }
if ( $this->style_for_context( 'layout', 'mini-cart' ) !== 'horizontal' ) {
$localize['button']['mini_cart_style']['tagline'] = false;
}
if ( $this->style_for_context( 'layout', $this->context() ) !== 'horizontal' ) {
$localize['button']['style']['tagline'] = false;
}
if ( $this->is_paypal_continuation() ) { if ( $this->is_paypal_continuation() ) {
$order = $this->session_handler->order(); $order = $this->session_handler->order();
assert( $order !== null ); assert( $order !== null );
@ -1160,7 +1170,8 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
} }
$this->request_data->dequeue_nonce_fix(); $this->request_data->dequeue_nonce_fix();
return $localize;
return apply_filters( 'woocommerce_paypal_payments_localized_script_data', $localize );
} }
/** /**
@ -1269,9 +1280,12 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
} }
if ( in_array( $context, array( 'checkout-block', 'cart-block' ), true ) ) { if ( in_array( $context, array( 'checkout-block', 'cart-block' ), true ) ) {
$disable_funding = array_diff( $disable_funding = array_merge(
$disable_funding,
array_diff(
array_keys( $this->all_funding_sources ), array_keys( $this->all_funding_sources ),
array( 'venmo', 'paylater' ) array( 'venmo', 'paylater' )
)
); );
} }
@ -1410,12 +1424,14 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
* *
* @param string $style The name of the style property. * @param string $style The name of the style property.
* @param string $context The context. * @param string $context The context.
* @param ?mixed $default The default value.
* *
* @return string * @return string|int
*/ */
private function style_for_context( string $style, string $context ): string { private function style_for_context( string $style, string $context, $default = null ) {
// Use the cart/checkout styles for blocks. if ( $context === 'checkout-block' ) {
$context = str_replace( '-block', '', $context ); $context = 'checkout-block-express';
}
$defaults = array( $defaults = array(
'layout' => 'vertical', 'layout' => 'vertical',
@ -1433,6 +1449,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
return $this->get_style_value( "button_{$context}_${style}" ) return $this->get_style_value( "button_{$context}_${style}" )
?? $this->get_style_value( "button_${style}" ) ?? $this->get_style_value( "button_${style}" )
?? ( $default ? $this->normalize_style_value( $default ) : null )
?? $this->normalize_style_value( $defaults[ $style ] ?? '' ); ?? $this->normalize_style_value( $defaults[ $style ] ?? '' );
} }
@ -1443,9 +1460,9 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
* @param string $apm The APM name, such as 'card'. * @param string $apm The APM name, such as 'card'.
* @param ?mixed $default The default value. * @param ?mixed $default The default value.
* *
* @return string * @return string|int
*/ */
private function style_for_apm( string $style, string $apm, $default = null ): string { private function style_for_apm( string $style, string $apm, $default = null ) {
return $this->get_style_value( "${apm}_button_${style}" ) return $this->get_style_value( "${apm}_button_${style}" )
?? ( $default ? $this->normalize_style_value( $default ) : null ) ?? ( $default ? $this->normalize_style_value( $default ) : null )
?? $this->style_for_context( $style, 'checkout' ); ?? $this->style_for_context( $style, 'checkout' );
@ -1455,9 +1472,9 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
* Returns the style property value or null. * Returns the style property value or null.
* *
* @param string $key The style property key in the settings. * @param string $key The style property key in the settings.
* @return string|null * @return string|int|null
*/ */
private function get_style_value( string $key ): ?string { private function get_style_value( string $key ) {
if ( ! $this->settings->has( $key ) ) { if ( ! $this->settings->has( $key ) ) {
return null; return null;
} }
@ -1468,27 +1485,49 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
* Converts the style property value to string. * Converts the style property value to string.
* *
* @param mixed $value The style property value. * @param mixed $value The style property value.
* @return string * @return string|int
*/ */
private function normalize_style_value( $value ): string { private function normalize_style_value( $value ) {
if ( is_bool( $value ) ) { if ( is_bool( $value ) ) {
$value = $value ? 'true' : 'false'; $value = $value ? 'true' : 'false';
} }
if ( is_int( $value ) ) {
return $value;
}
return (string) $value; return (string) $value;
} }
/** /**
* Returns a value between 25 and 55. * Fixes the style.
* *
* @param int $height The input value. * @param array $style The style properties.
* @return array
*/
private function normalize_style( array $style ): array {
if ( array_key_exists( 'tagline', $style ) && ( ! array_key_exists( 'layout', $style ) || $style['layout'] !== 'horizontal' ) ) {
$style['tagline'] = false;
}
if ( array_key_exists( 'height', $style ) && ! $style['height'] ) {
unset( $style['height'] );
}
return $style;
}
/**
* Returns a number between min and max.
*
* @param mixed $height The input value.
* @param int $min The minimum value.
* @param int $max The maximum value.
* @return int The normalized output value. * @return int The normalized output value.
*/ */
private function normalize_height( int $height ): int { private function normalize_height( $height, int $min, int $max ): int {
if ( $height < 25 ) { $height = (int) $height;
return 25; if ( $height < $min ) {
return $min;
} }
if ( $height > 55 ) { if ( $height > $max ) {
return 55; return $max;
} }
return $height; return $height;

View file

@ -14,6 +14,7 @@ use Exception;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderHelper; use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderHelper;
@ -144,10 +145,11 @@ class ApproveOrderEndpoint implements EndpointInterface {
$order = $this->api_endpoint->order( $data['order_id'] ); $order = $this->api_endpoint->order( $data['order_id'] );
if ( $order->payment_source() && $order->payment_source()->card() ) { $payment_source = $order->payment_source();
if ( $payment_source && $payment_source->name() === 'card' ) {
if ( $this->settings->has( 'disable_cards' ) ) { if ( $this->settings->has( 'disable_cards' ) ) {
$disabled_cards = (array) $this->settings->get( 'disable_cards' ); $disabled_cards = (array) $this->settings->get( 'disable_cards' );
$card = strtolower( $order->payment_source()->card()->brand() ); $card = strtolower( $payment_source->properties()->brand ?? '' );
if ( 'master_card' === $card ) { if ( 'master_card' === $card ) {
$card = 'mastercard'; $card = 'mastercard';
} }

View file

@ -25,12 +25,11 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException; use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator; use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler; use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcGateway\CardBillingMode; use WooCommerce\PayPalCommerce\WcGateway\CardBillingMode;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
@ -305,7 +304,7 @@ class CreateOrderEndpoint implements EndpointInterface {
} }
try { try {
$order = $this->create_paypal_order( $wc_order ); $order = $this->create_paypal_order( $wc_order, $payment_method, $data );
} catch ( Exception $exception ) { } catch ( Exception $exception ) {
$this->logger->error( 'Order creation failed: ' . $exception->getMessage() ); $this->logger->error( 'Order creation failed: ' . $exception->getMessage() );
throw $exception; throw $exception;
@ -416,6 +415,8 @@ class CreateOrderEndpoint implements EndpointInterface {
* Creates the order in the PayPal, uses data from WC order if provided. * Creates the order in the PayPal, uses data from WC order if provided.
* *
* @param \WC_Order|null $wc_order WC order to get data from. * @param \WC_Order|null $wc_order WC order to get data from.
* @param string $payment_method WC payment method.
* @param array $data Request data.
* *
* @return Order Created PayPal order. * @return Order Created PayPal order.
* *
@ -423,7 +424,7 @@ class CreateOrderEndpoint implements EndpointInterface {
* @throws PayPalApiException If create order request fails. * @throws PayPalApiException If create order request fails.
* phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber * phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
*/ */
private function create_paypal_order( \WC_Order $wc_order = null ): Order { private function create_paypal_order( \WC_Order $wc_order = null, string $payment_method = '', array $data = array() ): Order {
assert( $this->purchase_unit instanceof PurchaseUnit ); assert( $this->purchase_unit instanceof PurchaseUnit );
$funding_source = $this->parsed_request_data['funding_source'] ?? ''; $funding_source = $this->parsed_request_data['funding_source'] ?? '';
@ -465,7 +466,9 @@ class CreateOrderEndpoint implements EndpointInterface {
$payer, $payer,
null, null,
'', '',
$action $action,
$payment_method,
$data
); );
} catch ( PayPalApiException $exception ) { } catch ( PayPalApiException $exception ) {
// Looks like currently there is no proper way to validate the shipping address for PayPal, // Looks like currently there is no proper way to validate the shipping address for PayPal,

View file

@ -12,6 +12,39 @@ namespace WooCommerce\PayPalCommerce\Button\Helper;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
trait ContextTrait { trait ContextTrait {
/**
* Initializes context preconditions like is_cart() and is_checkout().
*
* @return void
*/
protected function init_context(): void {
if ( ! apply_filters( 'woocommerce_paypal_payments_block_classic_compat', true ) ) {
return;
}
/**
* Activate is_checkout() on woocommerce/classic-shortcode checkout blocks.
*
* @psalm-suppress MissingClosureParamType
*/
add_filter(
'woocommerce_is_checkout',
function ( $is_checkout ) {
if ( $is_checkout ) {
return $is_checkout;
}
return has_block( 'woocommerce/classic-shortcode {"shortcode":"checkout"}' );
}
);
// Activate is_cart() on woocommerce/classic-shortcode cart blocks.
if ( ! is_cart() && is_callable( 'wc_maybe_define_constant' ) ) {
if ( has_block( 'woocommerce/classic-shortcode' ) && ! has_block( 'woocommerce/classic-shortcode {"shortcode":"checkout"}' ) ) {
wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
}
}
}
/** /**
* Checks WC is_checkout() + WC checkout ajax requests. * Checks WC is_checkout() + WC checkout ajax requests.
*/ */
@ -94,6 +127,10 @@ trait ContextTrait {
return 'checkout'; return 'checkout';
} }
if ( $this->is_add_payment_method_page() ) {
return 'add-payment-method';
}
return 'mini-cart'; return 'mini-cart';
} }
@ -125,6 +162,11 @@ trait ContextTrait {
* @return bool * @return bool
*/ */
private function is_paypal_continuation(): bool { private function is_paypal_continuation(): bool {
/**
* Property is already defined in trait consumers.
*
* @psalm-suppress UndefinedThisPropertyFetch
*/
$order = $this->session_handler->order(); $order = $this->session_handler->order();
if ( ! $order ) { if ( ! $order ) {
return false; return false;
@ -137,7 +179,7 @@ trait ContextTrait {
} }
$source = $order->payment_source(); $source = $order->payment_source();
if ( $source && $source->card() ) { if ( $source && $source->name() === 'card' ) {
return false; // Ignore for DCC. return false; // Ignore for DCC.
} }
@ -147,4 +189,22 @@ trait ContextTrait {
return true; return true;
} }
/**
* Checks whether current page is Add payment method.
*
* @return bool
*/
private function is_add_payment_method_page(): bool {
/**
* Needed for WordPress `query_vars`.
*
* @psalm-suppress InvalidGlobal
*/
global $wp;
$page_id = wc_get_page_id( 'myaccount' );
return $page_id && is_page( $page_id ) && isset( $wp->query_vars['add-payment-method'] );
}
} }

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Button\Helper;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult as AuthResult; use WooCommerce\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult as AuthResult;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory;
/** /**
* Class ThreeDSecure * Class ThreeDSecure
@ -23,6 +24,13 @@ class ThreeDSecure {
const REJECT = 2; const REJECT = 2;
const RETRY = 3; const RETRY = 3;
/**
* Card authentication result factory.
*
* @var CardAuthenticationResultFactory
*/
private $card_authentication_result_factory;
/** /**
* The logger. * The logger.
* *
@ -33,10 +41,15 @@ class ThreeDSecure {
/** /**
* ThreeDSecure constructor. * ThreeDSecure constructor.
* *
* @param CardAuthenticationResultFactory $card_authentication_result_factory Card authentication result factory.
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
*/ */
public function __construct( LoggerInterface $logger ) { public function __construct(
CardAuthenticationResultFactory $card_authentication_result_factory,
LoggerInterface $logger
) {
$this->logger = $logger; $this->logger = $logger;
$this->card_authentication_result_factory = $card_authentication_result_factory;
} }
/** /**
@ -49,17 +62,22 @@ class ThreeDSecure {
* @return int * @return int
*/ */
public function proceed_with_order( Order $order ): int { public function proceed_with_order( Order $order ): int {
if ( ! $order->payment_source() ) { $payment_source = $order->payment_source();
return self::NO_DECISION; if ( ! $payment_source ) {
}
if ( ! $order->payment_source()->card() ) {
return self::NO_DECISION;
}
if ( ! $order->payment_source()->card()->authentication_result() ) {
return self::NO_DECISION; return self::NO_DECISION;
} }
$result = $order->payment_source()->card()->authentication_result(); if ( ! $payment_source->properties()->brand ?? '' ) {
return self::NO_DECISION;
}
if ( ! $payment_source->properties()->authentication_result ?? '' ) {
return self::NO_DECISION;
}
$authentication_result = $payment_source->properties()->authentication_result ?? null;
if ( $authentication_result ) {
$result = $this->card_authentication_result_factory->from_paypal_response( $authentication_result );
$this->logger->info( '3DS authentication result: ' . wc_print_r( $result->to_array(), true ) ); $this->logger->info( '3DS authentication result: ' . wc_print_r( $result->to_array(), true ) );
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_POSSIBLE ) { if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_POSSIBLE ) {
@ -72,6 +90,8 @@ class ThreeDSecure {
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_NO ) { if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_NO ) {
return $this->no_liability_shift( $result ); return $this->no_liability_shift( $result );
} }
}
return self::NO_DECISION; return self::NO_DECISION;
} }

View file

@ -89,6 +89,12 @@ class CardFieldsModule implements ModuleInterface {
add_filter( add_filter(
'ppcp_create_order_request_body_data', 'ppcp_create_order_request_body_data',
function( array $data ) use ( $c ): array { function( array $data ) use ( $c ): array {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$payment_method = wc_clean( wp_unslash( $_POST['payment_method'] ?? '' ) );
if ( $payment_method !== CreditCardGateway::ID ) {
return $data;
}
$settings = $c->get( 'wcgateway.settings' ); $settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings ); assert( $settings instanceof Settings );

View file

@ -27,7 +27,7 @@ return array(
}, },
'compat.ppec.subscriptions-handler' => static function ( ContainerInterface $container ) { 'compat.ppec.subscriptions-handler' => static function ( ContainerInterface $container ) {
$ppcp_renewal_handler = $container->get( 'subscription.renewal-handler' ); $ppcp_renewal_handler = $container->get( 'wc-subscriptions.renewal-handler' );
$gateway = $container->get( 'compat.ppec.mock-gateway' ); $gateway = $container->get( 'compat.ppec.mock-gateway' );
return new PPEC\SubscriptionsHandler( $ppcp_renewal_handler, $gateway ); return new PPEC\SubscriptionsHandler( $ppcp_renewal_handler, $gateway );

View file

@ -11,7 +11,7 @@ namespace WooCommerce\PayPalCommerce\Compat\PPEC;
use Automattic\WooCommerce\Utilities\OrderUtil; use Automattic\WooCommerce\Utilities\OrderUtil;
use stdClass; use stdClass;
use WooCommerce\PayPalCommerce\Subscription\RenewalHandler; use WooCommerce\PayPalCommerce\WcSubscriptions\RenewalHandler;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
/** /**

View file

@ -1,52 +1,9 @@
.ppcp-button-googlepay { .ppcp-button-googlepay {
margin: 7px 0;
overflow: hidden;
min-height: 40px; min-height: 40px;
height: 45px;
&.ppcp-button-pill {
border-radius: 50px;
}
&.ppcp-button-minicart {
display: block;
height: 40px;
}
} }
.woocommerce-checkout { .wp-block-woocommerce-checkout, .wp-block-woocommerce-cart {
.ppcp-button-googlepay { .gpay-button {
margin-top: 0; min-width: 0 !important;
}
}
.ppcp-has-googlepay-block {
.wp-block-woocommerce-checkout {
.ppcp-button-googlepay {
margin: 0;
height: 40px;
}
}
.wp-block-woocommerce-cart {
.ppcp-button-googlepay {
margin: 0;
height: 40px;
}
/* Workaround for blocks grid */
.wc-block-components-express-payment__event-buttons {
display: block;
li[id*="express-payment-method-ppcp-"] {
padding-bottom: 0;
}
}
}
}
.wp-admin {
.ppcp-button-googlepay {
pointer-events: none;
} }
} }

View file

@ -3,10 +3,13 @@ import {setVisible} from '../../../ppcp-button/resources/js/modules/Helper/Hidin
import {setEnabled} from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler'; import {setEnabled} from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler';
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder"; import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder";
import UpdatePaymentData from "./Helper/UpdatePaymentData"; import UpdatePaymentData from "./Helper/UpdatePaymentData";
import {apmButtonsInit} from "../../../ppcp-button/resources/js/modules/Helper/ApmButtons";
class GooglepayButton { class GooglepayButton {
constructor(context, externalHandler, buttonConfig, ppcpConfig) { constructor(context, externalHandler, buttonConfig, ppcpConfig) {
apmButtonsInit(ppcpConfig);
this.isInitialized = false; this.isInitialized = false;
this.context = context; this.context = context;
@ -111,7 +114,7 @@ class GooglepayButton {
} }
if (['cart-block', 'checkout-block'].indexOf(this.context) !== -1) { if (['cart-block', 'checkout-block'].indexOf(this.context) !== -1) {
config.ppcpButtonWrapper = '#express-payment-method-ppcp-gateway'; config.ppcpButtonWrapper = '#express-payment-method-ppcp-gateway-paypal';
} }
return config; return config;
@ -168,6 +171,10 @@ class GooglepayButton {
this.waitForWrapper(wrapper, () => { this.waitForWrapper(wrapper, () => {
jQuery(wrapper).addClass('ppcp-button-' + ppcpStyle.shape); jQuery(wrapper).addClass('ppcp-button-' + ppcpStyle.shape);
if (ppcpStyle.height) {
jQuery(wrapper).css('height', `${ppcpStyle.height}px`)
}
const button = const button =
this.paymentsClient.createButton({ this.paymentsClient.createButton({
onClick: this.onButtonClick.bind(this), onClick: this.onButtonClick.bind(this),

View file

@ -67,7 +67,7 @@ import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/Wi
buttonConfig.button.wrapper = selector; buttonConfig.button.wrapper = selector;
applyConfigOptions(buttonConfig); applyConfigOptions(buttonConfig);
const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-googlepay"></div>`; const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-apm ppcp-button-googlepay"></div>`;
if (!jQuery(selector).length) { if (!jQuery(selector).length) {
jQuery(ppcpConfig.button.wrapper).after(wrapperElement); jQuery(ppcpConfig.button.wrapper).after(wrapperElement);

View file

@ -24,13 +24,6 @@ const GooglePayComponent = () => {
manager.init(); manager.init();
}; };
useEffect(() => {
const bodyClass = 'ppcp-has-googlepay-block';
if (!document.body.classList.contains(bodyClass)) {
document.body.classList.add(bodyClass);
}
}, []);
useEffect(() => { useEffect(() => {
// Load GooglePay SDK // Load GooglePay SDK
loadCustomScript({ url: buttonConfig.sdk_url }).then(() => { loadCustomScript({ url: buttonConfig.sdk_url }).then(() => {
@ -51,14 +44,13 @@ const GooglePayComponent = () => {
}, [paypalLoaded, googlePayLoaded]); }, [paypalLoaded, googlePayLoaded]);
return ( return (
<div id={buttonConfig.button.wrapper.replace('#', '')} className="ppcp-button-googlepay"></div> <div id={buttonConfig.button.wrapper.replace('#', '')} className="ppcp-button-apm ppcp-button-googlepay"></div>
); );
} }
const features = ['products']; const features = ['products'];
let registerMethod = registerExpressPaymentMethod;
registerMethod({ registerExpressPaymentMethod({
name: buttonData.id, name: buttonData.id,
label: <div dangerouslySetInnerHTML={{__html: buttonData.title}}/>, label: <div dangerouslySetInnerHTML={{__html: buttonData.title}}/>,
content: <GooglePayComponent isEditing={false}/>, content: <GooglePayComponent isEditing={false}/>,

View file

@ -311,7 +311,7 @@ class Button implements ButtonInterface {
add_action( add_action(
$render_placeholder, $render_placeholder,
function () { function () {
echo '<span id="ppc-button-googlepay-container-minicart" class="ppcp-button-googlepay ppcp-button-minicart"></span>'; echo '<span id="ppc-button-googlepay-container-minicart" class="ppcp-button-apm ppcp-button-googlepay ppcp-button-minicart"></span>';
}, },
21 21
); );
@ -325,7 +325,7 @@ class Button implements ButtonInterface {
*/ */
private function googlepay_button(): void { private function googlepay_button(): void {
?> ?>
<div id="ppc-button-googlepay-container" class="ppcp-button-googlepay"> <div id="ppc-button-googlepay-container" class="ppcp-button-apm ppcp-button-googlepay">
<?php wp_nonce_field( 'woocommerce-process_checkout', 'woocommerce-process-checkout-nonce' ); ?> <?php wp_nonce_field( 'woocommerce-process_checkout', 'woocommerce-process-checkout-nonce' ); ?>
</div> </div>
<?php <?php

View file

@ -1,6 +1,7 @@
<?php <?php
/** /**
* Properties of the GooglePay module. * ApmApplies helper.
* Checks if GooglePay is available for a given country and currency.
* *
* @package WooCommerce\PayPalCommerce\Googlepay\Helper * @package WooCommerce\PayPalCommerce\Googlepay\Helper
*/ */
@ -15,7 +16,7 @@ namespace WooCommerce\PayPalCommerce\Googlepay\Helper;
class ApmApplies { class ApmApplies {
/** /**
* The matrix which countries and currency combinations can be used for DCC. * The matrix which countries and currency combinations can be used for GooglePay.
* *
* @var array * @var array
*/ */
@ -38,7 +39,7 @@ class ApmApplies {
/** /**
* DccApplies constructor. * DccApplies constructor.
* *
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for DCC. * @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for GooglePay.
* @param string $currency 3-letter currency code of the shop. * @param string $currency 3-letter currency code of the shop.
* @param string $country 2-letter country code of the shop. * @param string $country 2-letter country code of the shop.
*/ */

View file

@ -9,7 +9,7 @@ document.addEventListener(
const smartButtonLocationsSelector = '#field-smart_button_locations'; const smartButtonLocationsSelector = '#field-smart_button_locations';
const smartButtonLocationsSelect = smartButtonLocationsSelector + ' select'; const smartButtonLocationsSelect = smartButtonLocationsSelector + ' select';
const smartButtonSelectableLocations = ['product', 'cart', 'checkout', 'mini-cart']; const smartButtonSelectableLocations = ['product', 'cart', 'checkout', 'mini-cart', 'cart-block', 'checkout-block-express'];
const groupToggle = (selector, group) => { const groupToggle = (selector, group) => {
const toggleElement = document.querySelector(selector); const toggleElement = document.querySelector(selector);
@ -317,6 +317,7 @@ document.addEventListener(
'#field-button' + locationPrefix + '_label', '#field-button' + locationPrefix + '_label',
'#field-button' + locationPrefix + '_color', '#field-button' + locationPrefix + '_color',
'#field-button' + locationPrefix + '_shape', '#field-button' + locationPrefix + '_shape',
'#field-button' + locationPrefix + '_height',
'#field-button' + locationPrefix + '_preview', '#field-button' + locationPrefix + '_preview',
] ]
@ -324,11 +325,7 @@ document.addEventListener(
inputSelectors.push('#field-button_' + location + '_heading'); inputSelectors.push('#field-button_' + location + '_heading');
} }
if (location === 'mini-cart') { return inputSelectors.filter(selector => document.querySelector(selector));
inputSelectors.push('#field-button' + locationPrefix + '_height');
}
return inputSelectors
} }
const allPayLaterMessaginginputSelectors = () => { const allPayLaterMessaginginputSelectors = () => {

View file

@ -71,6 +71,12 @@ return array(
'api.paypal-host-sandbox' => static function( ContainerInterface $container ) : string { 'api.paypal-host-sandbox' => static function( ContainerInterface $container ) : string {
return PAYPAL_SANDBOX_API_URL; return PAYPAL_SANDBOX_API_URL;
}, },
'api.paypal-website-url-production' => static function( ContainerInterface $container ) : string {
return PAYPAL_URL;
},
'api.paypal-website-url-sandbox' => static function( ContainerInterface $container ) : string {
return PAYPAL_SANDBOX_URL;
},
'api.partner_merchant_id-production' => static function( ContainerInterface $container ) : string { 'api.partner_merchant_id-production' => static function( ContainerInterface $container ) : string {
return CONNECT_WOO_MERCHANT_ID; return CONNECT_WOO_MERCHANT_ID;
}, },
@ -89,6 +95,15 @@ return array(
} }
return $container->get( 'api.paypal-host-production' ); return $container->get( 'api.paypal-host-production' );
},
'api.paypal-website-url' => function( ContainerInterface $container ) : string {
$environment = $container->get( 'onboarding.environment' );
assert( $environment instanceof Environment );
if ( $environment->current_environment_is( Environment::SANDBOX ) ) {
return $container->get( 'api.paypal-website-url-sandbox' );
}
return $container->get( 'api.paypal-website-url-production' );
}, },
'api.bearer' => static function ( ContainerInterface $container ): Bearer { 'api.bearer' => static function ( ContainerInterface $container ): Bearer {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
node_modules
assets/js
assets/css

View file

@ -0,0 +1,17 @@
{
"name": "woocommerce/ppcp-paypal-subscriptions",
"type": "dhii-mod",
"description": "Module for PayPal Subscriptions API integration",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {
"psr-4": {
"WooCommerce\\PayPalCommerce\\PayPalSubscriptions\\": "src"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View file

@ -0,0 +1,14 @@
<?php
/**
* The PayPalSubscriptions module extensions.
*
* @package WooCommerce\PayPalCommerce\PayPalSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array();

View file

@ -0,0 +1,16 @@
<?php
/**
* The PayPalSubscriptions module.
*
* @package WooCommerce\PayPalCommerce\PayPalSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return new PayPalSubscriptionsModule();
};

View file

@ -1,5 +1,5 @@
{ {
"name": "ppcp-subscription", "name": "ppcp-paypal-subscriptions",
"version": "1.0.0", "version": "1.0.0",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"browserslist": [ "browserslist": [

View file

@ -0,0 +1,43 @@
<?php
/**
* The PayPalSubscriptions module services.
*
* @package WooCommerce\PayPalCommerce\PayPalSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
'paypal-subscriptions.deactivate-plan-endpoint' => static function ( ContainerInterface $container ): DeactivatePlanEndpoint {
return new DeactivatePlanEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.billing-plans' )
);
},
'paypal-subscriptions.api-handler' => static function( ContainerInterface $container ): SubscriptionsApiHandler {
return new SubscriptionsApiHandler(
$container->get( 'api.endpoint.catalog-products' ),
$container->get( 'api.factory.product' ),
$container->get( 'api.endpoint.billing-plans' ),
$container->get( 'api.factory.billing-cycle' ),
$container->get( 'api.factory.payment-preferences' ),
$container->get( 'api.shop.currency' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'paypal-subscriptions.module.url' => static function ( ContainerInterface $container ): string {
/**
* The path cannot be false.
*
* @psalm-suppress PossiblyFalseArgument
*/
return plugins_url(
'/modules/ppcp-paypal-subscriptions/',
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
);

View file

@ -2,12 +2,12 @@
/** /**
* The deactivate Subscription Plan Endpoint. * The deactivate Subscription Plan Endpoint.
* *
* @package WooCommerce\PayPalCommerce\OrderTracking\Endpoint * @package WooCommerce\PayPalCommerce\WcSubscriptions
*/ */
declare( strict_types=1 ); declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\Subscription; namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use Exception; use Exception;
use WC_Product; use WC_Product;

View file

@ -1,50 +1,40 @@
<?php <?php
/** /**
* The subscription module. * The PayPalSubscriptions module.
* *
* @package WooCommerce\PayPalCommerce\Subscription * @package WooCommerce\PayPalCommerce\PayPalSubscriptions
*/ */
declare(strict_types=1); declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription; namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use ActionScheduler_Store; use ActionScheduler_Store;
use Exception; use WC_Order;
use WC_Product; use WC_Product;
use WC_Product_Subscription; use WC_Product_Subscription;
use WC_Product_Subscription_Variation; use WC_Product_Subscription_Variation;
use WC_Product_Variable; use WC_Product_Variable;
use WC_Product_Variable_Subscription; use WC_Product_Variable_Subscription;
use WC_Subscription;
use WC_Subscriptions_Product; use WC_Subscriptions_Product;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use Psr\Log\LoggerInterface;
use WC_Order;
use WC_Subscription;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface; use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WP_Post; use WP_Post;
/** /**
* Class SubscriptionModule * Class SavedPaymentCheckerModule
*/ */
class SubscriptionModule implements ModuleInterface { class PayPalSubscriptionsModule implements ModuleInterface {
use TransactionIdHandlingTrait;
/** /**
* {@inheritDoc} * {@inheritDoc}
@ -60,470 +50,6 @@ class SubscriptionModule implements ModuleInterface {
* {@inheritDoc} * {@inheritDoc}
*/ */
public function run( ContainerInterface $c ): void { public function run( ContainerInterface $c ): void {
add_action(
'woocommerce_scheduled_subscription_payment_' . PayPalGateway::ID,
function ( $amount, $order ) use ( $c ) {
$this->renew( $order, $c );
},
10,
2
);
add_action(
'woocommerce_scheduled_subscription_payment_' . CreditCardGateway::ID,
function ( $amount, $order ) use ( $c ) {
$this->renew( $order, $c );
},
10,
2
);
add_action(
'woocommerce_subscription_payment_complete',
function ( $subscription ) use ( $c ) {
if ( ! in_array( $subscription->get_payment_method(), array( PayPalGateway::ID, CreditCardGateway::ID, CardButtonGateway::ID ), true ) ) {
return;
}
$paypal_subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $paypal_subscription_id ) {
return;
}
$payment_token_repository = $c->get( 'vaulting.repository.payment-token' );
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$this->add_payment_token_id( $subscription, $payment_token_repository, $logger );
if ( count( $subscription->get_related_orders() ) === 1 ) {
$parent_order = $subscription->get_parent();
if ( is_a( $parent_order, WC_Order::class ) ) {
$order_repository = $c->get( 'api.repository.order' );
$order = $order_repository->for_wc_order( $parent_order );
$transaction_id = $this->get_paypal_order_transaction_id( $order );
if ( $transaction_id ) {
$subscription->update_meta_data( 'ppcp_previous_transaction_reference', $transaction_id );
$subscription->save();
}
}
}
}
);
add_filter(
'woocommerce_gateway_description',
function ( $description, $id ) use ( $c ) {
$payment_token_repository = $c->get( 'vaulting.repository.payment-token' );
$settings = $c->get( 'wcgateway.settings' );
$subscription_helper = $c->get( 'subscription.helper' );
return $this->display_saved_paypal_payments( $settings, (string) $id, $payment_token_repository, (string) $description, $subscription_helper );
},
10,
2
);
add_filter(
'woocommerce_credit_card_form_fields',
function ( $default_fields, $id ) use ( $c ) {
$payment_token_repository = $c->get( 'vaulting.repository.payment-token' );
$settings = $c->get( 'wcgateway.settings' );
$subscription_helper = $c->get( 'subscription.helper' );
return $this->display_saved_credit_cards( $settings, $id, $payment_token_repository, $default_fields, $subscription_helper );
},
20,
2
);
add_filter(
'ppcp_create_order_request_body_data',
function( array $data ) use ( $c ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$wc_order_action = wc_clean( wp_unslash( $_POST['wc_order_action'] ?? '' ) );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$subscription_id = wc_clean( wp_unslash( $_POST['post_ID'] ?? '' ) );
if ( ! $subscription_id ) {
return $data;
}
$subscription = wc_get_order( $subscription_id );
if ( ! is_a( $subscription, WC_Subscription::class ) ) {
return $data;
}
if (
$wc_order_action === 'wcs_process_renewal' && $subscription->get_payment_method() === CreditCardGateway::ID
&& isset( $data['payment_source']['token'] ) && $data['payment_source']['token']['type'] === 'PAYMENT_METHOD_TOKEN'
&& isset( $data['payment_source']['token']['source']->card )
) {
$data['payment_source'] = array(
'card' => array(
'vault_id' => $data['payment_source']['token']['id'],
'stored_credential' => array(
'payment_initiator' => 'MERCHANT',
'payment_type' => 'RECURRING',
'usage' => 'SUBSEQUENT',
),
),
);
$previous_transaction_reference = $subscription->get_meta( 'ppcp_previous_transaction_reference' );
if ( $previous_transaction_reference ) {
$data['payment_source']['card']['stored_credential']['previous_transaction_reference'] = $previous_transaction_reference;
}
}
return $data;
}
);
$this->subscriptions_api_integration( $c );
add_action(
'admin_enqueue_scripts',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $hook ) use ( $c ) {
if ( ! is_string( $hook ) ) {
return;
}
$settings = $c->get( 'wcgateway.settings' );
$subscription_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : '';
if ( $hook !== 'post.php' || $subscription_mode !== 'subscriptions_api' ) {
return;
}
//phpcs:disable WordPress.Security.NonceVerification.Recommended
$post_id = wc_clean( wp_unslash( $_GET['post'] ?? '' ) );
$product = wc_get_product( $post_id );
if ( ! ( is_a( $product, WC_Product::class ) ) ) {
return;
}
$subscriptions_helper = $c->get( 'subscription.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
if (
! $subscriptions_helper->plugin_is_active()
|| ! (
is_a( $product, WC_Product_Subscription::class )
|| is_a( $product, WC_Product_Variable_Subscription::class )
|| is_a( $product, WC_Product_Subscription_Variation::class )
)
|| ! WC_Subscriptions_Product::is_subscription( $product )
) {
return;
}
$module_url = $c->get( 'subscription.module.url' );
wp_enqueue_script(
'ppcp-paypal-subscription',
untrailingslashit( $module_url ) . '/assets/js/paypal-subscription.js',
array( 'jquery' ),
$c->get( 'ppcp.asset-version' ),
true
);
$products = array( $this->set_product_config( $product ) );
if ( $product->get_type() === 'variable-subscription' ) {
$products = array();
/**
* Suppress pslam.
*
* @psalm-suppress TypeDoesNotContainType
*
* WC_Product_Variable_Subscription extends WC_Product_Variable.
*/
assert( $product instanceof WC_Product_Variable );
$available_variations = $product->get_available_variations();
foreach ( $available_variations as $variation ) {
/**
* The method is defined in WooCommerce.
*
* @psalm-suppress UndefinedMethod
*/
$variation = wc_get_product_object( 'variation', $variation['variation_id'] );
$products[] = $this->set_product_config( $variation );
}
}
wp_localize_script(
'ppcp-paypal-subscription',
'PayPalCommerceGatewayPayPalSubscriptionProducts',
$products
);
}
);
$endpoint = $c->get( 'subscription.deactivate-plan-endpoint' );
assert( $endpoint instanceof DeactivatePlanEndpoint );
add_action(
'wc_ajax_' . DeactivatePlanEndpoint::ENDPOINT,
array( $endpoint, 'handle_request' )
);
add_action(
'add_meta_boxes',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( string $post_type, $post_or_order_object ) use ( $c ) {
if ( ! function_exists( 'wcs_get_subscription' ) ) {
return;
}
$order = ( $post_or_order_object instanceof WP_Post )
? wc_get_order( $post_or_order_object->ID )
: $post_or_order_object;
if ( ! is_a( $order, WC_Order::class ) ) {
return;
}
$subscription = wcs_get_subscription( $order->get_id() );
if ( ! is_a( $subscription, WC_Subscription::class ) ) {
return;
}
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( ! $subscription_id ) {
return;
}
$screen_id = wc_get_page_screen_id( 'shop_subscription' );
remove_meta_box( 'woocommerce-subscription-schedule', $screen_id, 'side' );
$environment = $c->get( 'onboarding.environment' );
add_meta_box(
'ppcp_paypal_subscription',
__( 'PayPal Subscription', 'woocommerce-paypal-payments' ),
function() use ( $subscription_id, $environment ) {
$host = $environment->current_environment_is( Environment::SANDBOX ) ? 'https://www.sandbox.paypal.com' : 'https://www.paypal.com';
$url = trailingslashit( $host ) . 'billing/subscriptions/' . $subscription_id;
echo '<p>' . esc_html__( 'This subscription is linked to a PayPal Subscription, Cancel it to unlink.', 'woocommerce-paypal-payments' ) . '</p>';
echo '<p><strong>' . esc_html__( 'Subscription:', 'woocommerce-paypal-payments' ) . '</strong> <a href="' . esc_url( $url ) . '" target="_blank">' . esc_attr( $subscription_id ) . '</a></p>';
},
$post_type,
'side'
);
},
30,
2
);
add_action(
'action_scheduler_before_execute',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $action_id ) {
/**
* Class exist in WooCommerce.
*
* @psalm-suppress UndefinedClass
*/
$store = ActionScheduler_Store::instance();
$action = $store->fetch_action( $action_id );
$subscription_id = $action->get_args()['subscription_id'] ?? null;
if ( $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
if ( is_a( $subscription, WC_Subscription::class ) ) {
$paypal_subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $paypal_subscription_id ) {
as_unschedule_action( $action->get_hook(), $action->get_args() );
}
}
}
}
);
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
}
/**
* Handles a Subscription product renewal.
*
* @param \WC_Order $order WooCommerce order.
* @param ContainerInterface|null $container The container.
* @return void
*/
protected function renew( $order, $container ) {
if ( ! ( $order instanceof \WC_Order ) ) {
return;
}
$handler = $container->get( 'subscription.renewal-handler' );
$handler->renew( $order );
}
/**
* Adds Payment token ID to subscription.
*
* @param \WC_Subscription $subscription The subscription.
* @param PaymentTokenRepository $payment_token_repository The payment repository.
* @param LoggerInterface $logger The logger.
*/
protected function add_payment_token_id(
\WC_Subscription $subscription,
PaymentTokenRepository $payment_token_repository,
LoggerInterface $logger
) {
try {
$tokens = $payment_token_repository->all_for_user_id( $subscription->get_customer_id() );
if ( $tokens ) {
$latest_token_id = end( $tokens )->id() ? end( $tokens )->id() : '';
$subscription->update_meta_data( 'payment_token_id', $latest_token_id );
$subscription->save();
}
} catch ( RuntimeException $error ) {
$message = sprintf(
// translators: %1$s is the payment token Id, %2$s is the error message.
__(
'Could not add token Id to subscription %1$s: %2$s',
'woocommerce-paypal-payments'
),
$subscription->get_id(),
$error->getMessage()
);
$logger->log( 'warning', $message );
}
}
/**
* Displays saved PayPal payments.
*
* @param Settings $settings The settings.
* @param string $id The payment gateway Id.
* @param PaymentTokenRepository $payment_token_repository The payment token repository.
* @param string $description The payment gateway description.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @return string
*/
protected function display_saved_paypal_payments(
Settings $settings,
string $id,
PaymentTokenRepository $payment_token_repository,
string $description,
SubscriptionHelper $subscription_helper
): string {
if ( $settings->has( 'vault_enabled' )
&& $settings->get( 'vault_enabled' )
&& PayPalGateway::ID === $id
&& $subscription_helper->is_subscription_change_payment()
) {
$tokens = $payment_token_repository->all_for_user_id( get_current_user_id() );
if ( ! $tokens || ! $payment_token_repository->tokens_contains_paypal( $tokens ) ) {
return esc_html__(
'No PayPal payments saved, in order to use a saved payment you first need to create it through a purchase.',
'woocommerce-paypal-payments'
);
}
$output = sprintf(
'<p class="form-row form-row-wide"><label>%1$s</label><select id="saved-paypal-payment" name="saved_paypal_payment">',
esc_html__( 'Select a saved PayPal payment', 'woocommerce-paypal-payments' )
);
foreach ( $tokens as $token ) {
if ( isset( $token->source()->paypal ) ) {
$output .= sprintf(
'<option value="%1$s">%2$s</option>',
$token->id(),
$token->source()->paypal->payer->email_address
);
}
}
$output .= '</select></p>';
return $output;
}
return $description;
}
/**
* Displays saved credit cards.
*
* @param Settings $settings The settings.
* @param string $id The payment gateway Id.
* @param PaymentTokenRepository $payment_token_repository The payment token repository.
* @param array $default_fields Default payment gateway fields.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @return array|mixed|string
* @throws NotFoundException When setting was not found.
*/
protected function display_saved_credit_cards(
Settings $settings,
string $id,
PaymentTokenRepository $payment_token_repository,
array $default_fields,
SubscriptionHelper $subscription_helper
) {
if ( $settings->has( 'vault_enabled_dcc' )
&& $settings->get( 'vault_enabled_dcc' )
&& $subscription_helper->is_subscription_change_payment()
&& CreditCardGateway::ID === $id
) {
$tokens = $payment_token_repository->all_for_user_id( get_current_user_id() );
if ( ! $tokens || ! $payment_token_repository->tokens_contains_card( $tokens ) ) {
$default_fields = array();
$default_fields['saved-credit-card'] = esc_html__(
'No Credit Card saved, in order to use a saved Credit Card you first need to create it through a purchase.',
'woocommerce-paypal-payments'
);
return $default_fields;
}
$output = sprintf(
'<p class="form-row form-row-wide"><label>%1$s</label><select id="saved-credit-card" name="saved_credit_card">',
esc_html__( 'Select a saved Credit Card payment', 'woocommerce-paypal-payments' )
);
foreach ( $tokens as $token ) {
if ( isset( $token->source()->card ) ) {
$output .= sprintf(
'<option value="%1$s">%2$s ...%3$s</option>',
$token->id(),
$token->source()->card->brand,
$token->source()->card->last_digits
);
}
}
$output .= '</select></p>';
$default_fields = array();
$default_fields['saved-credit-card'] = $output;
return $default_fields;
}
return $default_fields;
}
/**
* Adds PayPal subscriptions API integration.
*
* @param ContainerInterface $c The container.
* @return void
* @throws Exception When something went wrong.
*/
protected function subscriptions_api_integration( ContainerInterface $c ): void {
add_action( add_action(
'save_post', 'save_post',
/** /**
@ -554,7 +80,7 @@ class SubscriptionModule implements ModuleInterface {
return; return;
} }
$subscriptions_api_handler = $c->get( 'subscription.api-handler' ); $subscriptions_api_handler = $c->get( 'paypal-subscriptions.api-handler' );
assert( $subscriptions_api_handler instanceof SubscriptionsApiHandler ); assert( $subscriptions_api_handler instanceof SubscriptionsApiHandler );
$this->update_subscription_product_meta( $product, $subscriptions_api_handler ); $this->update_subscription_product_meta( $product, $subscriptions_api_handler );
}, },
@ -571,7 +97,7 @@ class SubscriptionModule implements ModuleInterface {
function( $variation_id ) use ( $c ) { function( $variation_id ) use ( $c ) {
$wcsnonce_save_variations = wc_clean( wp_unslash( $_POST['_wcsnonce_save_variations'] ?? '' ) ); $wcsnonce_save_variations = wc_clean( wp_unslash( $_POST['_wcsnonce_save_variations'] ?? '' ) );
$subscriptions_helper = $c->get( 'subscription.helper' ); $subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper ); assert( $subscriptions_helper instanceof SubscriptionHelper );
if ( if (
@ -588,7 +114,7 @@ class SubscriptionModule implements ModuleInterface {
return; return;
} }
$subscriptions_api_handler = $c->get( 'subscription.api-handler' ); $subscriptions_api_handler = $c->get( 'paypal-subscriptions.api-handler' );
assert( $subscriptions_api_handler instanceof SubscriptionsApiHandler ); assert( $subscriptions_api_handler instanceof SubscriptionsApiHandler );
$this->update_subscription_product_meta( $product, $subscriptions_api_handler ); $this->update_subscription_product_meta( $product, $subscriptions_api_handler );
}, },
@ -947,6 +473,236 @@ class SubscriptionModule implements ModuleInterface {
10, 10,
3 3
); );
add_action(
'admin_enqueue_scripts',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $hook ) use ( $c ) {
if ( ! is_string( $hook ) ) {
return;
}
$settings = $c->get( 'wcgateway.settings' );
$subscription_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : '';
if ( $hook !== 'post.php' || $subscription_mode !== 'subscriptions_api' ) {
return;
}
//phpcs:disable WordPress.Security.NonceVerification.Recommended
$post_id = wc_clean( wp_unslash( $_GET['post'] ?? '' ) );
$product = wc_get_product( $post_id );
if ( ! ( is_a( $product, WC_Product::class ) ) ) {
return;
}
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
if (
! $subscriptions_helper->plugin_is_active()
|| ! (
is_a( $product, WC_Product_Subscription::class )
|| is_a( $product, WC_Product_Variable_Subscription::class )
|| is_a( $product, WC_Product_Subscription_Variation::class )
)
|| ! WC_Subscriptions_Product::is_subscription( $product )
) {
return;
}
$module_url = $c->get( 'paypal-subscriptions.module.url' );
wp_enqueue_script(
'ppcp-paypal-subscription',
untrailingslashit( $module_url ) . '/assets/js/paypal-subscription.js',
array( 'jquery' ),
$c->get( 'ppcp.asset-version' ),
true
);
$products = array( $this->set_product_config( $product ) );
if ( $product->get_type() === 'variable-subscription' ) {
$products = array();
/**
* Suppress pslam.
*
* @psalm-suppress TypeDoesNotContainType
*
* WC_Product_Variable_Subscription extends WC_Product_Variable.
*/
assert( $product instanceof WC_Product_Variable );
$available_variations = $product->get_available_variations();
foreach ( $available_variations as $variation ) {
/**
* The method is defined in WooCommerce.
*
* @psalm-suppress UndefinedMethod
*/
$variation = wc_get_product_object( 'variation', $variation['variation_id'] );
$products[] = $this->set_product_config( $variation );
}
}
wp_localize_script(
'ppcp-paypal-subscription',
'PayPalCommerceGatewayPayPalSubscriptionProducts',
$products
);
}
);
$endpoint = $c->get( 'paypal-subscriptions.deactivate-plan-endpoint' );
assert( $endpoint instanceof DeactivatePlanEndpoint );
add_action(
'wc_ajax_' . DeactivatePlanEndpoint::ENDPOINT,
array( $endpoint, 'handle_request' )
);
add_action(
'add_meta_boxes',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( string $post_type, $post_or_order_object ) use ( $c ) {
if ( ! function_exists( 'wcs_get_subscription' ) ) {
return;
}
$order = ( $post_or_order_object instanceof WP_Post )
? wc_get_order( $post_or_order_object->ID )
: $post_or_order_object;
if ( ! is_a( $order, WC_Order::class ) ) {
return;
}
$subscription = wcs_get_subscription( $order->get_id() );
if ( ! is_a( $subscription, WC_Subscription::class ) ) {
return;
}
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( ! $subscription_id ) {
return;
}
$screen_id = wc_get_page_screen_id( 'shop_subscription' );
remove_meta_box( 'woocommerce-subscription-schedule', $screen_id, 'side' );
$host = $c->get( 'api.paypal-website-url' );
add_meta_box(
'ppcp_paypal_subscription',
__( 'PayPal Subscription', 'woocommerce-paypal-payments' ),
function() use ( $subscription_id, $host ) {
$url = trailingslashit( $host ) . 'billing/subscriptions/' . $subscription_id;
echo '<p>' . esc_html__( 'This subscription is linked to a PayPal Subscription, Cancel it to unlink.', 'woocommerce-paypal-payments' ) . '</p>';
echo '<p><strong>' . esc_html__( 'Subscription:', 'woocommerce-paypal-payments' ) . '</strong> <a href="' . esc_url( $url ) . '" target="_blank">' . esc_attr( $subscription_id ) . '</a></p>';
},
$post_type,
'side'
);
},
30,
2
);
add_action(
'action_scheduler_before_execute',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $action_id ) {
/**
* Class exist in WooCommerce.
*
* @psalm-suppress UndefinedClass
*/
$store = ActionScheduler_Store::instance();
$action = $store->fetch_action( $action_id );
$subscription_id = $action->get_args()['subscription_id'] ?? null;
if ( $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
if ( is_a( $subscription, WC_Subscription::class ) ) {
$paypal_subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $paypal_subscription_id ) {
as_unschedule_action( $action->get_hook(), $action->get_args() );
}
}
}
}
);
}
/**
* Updates subscription product meta.
*
* @param WC_Product $product The product.
* @param SubscriptionsApiHandler $subscriptions_api_handler The subscription api handler.
* @return void
*/
private function update_subscription_product_meta( WC_Product $product, SubscriptionsApiHandler $subscriptions_api_handler ): void {
// phpcs:ignore WordPress.Security.NonceVerification
$enable_subscription_product = wc_clean( wp_unslash( $_POST['_ppcp_enable_subscription_product'] ?? '' ) );
$product->update_meta_data( '_ppcp_enable_subscription_product', $enable_subscription_product );
$product->save();
if ( ( $product->get_type() === 'subscription' || $product->get_type() === 'subscription_variation' ) && $enable_subscription_product === 'yes' ) {
if ( $product->meta_exists( 'ppcp_subscription_product' ) && $product->meta_exists( 'ppcp_subscription_plan' ) ) {
$subscriptions_api_handler->update_product( $product );
$subscriptions_api_handler->update_plan( $product );
return;
}
if ( ! $product->meta_exists( 'ppcp_subscription_product' ) ) {
$subscriptions_api_handler->create_product( $product );
}
if ( $product->meta_exists( 'ppcp_subscription_product' ) && ! $product->meta_exists( 'ppcp_subscription_plan' ) ) {
// phpcs:ignore WordPress.Security.NonceVerification
$subscription_plan_name = wc_clean( wp_unslash( $_POST['_ppcp_subscription_plan_name'] ?? '' ) );
if ( ! is_string( $subscription_plan_name ) ) {
return;
}
$product->update_meta_data( '_ppcp_subscription_plan_name', $subscription_plan_name );
$product->save();
$subscriptions_api_handler->create_plan( $subscription_plan_name, $product );
}
}
}
/**
* Returns subscription product configuration.
*
* @param WC_Product $product The product.
* @return array
*/
private function set_product_config( WC_Product $product ): array {
$plan = $product->get_meta( 'ppcp_subscription_plan' ) ?? array();
$plan_id = $plan['id'] ?? '';
return array(
'product_connected' => $product->get_meta( '_ppcp_enable_subscription_product' ) ?? '',
'plan_id' => $plan_id,
'product_id' => $product->get_id(),
'ajax' => array(
'deactivate_plan' => array(
'endpoint' => \WC_AJAX::get_endpoint( DeactivatePlanEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( DeactivatePlanEndpoint::ENDPOINT ),
),
),
);
} }
/** /**
@ -1019,66 +775,4 @@ class SubscriptionModule implements ModuleInterface {
); );
} }
} }
/**
* Updates subscription product meta.
*
* @param WC_Product $product The product.
* @param SubscriptionsApiHandler $subscriptions_api_handler The subscription api handler.
* @return void
*/
private function update_subscription_product_meta( WC_Product $product, SubscriptionsApiHandler $subscriptions_api_handler ): void {
// phpcs:ignore WordPress.Security.NonceVerification
$enable_subscription_product = wc_clean( wp_unslash( $_POST['_ppcp_enable_subscription_product'] ?? '' ) );
$product->update_meta_data( '_ppcp_enable_subscription_product', $enable_subscription_product );
$product->save();
if ( ( $product->get_type() === 'subscription' || $product->get_type() === 'subscription_variation' ) && $enable_subscription_product === 'yes' ) {
if ( $product->meta_exists( 'ppcp_subscription_product' ) && $product->meta_exists( 'ppcp_subscription_plan' ) ) {
$subscriptions_api_handler->update_product( $product );
$subscriptions_api_handler->update_plan( $product );
return;
}
if ( ! $product->meta_exists( 'ppcp_subscription_product' ) ) {
$subscriptions_api_handler->create_product( $product );
}
if ( $product->meta_exists( 'ppcp_subscription_product' ) && ! $product->meta_exists( 'ppcp_subscription_plan' ) ) {
// phpcs:ignore WordPress.Security.NonceVerification
$subscription_plan_name = wc_clean( wp_unslash( $_POST['_ppcp_subscription_plan_name'] ?? '' ) );
if ( ! is_string( $subscription_plan_name ) ) {
return;
}
$product->update_meta_data( '_ppcp_subscription_plan_name', $subscription_plan_name );
$product->save();
$subscriptions_api_handler->create_plan( $subscription_plan_name, $product );
}
}
}
/**
* Returns subscription product configuration.
*
* @param WC_Product $product The product.
* @return array
*/
private function set_product_config( WC_Product $product ): array {
$plan = $product->get_meta( 'ppcp_subscription_plan' ) ?? array();
$plan_id = $plan['id'] ?? '';
return array(
'product_connected' => $product->get_meta( '_ppcp_enable_subscription_product' ) ?? '',
'plan_id' => $plan_id,
'product_id' => $product->get_id(),
'ajax' => array(
'deactivate_plan' => array(
'endpoint' => \WC_AJAX::get_endpoint( DeactivatePlanEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( DeactivatePlanEndpoint::ENDPOINT ),
),
),
);
}
} }

View file

@ -2,12 +2,12 @@
/** /**
* The subscription module. * The subscription module.
* *
* @package WooCommerce\PayPalCommerce\Subscription * @package WooCommerce\PayPalCommerce\WcSubscriptions
*/ */
declare(strict_types=1); declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription; namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WC_Product; use WC_Product;

View file

@ -0,0 +1,14 @@
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3.25.0"
}
],
[
"@babel/preset-react"
]
]
}

View file

@ -0,0 +1,3 @@
node_modules
assets/js
assets/css

View file

@ -1,7 +1,7 @@
{ {
"name": "woocommerce/ppcp-subscription", "name": "woocommerce/ppcp-save-payment-methods",
"type": "dhii-mod", "type": "dhii-mod",
"description": "Subscription module for PPCP", "description": "Save payment methods module for PPCP",
"license": "GPL-2.0", "license": "GPL-2.0",
"require": { "require": {
"php": "^7.2 | ^8.0", "php": "^7.2 | ^8.0",
@ -9,7 +9,7 @@
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"WooCommerce\\PayPalCommerce\\Subscription\\": "src" "WooCommerce\\PayPalCommerce\\SavePaymentMethods\\": "src"
} }
}, },
"minimum-stability": "dev", "minimum-stability": "dev",

View file

@ -0,0 +1,14 @@
<?php
/**
* The save payment methods module extensions.
*
* @package WooCommerce\PayPalCommerce\SavePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array();

View file

@ -0,0 +1,16 @@
<?php
/**
* The save payment methods module.
*
* @package WooCommerce\PayPalCommerce\SavePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return new SavePaymentMethodsModule();
};

View file

@ -0,0 +1,34 @@
{
"name": "ppcp-save-payment-methods",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"browserslist": [
"> 0.5%",
"Safari >= 8",
"Chrome >= 41",
"Firefox >= 43",
"Edge >= 14"
],
"dependencies": {
"core-js": "^3.25.0",
"@paypal/paypal-js": "^6.0.0"
},
"devDependencies": {
"@babel/core": "^7.19",
"@babel/preset-env": "^7.19",
"@babel/preset-react": "^7.18.6",
"@woocommerce/dependency-extraction-webpack-plugin": "^2.2.0",
"babel-loader": "^8.2",
"cross-env": "^7.0.3",
"file-loader": "^6.2.0",
"sass": "^1.42.1",
"sass-loader": "^12.1.0",
"webpack": "^5.76",
"webpack-cli": "^4.10"
},
"scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",
"watch": "cross-env BABEL_ENV=default NODE_ENV=production webpack --watch",
"dev": "cross-env BABEL_ENV=default webpack --watch"
}
}

View file

@ -0,0 +1,182 @@
import {
getCurrentPaymentMethod,
ORDER_BUTTON_SELECTOR,
PaymentMethods
} from "../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState";
import {loadScript} from "@paypal/paypal-js";
import {
setVisible,
setVisibleByClass
} from "../../../ppcp-button/resources/js/modules/Helper/Hiding";
import ErrorHandler from "../../../ppcp-button/resources/js/modules/ErrorHandler";
import {cardFieldStyles} from "../../../ppcp-button/resources/js/modules/Helper/CardFieldsHelper";
const errorHandler = new ErrorHandler(
PayPalCommerceGateway.labels.error.generic,
document.querySelector('.woocommerce-notices-wrapper')
);
const init = () => {
setVisibleByClass(ORDER_BUTTON_SELECTOR, getCurrentPaymentMethod() !== PaymentMethods.PAYPAL, 'ppcp-hidden');
setVisible(`#ppc-button-${PaymentMethods.PAYPAL}-save-payment-method`, getCurrentPaymentMethod() === PaymentMethods.PAYPAL);
}
document.addEventListener(
'DOMContentLoaded',
() => {
jQuery(document.body).on('click init_add_payment_method', '.payment_methods input.input-radio', function () {
init()
});
setTimeout(() => {
loadScript({
clientId: ppcp_add_payment_method.client_id,
merchantId: ppcp_add_payment_method.merchant_id,
dataUserIdToken: ppcp_add_payment_method.id_token,
components: 'buttons,card-fields',
})
.then((paypal) => {
errorHandler.clear();
paypal.Buttons(
{
createVaultSetupToken: async () => {
const response = await fetch(ppcp_add_payment_method.ajax.create_setup_token.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: ppcp_add_payment_method.ajax.create_setup_token.nonce,
})
})
const result = await response.json()
if (result.data.id) {
return result.data.id
}
errorHandler.message(ppcp_add_payment_method.error_message);
},
onApprove: async ({vaultSetupToken}) => {
const response = await fetch(ppcp_add_payment_method.ajax.create_payment_token.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: ppcp_add_payment_method.ajax.create_payment_token.nonce,
vault_setup_token: vaultSetupToken,
})
})
const result = await response.json();
if(result.success === true) {
window.location.href = ppcp_add_payment_method.payment_methods_page;
return;
}
errorHandler.message(ppcp_add_payment_method.error_message);
},
onError: (error) => {
console.error(error)
errorHandler.message(ppcp_add_payment_method.error_message);
}
},
).render(`#ppc-button-${PaymentMethods.PAYPAL}-save-payment-method`);
const cardField = paypal.CardFields({
createVaultSetupToken: async () => {
const response = await fetch(ppcp_add_payment_method.ajax.create_setup_token.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: ppcp_add_payment_method.ajax.create_setup_token.nonce,
payment_method: PaymentMethods.CARDS,
verification_method: ppcp_add_payment_method.verification_method
})
})
const result = await response.json()
if (result.data.id) {
return result.data.id
}
errorHandler.message(ppcp_add_payment_method.error_message);
},
onApprove: async ({vaultSetupToken}) => {
const response = await fetch(ppcp_add_payment_method.ajax.create_payment_token.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: ppcp_add_payment_method.ajax.create_payment_token.nonce,
vault_setup_token: vaultSetupToken,
payment_method: PaymentMethods.CARDS
})
})
const result = await response.json();
if(result.success === true) {
window.location.href = ppcp_add_payment_method.payment_methods_page;
return;
}
errorHandler.message(ppcp_add_payment_method.error_message);
},
onError: (error) => {
console.error(error)
errorHandler.message(ppcp_add_payment_method.error_message);
}
});
if (cardField.isEligible()) {
const nameField = document.getElementById('ppcp-credit-card-gateway-card-name');
if (nameField) {
let styles = cardFieldStyles(nameField);
cardField.NameField({style: {'input': styles}}).render(nameField.parentNode);
nameField.hidden = true;
}
const numberField = document.getElementById('ppcp-credit-card-gateway-card-number');
if (numberField) {
let styles = cardFieldStyles(numberField);
cardField.NumberField({style: {'input': styles}}).render(numberField.parentNode);
numberField.hidden = true;
}
const expiryField = document.getElementById('ppcp-credit-card-gateway-card-expiry');
if (expiryField) {
let styles = cardFieldStyles(expiryField);
cardField.ExpiryField({style: {'input': styles}}).render(expiryField.parentNode);
expiryField.hidden = true;
}
const cvvField = document.getElementById('ppcp-credit-card-gateway-card-cvc');
if (cvvField) {
let styles = cardFieldStyles(cvvField);
cardField.CVVField({style: {'input': styles}}).render(cvvField.parentNode);
cvvField.hidden = true;
}
}
document.querySelector('#place_order').addEventListener("click", (event) => {
event.preventDefault();
cardField.submit()
.catch((error) => {
console.error(error)
});
});
})
}, 1000)
}
);

View file

@ -0,0 +1,90 @@
<?php
/**
* The save payment methods module services.
*
* @package WooCommerce\PayPalCommerce\SavePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CaptureCardPayment;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Helper\SavePaymentMethodsApplies;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
'save-payment-methods.eligible' => static function ( ContainerInterface $container ): bool {
$save_payment_methods_applies = $container->get( 'save-payment-methods.helpers.save-payment-methods-applies' );
assert( $save_payment_methods_applies instanceof SavePaymentMethodsApplies );
return $save_payment_methods_applies->for_country_currency();
},
'save-payment-methods.helpers.save-payment-methods-applies' => static function ( ContainerInterface $container ) : SavePaymentMethodsApplies {
return new SavePaymentMethodsApplies(
$container->get( 'save-payment-methods.supported-country-currency-matrix' ),
$container->get( 'api.shop.currency' ),
$container->get( 'api.shop.country' )
);
},
'save-payment-methods.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array {
return apply_filters(
'woocommerce_paypal_payments_save_payment_methods_supported_country_currency_matrix',
array(
'US' => array(
'AUD',
'CAD',
'EUR',
'GBP',
'JPY',
'USD',
),
)
);
},
'save-payment-methods.module.url' => static function ( ContainerInterface $container ): string {
/**
* The path cannot be false.
*
* @psalm-suppress PossiblyFalseArgument
*/
return plugins_url(
'/modules/ppcp-save-payment-methods/',
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'save-payment-methods.endpoint.create-setup-token' => static function ( ContainerInterface $container ): CreateSetupToken {
return new CreateSetupToken(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.payment-method-tokens' )
);
},
'save-payment-methods.wc-payment-tokens' => static function( ContainerInterface $container ): WooCommercePaymentTokens {
return new WooCommercePaymentTokens(
$container->get( 'vaulting.payment-token-helper' ),
$container->get( 'vaulting.payment-token-factory' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'save-payment-methods.endpoint.create-payment-token' => static function ( ContainerInterface $container ): CreatePaymentToken {
return new CreatePaymentToken(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.payment-method-tokens' ),
$container->get( 'save-payment-methods.wc-payment-tokens' )
);
},
'save-payment-methods.endpoint.capture-card-payment' => static function( ContainerInterface $container ): CaptureCardPayment {
return new CaptureCardPayment(
$container->get( 'button.request-data' ),
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'api.factory.order' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'session.handler' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
);

View file

@ -0,0 +1,219 @@
<?php
/**
* The Capture Card Payment endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint;
use Psr\Log\LoggerInterface;
use RuntimeException;
use stdClass;
use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\RequestTrait;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WP_Error;
/**
* Class CaptureCardPayment
*/
class CaptureCardPayment implements EndpointInterface {
use RequestTrait;
const ENDPOINT = 'ppc-capture-card-payment';
/**
* The request data.
*
* @var RequestData
*/
private $request_data;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The order factory.
*
* @var OrderFactory
*/
private $order_factory;
/**
* The purchase unit factory.
*
* @var PurchaseUnitFactory
*/
private $purchase_unit_factory;
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/**
* The session handler.
*
* @var SessionHandler
*/
private $session_handler;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* CaptureCardPayment constructor.
*
* @param RequestData $request_data The request data.
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param OrderFactory $order_factory The order factory.
* @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param SessionHandler $session_handler The session handler.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
string $host,
Bearer $bearer,
OrderFactory $order_factory,
PurchaseUnitFactory $purchase_unit_factory,
OrderEndpoint $order_endpoint,
SessionHandler $session_handler,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->host = $host;
$this->bearer = $bearer;
$this->order_factory = $order_factory;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->order_endpoint = $order_endpoint;
$this->logger = $logger;
$this->session_handler = $session_handler;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
*/
public function handle_request(): bool {
$data = $this->request_data->read_request( $this->nonce() );
$tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id() );
foreach ( $tokens as $token ) {
if ( $token->get_id() === (int) $data['payment_token'] ) {
try {
$order = $this->create_order( $token->get_token() );
$id = $order->id ?? '';
$status = $order->status ?? '';
$payment_source = isset( $order->payment_source->card ) ? 'card' : '';
if ( $id && $status && $payment_source ) {
WC()->session->set(
'ppcp_saved_payment_card',
array(
'order_id' => $id,
'status' => $status,
'payment_source' => $payment_source,
)
);
wp_send_json_success();
return true;
}
} catch ( RuntimeException $exception ) {
wp_send_json_error();
return false;
}
}
}
wp_send_json_error();
return false;
}
/**
* Creates PayPal order from the given card vault id.
*
* @param string $vault_id Vault id.
* @return stdClass
* @throws RuntimeException When request fails.
*/
private function create_order( string $vault_id ): stdClass {
$items = array( $this->purchase_unit_factory->from_wc_cart() );
$data = array(
'intent' => 'CAPTURE',
'purchase_units' => array_map(
static function ( PurchaseUnit $item ): array {
return $item->to_array( true, false );
},
$items
),
'payment_source' => array(
'card' => array(
'vault_id' => $vault_id,
),
),
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/checkout/orders';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'PayPal-Request-Id' => uniqid( 'ppcp-', true ),
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
throw new RuntimeException( $response->get_error_message() );
}
return json_decode( $response['body'] );
}
}

View file

@ -0,0 +1,143 @@
<?php
/**
* The Create Payment Token endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint;
use Exception;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\SavePaymentMethods\WooCommercePaymentTokens;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Class CreatePaymentToken
*/
class CreatePaymentToken implements EndpointInterface {
const ENDPOINT = 'ppc-create-payment-token';
/**
* The request data.
*
* @var RequestData
*/
private $request_data;
/**
* The payment method tokens endpoint.
*
* @var PaymentMethodTokensEndpoint
*/
private $payment_method_tokens_endpoint;
/**
* The WC payment tokens.
*
* @var WooCommercePaymentTokens
*/
private $wc_payment_tokens;
/**
* CreatePaymentToken constructor.
*
* @param RequestData $request_data The request data.
* @param PaymentMethodTokensEndpoint $payment_method_tokens_endpoint The payment method tokens endpoint.
* @param WooCommercePaymentTokens $wc_payment_tokens The WC payment tokens.
*/
public function __construct(
RequestData $request_data,
PaymentMethodTokensEndpoint $payment_method_tokens_endpoint,
WooCommercePaymentTokens $wc_payment_tokens
) {
$this->request_data = $request_data;
$this->payment_method_tokens_endpoint = $payment_method_tokens_endpoint;
$this->wc_payment_tokens = $wc_payment_tokens;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
* @throws Exception On Error.
*/
public function handle_request(): bool {
try {
$data = $this->request_data->read_request( $this->nonce() );
/**
* Suppress ArgumentTypeCoercion
*
* @psalm-suppress ArgumentTypeCoercion
*/
$payment_source = new PaymentSource(
'token',
(object) array(
'id' => $data['vault_setup_token'],
'type' => 'SETUP_TOKEN',
)
);
$result = $this->payment_method_tokens_endpoint->payment_tokens( $payment_source );
if ( is_user_logged_in() && isset( $result->customer->id ) ) {
update_user_meta( get_current_user_id(), '_ppcp_target_customer_id', $result->customer->id );
if ( isset( $result->payment_source->paypal ) ) {
$email = '';
if ( isset( $result->payment_source->paypal->email_address ) ) {
$email = $result->payment_source->paypal->email_address;
}
$this->wc_payment_tokens->create_payment_token_paypal(
get_current_user_id(),
$result->id,
$email
);
}
if ( isset( $result->payment_source->card ) ) {
$token = new \WC_Payment_Token_CC();
$token->set_token( $result->id );
$token->set_user_id( get_current_user_id() );
$token->set_gateway_id( CreditCardGateway::ID );
$token->set_last4( $result->payment_source->card->last_digits ?? '' );
$expiry = explode( '-', $result->payment_source->card->expiry ?? '' );
$token->set_expiry_year( $expiry[0] ?? '' );
$token->set_expiry_month( $expiry[1] ?? '' );
$brand = $result->payment_source->card->brand ?? __( 'N/A', 'woocommerce-paypal-payments' );
if ( $brand ) {
$token->set_card_type( $brand );
}
$token->save();
}
}
wp_send_json_success( $result );
return true;
} catch ( Exception $exception ) {
wp_send_json_error();
return false;
}
}
}

View file

@ -0,0 +1,115 @@
<?php
/**
* The Create Setup Token endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint;
use Exception;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
/**
* Class CreateSetupToken
*/
class CreateSetupToken implements EndpointInterface {
const ENDPOINT = 'ppc-create-setup-token';
/**
* The request data helper.
*
* @var RequestData
*/
private $request_data;
/**
* Payment Method Tokens endpoint.
*
* @var PaymentMethodTokensEndpoint
*/
private $payment_method_tokens_endpoint;
/**
* CreateSetupToken constructor.
*
* @param RequestData $request_data The request data helper.
* @param PaymentMethodTokensEndpoint $payment_method_tokens_endpoint Payment Method Tokens endpoint.
*/
public function __construct(
RequestData $request_data,
PaymentMethodTokensEndpoint $payment_method_tokens_endpoint
) {
$this->request_data = $request_data;
$this->payment_method_tokens_endpoint = $payment_method_tokens_endpoint;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
* @throws Exception On Error.
*/
public function handle_request(): bool {
try {
$data = $this->request_data->read_request( $this->nonce() );
$payment_source = new PaymentSource(
'paypal',
(object) array(
'usage_type' => 'MERCHANT',
'experience_context' => (object) array(
'return_url' => esc_url( wc_get_account_endpoint_url( 'payment-methods' ) ),
'cancel_url' => esc_url( wc_get_account_endpoint_url( 'add-payment-method' ) ),
),
)
);
$payment_method = $data['payment_method'] ?? '';
if ( $payment_method === CreditCardGateway::ID ) {
$properties = (object) array();
$verification_method = $data['verification_method'] ?? '';
if ( $verification_method === 'SCA_WHEN_REQUIRED' || $verification_method === 'SCA_ALWAYS' ) {
$properties = (object) array(
'verification_method' => $verification_method,
'usage_type' => 'MERCHANT',
'experience_context' => (object) array(
'return_url' => esc_url( wc_get_account_endpoint_url( 'payment-methods' ) ),
'cancel_url' => esc_url( wc_get_account_endpoint_url( 'add-payment-method' ) ),
),
);
}
$payment_source = new PaymentSource(
'card',
$properties
);
}
$result = $this->payment_method_tokens_endpoint->setup_tokens( $payment_source );
wp_send_json_success( $result );
return true;
} catch ( Exception $exception ) {
wp_send_json_error();
return false;
}
}
}

View file

@ -0,0 +1,66 @@
<?php
/**
* Properties of the Save Payment Methods module.
*
* @package WooCommerce\PayPalCommerce\SavePaymentMethods\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods\Helper;
/**
* Class SavePaymentMethodsApplies
*/
class SavePaymentMethodsApplies {
/**
* The matrix which countries and currency combinations can be used for Save Payment Methods.
*
* @var array
*/
private $allowed_country_currency_matrix;
/**
* 3-letter currency code of the shop.
*
* @var string
*/
private $currency;
/**
* 2-letter country code of the shop.
*
* @var string
*/
private $country;
/**
* SavePaymentMethodsApplies constructor.
*
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for Save Payment Methods.
* @param string $currency 3-letter currency code of the shop.
* @param string $country 2-letter country code of the shop.
*/
public function __construct(
array $allowed_country_currency_matrix,
string $currency,
string $country
) {
$this->allowed_country_currency_matrix = $allowed_country_currency_matrix;
$this->currency = $currency;
$this->country = $country;
}
/**
* Returns whether Save Payment Methods can be used in the current country and the current currency used.
*
* @return bool
*/
public function for_country_currency(): bool {
if ( ! in_array( $this->country, array_keys( $this->allowed_country_currency_matrix ), true ) ) {
return false;
}
return in_array( $this->currency, $this->allowed_country_currency_matrix[ $this->country ], true );
}
}

View file

@ -0,0 +1,377 @@
<?php
/**
* The save payment methods module.
*
* @package WooCommerce\PayPalCommerce\Applepay
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods;
use Psr\Log\LoggerInterface;
use WC_Order;
use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CaptureCardPayment;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class SavePaymentMethodsModule
*/
class SavePaymentMethodsModule implements ModuleInterface {
use ContextTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
if ( ! $c->get( 'save-payment-methods.eligible' ) ) {
return;
}
add_filter(
'woocommerce_paypal_payments_localized_script_data',
function( array $localized_script_data ) use ( $c ) {
$api = $c->get( 'api.user-id-token' );
assert( $api instanceof UserIdToken );
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
$localized_script_data = $this->add_id_token_to_script_data( $api, $logger, $localized_script_data );
$localized_script_data['ajax']['capture_card_payment'] = array(
'endpoint' => \WC_AJAX::get_endpoint( CaptureCardPayment::ENDPOINT ),
'nonce' => wp_create_nonce( CaptureCardPayment::nonce() ),
);
return $localized_script_data;
}
);
// Adds attributes needed to save payment method.
add_filter(
'ppcp_create_order_request_body_data',
function( array $data, string $payment_method, array $request_data ): array {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$wc_order_action = wc_clean( wp_unslash( $_POST['wc_order_action'] ?? '' ) );
if ( $wc_order_action === 'wcs_process_renewal' ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$subscription_id = wc_clean( wp_unslash( $_POST['post_ID'] ?? '' ) );
$subscription = wcs_get_subscription( (int) $subscription_id );
if ( $subscription ) {
$customer_id = $subscription->get_customer_id();
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, PayPalGateway::ID );
foreach ( $wc_tokens as $token ) {
$data['payment_source'] = array(
'paypal' => array(
'vault_id' => $token->get_token(),
),
);
return $data;
}
}
}
if ( $payment_method === CreditCardGateway::ID ) {
$save_payment_method = $request_data['save_payment_method'] ?? false;
if ( $save_payment_method ) {
$data['payment_source'] = array(
'card' => array(
'attributes' => array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
),
),
),
);
$target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
if ( $target_customer_id ) {
$data['payment_source']['card']['attributes']['customer'] = array(
'id' => $target_customer_id,
);
}
}
}
if ( $payment_method === PayPalGateway::ID ) {
$data['payment_source'] = array(
'paypal' => array(
'attributes' => array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
'usage_type' => 'MERCHANT',
),
),
),
);
}
return $data;
},
10,
3
);
add_action(
'woocommerce_paypal_payments_after_order_processor',
function( WC_Order $wc_order, Order $order ) use ( $c ) {
$payment_source = $order->payment_source();
assert( $payment_source instanceof PaymentSource );
$payment_vault_attributes = $payment_source->properties()->attributes->vault ?? null;
if ( $payment_vault_attributes ) {
$customer_id = $payment_vault_attributes->customer->id ?? '';
$token_id = $payment_vault_attributes->id ?? '';
if ( ! $customer_id || ! $token_id ) {
return;
}
update_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', $customer_id );
$wc_payment_tokens = $c->get( 'save-payment-methods.wc-payment-tokens' );
assert( $wc_payment_tokens instanceof WooCommercePaymentTokens );
if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) {
$token = new \WC_Payment_Token_CC();
$token->set_token( $token_id );
$token->set_user_id( $wc_order->get_customer_id() );
$token->set_gateway_id( CreditCardGateway::ID );
$token->set_last4( $payment_source->properties()->last_digits ?? '' );
$expiry = explode( '-', $payment_source->properties()->expiry ?? '' );
$token->set_expiry_year( $expiry[0] ?? '' );
$token->set_expiry_month( $expiry[1] ?? '' );
$token->set_card_type( $payment_source->properties()->brand ?? '' );
$token->save();
}
if ( $wc_order->get_payment_method() === PayPalGateway::ID ) {
$wc_payment_tokens->create_payment_token_paypal(
$wc_order->get_customer_id(),
$token_id,
$payment_source->properties()->email_address ?? ''
);
}
}
},
10,
2
);
add_filter( 'woocommerce_paypal_payments_disable_add_payment_method', '__return_false' );
add_filter( 'woocommerce_paypal_payments_subscription_renewal_return_before_create_order_without_token', '__return_false' );
add_filter( 'woocommerce_paypal_payments_should_render_card_custom_fields', '__return_false' );
add_action(
'wp_enqueue_scripts',
function() use ( $c ) {
if ( ! is_user_logged_in() || ! $this->is_add_payment_method_page() ) {
return;
}
$module_url = $c->get( 'save-payment-methods.module.url' );
wp_enqueue_script(
'ppcp-add-payment-method',
untrailingslashit( $module_url ) . '/assets/js/add-payment-method.js',
array( 'jquery' ),
$c->get( 'ppcp.asset-version' ),
true
);
$api = $c->get( 'api.user-id-token' );
assert( $api instanceof UserIdToken );
try {
$target_customer_id = '';
if ( is_user_logged_in() ) {
$target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
}
$id_token = $api->id_token( $target_customer_id );
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$verification_method = $settings->has( '3d_secure_contingency' ) ? $settings->get( '3d_secure_contingency' ) : '';
wp_localize_script(
'ppcp-add-payment-method',
'ppcp_add_payment_method',
array(
'client_id' => $c->get( 'button.client_id' ),
'merchant_id' => $c->get( 'api.merchant_id' ),
'id_token' => $id_token,
'payment_methods_page' => wc_get_account_endpoint_url( 'payment-methods' ),
'error_message' => __( 'Could not save payment method.', 'woocommerce-paypal-payments' ),
'verification_method' => $verification_method,
'ajax' => array(
'create_setup_token' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreateSetupToken::ENDPOINT ),
'nonce' => wp_create_nonce( CreateSetupToken::nonce() ),
),
'create_payment_token' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentToken::ENDPOINT ),
'nonce' => wp_create_nonce( CreatePaymentToken::nonce() ),
),
),
)
);
} catch ( RuntimeException $exception ) {
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger->error( $error );
}
}
);
add_action(
'woocommerce_add_payment_method_form_bottom',
function () {
if ( ! is_user_logged_in() || ! is_add_payment_method_page() ) {
return;
}
echo '<div id="ppc-button-' . esc_attr( PayPalGateway::ID ) . '-save-payment-method"></div>';
}
);
add_action(
'wc_ajax_' . CreateSetupToken::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'save-payment-methods.endpoint.create-setup-token' );
assert( $endpoint instanceof CreateSetupToken );
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . CreatePaymentToken::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token' );
assert( $endpoint instanceof CreatePaymentToken );
$endpoint->handle_request();
}
);
add_action(
'woocommerce_paypal_payments_before_delete_payment_token',
function( string $token_id ) use ( $c ) {
try {
$endpoint = $c->get( 'api.endpoint.payment-tokens' );
assert( $endpoint instanceof PaymentTokensEndpoint );
$endpoint->delete( $token_id );
} catch ( RuntimeException $exception ) {
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger->error( $error );
}
}
);
add_filter(
'woocommerce_paypal_payments_credit_card_gateway_vault_supports',
function( array $supports ) use ( $c ): array {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof ContainerInterface );
if ( $settings->has( 'vault_enabled_dcc' ) && $settings->get( 'vault_enabled_dcc' ) ) {
$supports[] = 'tokenization';
}
return $supports;
}
);
add_action(
'wc_ajax_' . CaptureCardPayment::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'save-payment-methods.endpoint.capture-card-payment' );
assert( $endpoint instanceof CaptureCardPayment );
$endpoint->handle_request();
}
);
}
/**
* Adds id token to localized script data.
*
* @param UserIdToken $api User id token api.
* @param LoggerInterface $logger The logger.
* @param array $localized_script_data The localized script data.
* @return array
*/
private function add_id_token_to_script_data(
UserIdToken $api,
LoggerInterface $logger,
array $localized_script_data
): array {
try {
$target_customer_id = '';
if ( is_user_logged_in() ) {
$target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
}
$id_token = $api->id_token( $target_customer_id );
$localized_script_data['save_payment_methods'] = array(
'id_token' => $id_token,
);
$localized_script_data['data_client_id']['set_attribute'] = false;
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger->error( $error );
}
return $localized_script_data;
}
}

View file

@ -0,0 +1,102 @@
<?php
/**
* Service to create WC Payment Tokens.
*
* @package WooCommerce\PayPalCommerce\Applepay
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods;
use Exception;
use Psr\Log\LoggerInterface;
use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenFactory;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenPayPal;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Class WooCommercePaymentTokens
*/
class WooCommercePaymentTokens {
/**
* The payment token helper.
*
* @var PaymentTokenHelper
*/
private $payment_token_helper;
/**
* The payment token factory.
*
* @var PaymentTokenFactory
*/
private $payment_token_factory;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* WooCommercePaymentTokens constructor.
*
* @param PaymentTokenHelper $payment_token_helper The payment token helper.
* @param PaymentTokenFactory $payment_token_factory The payment token factory.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
PaymentTokenHelper $payment_token_helper,
PaymentTokenFactory $payment_token_factory,
LoggerInterface $logger
) {
$this->payment_token_helper = $payment_token_helper;
$this->payment_token_factory = $payment_token_factory;
$this->logger = $logger;
}
/**
* Creates a WC Payment Token for PayPal payment.
*
* @param int $customer_id The WC customer ID.
* @param string $token The PayPal payment token.
* @param string $email The PayPal customer email.
*
* @return void
*/
public function create_payment_token_paypal(
int $customer_id,
string $token,
string $email
): void {
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, PayPalGateway::ID );
if ( $this->payment_token_helper->token_exist( $wc_tokens, $token ) ) {
return;
}
$payment_token_paypal = $this->payment_token_factory->create( 'paypal' );
assert( $payment_token_paypal instanceof PaymentTokenPayPal );
$payment_token_paypal->set_token( $token );
$payment_token_paypal->set_user_id( $customer_id );
$payment_token_paypal->set_gateway_id( PayPalGateway::ID );
if ( $email && is_email( $email ) ) {
$payment_token_paypal->set_email( $email );
}
try {
$payment_token_paypal->save();
} catch ( Exception $exception ) {
$this->logger->error(
"Could not create WC payment token PayPal for customer {$customer_id}. " . $exception->getMessage()
);
}
}
}

View file

@ -0,0 +1,38 @@
const path = require('path');
const isProduction = process.env.NODE_ENV === 'production';
const DependencyExtractionWebpackPlugin = require( '@woocommerce/dependency-extraction-webpack-plugin' );
module.exports = {
devtool: isProduction ? 'source-map' : 'eval-source-map',
mode: isProduction ? 'production' : 'development',
target: 'web',
plugins: [ new DependencyExtractionWebpackPlugin() ],
entry: {
'add-payment-method': path.resolve('./resources/js/add-payment-method.js')
},
output: {
path: path.resolve(__dirname, 'assets/'),
filename: 'js/[name].js',
},
module: {
rules: [{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
{
loader: 'file-loader',
options: {
name: 'css/[name].css',
}
},
{loader:'sass-loader'}
]
}]
}
};

File diff suppressed because it is too large Load diff

View file

@ -10,12 +10,12 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavedPaymentChecker; namespace WooCommerce\PayPalCommerce\SavedPaymentChecker;
use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array( return array(
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array { 'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
$subscription_helper = $container->get( 'subscription.helper' ); $subscription_helper = $container->get( 'wc-subscriptions.helper' );
assert( $subscription_helper instanceof SubscriptionHelper ); assert( $subscription_helper instanceof SubscriptionHelper );
$insert_after = function( array $array, string $key, array $new ): array { $insert_after = function( array $array, string $key, array $new ): array {

View file

@ -16,7 +16,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokenEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokenEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;

View file

@ -11,7 +11,7 @@ namespace WooCommerce\PayPalCommerce\SavedPaymentChecker;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WC_Order; use WC_Order;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface; use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
@ -43,7 +43,7 @@ class SavedPaymentCheckerModule implements ModuleInterface {
add_filter( add_filter(
'woocommerce_paypal_payments_order_intent', 'woocommerce_paypal_payments_order_intent',
function( string $intent ) use ( $c ) { function( string $intent ) use ( $c ) {
$subscription_helper = $c->get( 'subscription.helper' ); $subscription_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscription_helper instanceof SubscriptionHelper ); assert( $subscription_helper instanceof SubscriptionHelper );
if ( $subscription_helper->cart_contains_subscription() || $subscription_helper->current_product_is_subscription() ) { if ( $subscription_helper->cart_contains_subscription() || $subscription_helper->current_product_is_subscription() ) {
@ -60,7 +60,7 @@ class SavedPaymentCheckerModule implements ModuleInterface {
add_action( add_action(
'woocommerce_paypal_payments_before_handle_payment_success', 'woocommerce_paypal_payments_before_handle_payment_success',
function( WC_Order $wc_order ) use ( $c ) { function( WC_Order $wc_order ) use ( $c ) {
$subscription_helper = $c->get( 'subscription.helper' ); $subscription_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscription_helper instanceof SubscriptionHelper ); assert( $subscription_helper instanceof SubscriptionHelper );
if ( $subscription_helper->has_subscription( $wc_order->get_id() ) ) { if ( $subscription_helper->has_subscription( $wc_order->get_id() ) ) {
@ -93,7 +93,7 @@ class SavedPaymentCheckerModule implements ModuleInterface {
add_action( add_action(
'woocommerce_email_before_order_table', 'woocommerce_email_before_order_table',
function( WC_Order $order ) use ( $c ) { function( WC_Order $order ) use ( $c ) {
$subscription_helper = $c->get( 'subscription.helper' ); $subscription_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscription_helper instanceof SubscriptionHelper ); assert( $subscription_helper instanceof SubscriptionHelper );
$logger = $c->get( 'woocommerce.logger.woocommerce' ); $logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface ); assert( $logger instanceof LoggerInterface );
@ -119,7 +119,7 @@ class SavedPaymentCheckerModule implements ModuleInterface {
add_action( add_action(
'woocommerce_email_after_order_table', 'woocommerce_email_after_order_table',
function( WC_Order $order ) use ( $c ) { function( WC_Order $order ) use ( $c ) {
$subscription_helper = $c->get( 'subscription.helper' ); $subscription_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscription_helper instanceof SubscriptionHelper ); assert( $subscription_helper instanceof SubscriptionHelper );
$logger = $c->get( 'woocommerce.logger.woocommerce' ); $logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface ); assert( $logger instanceof LoggerInterface );

View file

@ -9,7 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\StatusReport; namespace WooCommerce\PayPalCommerce\StatusReport;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface; use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
@ -65,7 +65,7 @@ class StatusReportModule implements ModuleInterface {
$messages_apply = $c->get( 'button.helper.messages-apply' ); $messages_apply = $c->get( 'button.helper.messages-apply' );
/* @var SubscriptionHelper $subscription_helper The subscription helper class. */ /* @var SubscriptionHelper $subscription_helper The subscription helper class. */
$subscription_helper = $c->get( 'subscription.helper' ); $subscription_helper = $c->get( 'wc-subscriptions.helper' );
$last_webhook_storage = $c->get( 'webhook.last-webhook-storage' ); $last_webhook_storage = $c->get( 'webhook.last-webhook-storage' );
assert( $last_webhook_storage instanceof WebhookEventStorage ); assert( $last_webhook_storage instanceof WebhookEventStorage );

View file

@ -1,74 +0,0 @@
<?php
/**
* The services
*
* @package WooCommerce\PayPalCommerce\Subscription
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
return array(
'subscription.helper' => static function ( ContainerInterface $container ): SubscriptionHelper {
return new SubscriptionHelper( $container->get( 'wcgateway.settings' ) );
},
'subscription.renewal-handler' => static function ( ContainerInterface $container ): RenewalHandler {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$repository = $container->get( 'vaulting.repository.payment-token' );
$endpoint = $container->get( 'api.endpoint.order' );
$purchase_unit_factory = $container->get( 'api.factory.purchase-unit' );
$payer_factory = $container->get( 'api.factory.payer' );
$environment = $container->get( 'onboarding.environment' );
$settings = $container->get( 'wcgateway.settings' );
$authorized_payments_processor = $container->get( 'wcgateway.processor.authorized-payments' );
return new RenewalHandler(
$logger,
$repository,
$endpoint,
$purchase_unit_factory,
$container->get( 'api.factory.shipping-preference' ),
$payer_factory,
$environment,
$settings,
$authorized_payments_processor
);
},
'subscription.repository.payment-token' => static function ( ContainerInterface $container ): PaymentTokenRepository {
$factory = $container->get( 'api.factory.payment-token' );
$endpoint = $container->get( 'api.endpoint.payment-token' );
return new PaymentTokenRepository( $factory, $endpoint );
},
'subscription.api-handler' => static function( ContainerInterface $container ): SubscriptionsApiHandler {
return new SubscriptionsApiHandler(
$container->get( 'api.endpoint.catalog-products' ),
$container->get( 'api.factory.product' ),
$container->get( 'api.endpoint.billing-plans' ),
$container->get( 'api.factory.billing-cycle' ),
$container->get( 'api.factory.payment-preferences' ),
$container->get( 'api.shop.currency' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'subscription.module.url' => static function ( ContainerInterface $container ): string {
/**
* The path cannot be false.
*
* @psalm-suppress PossiblyFalseArgument
*/
return plugins_url(
'/modules/ppcp-subscription/',
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'subscription.deactivate-plan-endpoint' => static function ( ContainerInterface $container ): DeactivatePlanEndpoint {
return new DeactivatePlanEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.billing-plans' )
);
},
);

View file

@ -31,7 +31,7 @@ return array(
}, },
'vaulting.credit-card-handler' => function( ContainerInterface $container ): VaultedCreditCardHandler { 'vaulting.credit-card-handler' => function( ContainerInterface $container ): VaultedCreditCardHandler {
return new VaultedCreditCardHandler( return new VaultedCreditCardHandler(
$container->get( 'subscription.helper' ), $container->get( 'wc-subscriptions.helper' ),
$container->get( 'vaulting.repository.payment-token' ), $container->get( 'vaulting.repository.payment-token' ),
$container->get( 'api.factory.purchase-unit' ), $container->get( 'api.factory.purchase-unit' ),
$container->get( 'api.factory.payer' ), $container->get( 'api.factory.payer' ),

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